1 /*
<lambda>null2  * Copyright 2021 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.platform
18 
19 import android.graphics.Typeface
20 import android.text.SpannableStringBuilder
21 import android.text.style.ForegroundColorSpan
22 import androidx.compose.ui.graphics.Brush
23 import androidx.compose.ui.graphics.Color
24 import androidx.compose.ui.graphics.SolidColor
25 import androidx.compose.ui.graphics.toArgb
26 import androidx.compose.ui.text.AnnotatedString
27 import androidx.compose.ui.text.SpanStyle
28 import androidx.compose.ui.text.TextStyle
29 import androidx.compose.ui.text.font.FontStyle
30 import androidx.compose.ui.text.font.FontWeight
31 import androidx.compose.ui.text.matchers.assertThat
32 import androidx.compose.ui.text.platform.extensions.flattenFontStylesAndApply
33 import androidx.compose.ui.text.platform.extensions.setSpanStyles
34 import androidx.compose.ui.text.platform.style.ShaderBrushSpan
35 import androidx.compose.ui.unit.Density
36 import androidx.compose.ui.unit.sp
37 import androidx.test.ext.junit.runners.AndroidJUnit4
38 import androidx.test.filters.SmallTest
39 import org.junit.Test
40 import org.junit.runner.RunWith
41 import org.mockito.ArgumentMatchers.anyInt
42 import org.mockito.kotlin.any
43 import org.mockito.kotlin.argThat
44 import org.mockito.kotlin.eq
45 import org.mockito.kotlin.inOrder
46 import org.mockito.kotlin.mock
47 import org.mockito.kotlin.never
48 import org.mockito.kotlin.times
49 import org.mockito.kotlin.verify
50 
51 @RunWith(AndroidJUnit4::class)
52 @SmallTest
53 class SpannableExtensionsTest {
54     @Test
55     fun flattenStylesAndApply_emptyList() {
56         val spanStyles = listOf<AnnotatedString.Range<SpanStyle>>()
57         val block = mock<(SpanStyle, Int, Int) -> Unit>()
58         flattenFontStylesAndApply(
59             contextFontSpanStyle = null,
60             spanStyles = spanStyles,
61             block = block
62         )
63 
64         verify(block, never()).invoke(any(), anyInt(), anyInt())
65     }
66 
67     @Test
68     fun flattenStylesAndApply_oneStyle() {
69         val spanStyle = SpanStyle(fontWeight = FontWeight(123))
70         val start = 4
71         val end = 10
72         val spanStyles = listOf(AnnotatedString.Range(spanStyle, start, end))
73         val block = mock<(SpanStyle, Int, Int) -> Unit>()
74         flattenFontStylesAndApply(
75             contextFontSpanStyle = null,
76             spanStyles = spanStyles,
77             block = block
78         )
79         verify(block, times(1)).invoke(spanStyle, start, end)
80     }
81 
82     @Test
83     fun flattenStylesAndApply_containedByOldStyle() {
84         val spanStyle1 = SpanStyle(fontWeight = FontWeight(123))
85         val spanStyle2 = SpanStyle(fontStyle = FontStyle.Italic)
86 
87         val spanStyles =
88             listOf(
89                 AnnotatedString.Range(spanStyle1, 3, 10),
90                 AnnotatedString.Range(spanStyle2, 4, 6)
91             )
92         val block = mock<(SpanStyle, Int, Int) -> Unit>()
93         flattenFontStylesAndApply(
94             contextFontSpanStyle = null,
95             spanStyles = spanStyles,
96             block = block
97         )
98         inOrder(block) {
99             verify(block).invoke(spanStyle1, 3, 4)
100             verify(block).invoke(spanStyle1.merge(spanStyle2), 4, 6)
101             verify(block).invoke(spanStyle1, 6, 10)
102             verifyNoMoreInteractions()
103         }
104     }
105 
106     @Test
107     fun flattenStylesAndApply_containedByOldStyle_sharedStart() {
108         val spanStyle1 = SpanStyle(fontWeight = FontWeight(123))
109         val spanStyle2 = SpanStyle(fontStyle = FontStyle.Italic)
110 
111         val spanStyles =
112             listOf(
113                 AnnotatedString.Range(spanStyle1, 3, 10),
114                 AnnotatedString.Range(spanStyle2, 3, 6)
115             )
116         val block = mock<(SpanStyle, Int, Int) -> Unit>()
117         flattenFontStylesAndApply(
118             contextFontSpanStyle = null,
119             spanStyles = spanStyles,
120             block = block
121         )
122         inOrder(block) {
123             verify(block).invoke(spanStyle1.merge(spanStyle2), 3, 6)
124             verify(block).invoke(spanStyle1, 6, 10)
125             verifyNoMoreInteractions()
126         }
127     }
128 
129     @Test
130     fun flattenStylesAndApply_containedByOldStyle_sharedEnd() {
131         val spanStyle1 = SpanStyle(fontWeight = FontWeight(123))
132         val spanStyle2 = SpanStyle(fontStyle = FontStyle.Italic)
133 
134         val spanStyles =
135             listOf(
136                 AnnotatedString.Range(spanStyle1, 3, 10),
137                 AnnotatedString.Range(spanStyle2, 5, 10)
138             )
139         val block = mock<(SpanStyle, Int, Int) -> Unit>()
140         flattenFontStylesAndApply(
141             contextFontSpanStyle = null,
142             spanStyles = spanStyles,
143             block = block
144         )
145         inOrder(block) {
146             verify(block).invoke(spanStyle1, 3, 5)
147             verify(block).invoke(spanStyle1.merge(spanStyle2), 5, 10)
148             verifyNoMoreInteractions()
149         }
150     }
151 
152     @Test
153     fun flattenStylesAndApply_sameRange() {
154         val spanStyle1 = SpanStyle(fontWeight = FontWeight(123))
155         val spanStyle2 = SpanStyle(fontStyle = FontStyle.Italic)
156 
157         val spanStyles =
158             listOf(
159                 AnnotatedString.Range(spanStyle1, 3, 10),
160                 AnnotatedString.Range(spanStyle2, 3, 10)
161             )
162         val block = mock<(SpanStyle, Int, Int) -> Unit>()
163         flattenFontStylesAndApply(
164             contextFontSpanStyle = null,
165             spanStyles = spanStyles,
166             block = block
167         )
168         inOrder(block) {
169             verify(block).invoke(spanStyle1.merge(spanStyle2), 3, 10)
170             verifyNoMoreInteractions()
171         }
172     }
173 
174     @Test
175     fun flattenStylesAndApply_overlappingStyles() {
176         val spanStyle1 = SpanStyle(fontWeight = FontWeight(123))
177         val spanStyle2 = SpanStyle(fontStyle = FontStyle.Italic)
178 
179         val spanStyles =
180             listOf(
181                 AnnotatedString.Range(spanStyle1, 3, 10),
182                 AnnotatedString.Range(spanStyle2, 6, 19)
183             )
184         val block = mock<(SpanStyle, Int, Int) -> Unit>()
185         flattenFontStylesAndApply(
186             contextFontSpanStyle = null,
187             spanStyles = spanStyles,
188             block = block
189         )
190         inOrder(block) {
191             verify(block).invoke(spanStyle1, 3, 6)
192             verify(block).invoke(spanStyle1.merge(spanStyle2), 6, 10)
193             verify(block).invoke(spanStyle2, 10, 19)
194             verifyNoMoreInteractions()
195         }
196     }
197 
198     @Test
199     fun flattenStylesAndApply_notIntersectedStyles() {
200         val spanStyle1 = SpanStyle(fontWeight = FontWeight(123))
201         val spanStyle2 = SpanStyle(fontStyle = FontStyle.Italic)
202 
203         val spanStyles =
204             listOf(
205                 AnnotatedString.Range(spanStyle1, 3, 4),
206                 AnnotatedString.Range(spanStyle2, 8, 10)
207             )
208         val block = mock<(SpanStyle, Int, Int) -> Unit>()
209         flattenFontStylesAndApply(
210             contextFontSpanStyle = null,
211             spanStyles = spanStyles,
212             block = block
213         )
214         inOrder(block) {
215             verify(block).invoke(spanStyle1, 3, 4)
216             verify(block).invoke(spanStyle2, 8, 10)
217             verifyNoMoreInteractions()
218         }
219     }
220 
221     @Test
222     fun flattenStylesAndApply_containedByOldStyle_appliedInOrder() {
223         val spanStyle1 = SpanStyle(fontWeight = FontWeight(123))
224         val spanStyle2 = SpanStyle(fontWeight = FontWeight(200))
225 
226         val spanStyles =
227             listOf(
228                 AnnotatedString.Range(spanStyle1, 3, 10),
229                 AnnotatedString.Range(spanStyle2, 5, 9)
230             )
231         val block = mock<(SpanStyle, Int, Int) -> Unit>()
232         flattenFontStylesAndApply(
233             contextFontSpanStyle = null,
234             spanStyles = spanStyles,
235             block = block
236         )
237         inOrder(block) {
238             verify(block).invoke(spanStyle1, 3, 5)
239             // spanStyle2 will overwrite spanStyle1 in [5, 9).
240             verify(block).invoke(spanStyle2, 5, 9)
241             verify(block).invoke(spanStyle1, 9, 10)
242             verifyNoMoreInteractions()
243         }
244     }
245 
246     @Test
247     fun flattenStylesAndApply_containsOldStyle_appliedInOrder() {
248         val spanStyle1 = SpanStyle(fontWeight = FontWeight(123))
249         val spanStyle2 = SpanStyle(fontWeight = FontWeight(200))
250 
251         val spanStyles =
252             listOf(
253                 AnnotatedString.Range(spanStyle1, 5, 7),
254                 AnnotatedString.Range(spanStyle2, 3, 10)
255             )
256         val block = mock<(SpanStyle, Int, Int) -> Unit>()
257         flattenFontStylesAndApply(
258             contextFontSpanStyle = null,
259             spanStyles = spanStyles,
260             block = block
261         )
262         inOrder(block) {
263             // Ideally we can only have 1 spanStyle, but it will overcomplicate the code.
264             verify(block).invoke(spanStyle2, 3, 5)
265             // spanStyle2 will overwrite spanStyle1 in [5, 7).
266             verify(block).invoke(spanStyle2, 5, 7)
267             verify(block).invoke(spanStyle2, 7, 10)
268             verifyNoMoreInteractions()
269         }
270     }
271 
272     @Test
273     fun flattenStylesAndApply_notIntersected_appliedInIndexOrder() {
274         val spanStyle1 = SpanStyle(fontWeight = FontWeight(100))
275         val spanStyle2 = SpanStyle(fontWeight = FontWeight(200))
276         val spanStyle3 = SpanStyle(fontWeight = FontWeight(300))
277 
278         val spanStyles =
279             listOf(
280                 AnnotatedString.Range(spanStyle3, 7, 8),
281                 AnnotatedString.Range(spanStyle2, 3, 4),
282                 AnnotatedString.Range(spanStyle1, 1, 2)
283             )
284         val block = mock<(SpanStyle, Int, Int) -> Unit>()
285         flattenFontStylesAndApply(
286             contextFontSpanStyle = null,
287             spanStyles = spanStyles,
288             block = block
289         )
290         // Despite that spanStyle3 is applied first, the spanStyles are applied in the index order.
291         inOrder(block) {
292             verify(block).invoke(spanStyle1, 1, 2)
293             verify(block).invoke(spanStyle2, 3, 4)
294             verify(block).invoke(spanStyle3, 7, 8)
295             verifyNoMoreInteractions()
296         }
297     }
298 
299     @Test
300     fun flattenStylesAndApply_intersected_appliedInIndexOrder() {
301         val spanStyle1 = SpanStyle(fontWeight = FontWeight(100))
302         val spanStyle2 = SpanStyle(fontWeight = FontWeight(200))
303 
304         val spanStyles =
305             listOf(AnnotatedString.Range(spanStyle1, 5, 9), AnnotatedString.Range(spanStyle2, 3, 6))
306         val block = mock<(SpanStyle, Int, Int) -> Unit>()
307         flattenFontStylesAndApply(
308             contextFontSpanStyle = null,
309             spanStyles = spanStyles,
310             block = block
311         )
312         inOrder(block) {
313             verify(block).invoke(spanStyle2, 3, 5)
314             // SpanStyles are applied in index order, but since spanStyle2 is applied later, it
315             // will overwrite spanStyle1's fontWeight.
316             verify(block).invoke(spanStyle2, 5, 6)
317             verify(block).invoke(spanStyle1, 6, 9)
318             verifyNoMoreInteractions()
319         }
320     }
321 
322     @Test
323     fun flattenStylesAndApply_allEmptyRanges_notApplied() {
324         val contextSpanStyle = SpanStyle(fontWeight = FontWeight(400))
325         val spanStyle1 = SpanStyle(fontWeight = FontWeight(100))
326         val spanStyle2 = SpanStyle(fontWeight = FontWeight(200))
327         val spanStyle3 = SpanStyle(fontWeight = FontWeight(300))
328 
329         val spanStyles =
330             listOf(
331                 AnnotatedString.Range(spanStyle1, 2, 2),
332                 AnnotatedString.Range(spanStyle2, 4, 4),
333                 AnnotatedString.Range(spanStyle3, 0, 0),
334             )
335         val block = mock<(SpanStyle, Int, Int) -> Unit>()
336         flattenFontStylesAndApply(
337             contextFontSpanStyle = contextSpanStyle,
338             spanStyles = spanStyles,
339             block = block
340         )
341         inOrder(block) {
342             verify(block).invoke(contextSpanStyle, 0, 2)
343             verify(block).invoke(contextSpanStyle, 2, 4)
344             verifyNoMoreInteractions()
345         }
346     }
347 
348     @Test
349     fun flattenStylesAndApply_emptySpanRange_shouldNotApply() {
350         val spanStyle1 = SpanStyle(fontWeight = FontWeight(100))
351         val spanStyle2 = SpanStyle(fontStyle = FontStyle.Italic)
352         val spanStyle3 = SpanStyle(fontWeight = FontWeight(200))
353 
354         val spanStyles =
355             listOf(
356                 AnnotatedString.Range(spanStyle3, 4, 10),
357                 AnnotatedString.Range(spanStyle2, 1, 7),
358                 AnnotatedString.Range(spanStyle1, 3, 3)
359             )
360         val block = mock<(SpanStyle, Int, Int) -> Unit>()
361         flattenFontStylesAndApply(
362             contextFontSpanStyle = null,
363             spanStyles = spanStyles,
364             block = block
365         )
366         inOrder(block) {
367             verify(block).invoke(spanStyle2, 1, 3)
368             verify(block).invoke(spanStyle2, 3, 4)
369             verify(block).invoke(spanStyle3.merge(spanStyle2), 4, 7)
370             verify(block).invoke(spanStyle3, 7, 10)
371             verifyNoMoreInteractions()
372         }
373     }
374 
375     @Test
376     fun flattenStylesAndApply_emptySpanRangeBeginning_shouldNotApply() {
377         val spanStyle1 = SpanStyle(fontWeight = FontWeight(100))
378         val spanStyle2 = SpanStyle(fontStyle = FontStyle.Italic)
379 
380         val spanStyles =
381             listOf(AnnotatedString.Range(spanStyle1, 0, 0), AnnotatedString.Range(spanStyle2, 0, 7))
382         val block = mock<(SpanStyle, Int, Int) -> Unit>()
383         flattenFontStylesAndApply(
384             contextFontSpanStyle = null,
385             spanStyles = spanStyles,
386             block = block
387         )
388         inOrder(block) {
389             verify(block).invoke(spanStyle2, 0, 7)
390             verifyNoMoreInteractions()
391         }
392     }
393 
394     @Test
395     fun flattenStylesAndApply_withContextSpanStyle_inheritContext() {
396         val color = Color.Red
397         val fontStyle = FontStyle.Italic
398         val fontWeight = FontWeight(200)
399         val contextSpanStyle = SpanStyle(color = color, fontStyle = fontStyle)
400         val spanStyle = SpanStyle(fontWeight = fontWeight)
401 
402         val spanStyles = listOf(AnnotatedString.Range(spanStyle, 3, 6))
403         val block = mock<(SpanStyle, Int, Int) -> Unit>()
404         flattenFontStylesAndApply(
405             contextFontSpanStyle = contextSpanStyle,
406             spanStyles = spanStyles,
407             block = block
408         )
409         inOrder(block) {
410             verify(block)
411                 .invoke(
412                     argThat {
413                         this ==
414                             SpanStyle(color = color, fontStyle = fontStyle, fontWeight = fontWeight)
415                     },
416                     eq(3),
417                     eq(6)
418                 )
419             verifyNoMoreInteractions()
420         }
421     }
422 
423     @Test
424     fun flattenStylesAndApply_withContextSpanStyle_multipleSpanStyles_inheritContext() {
425         val contextColor = Color.Red
426         val contextFontWeight = FontWeight.Light
427         val contextFontStyle = FontStyle.Normal
428         val contextFontSize = 18.sp
429 
430         val fontWeight = FontWeight.Bold
431         val fontStyle = FontStyle.Italic
432         val fontSize = 24.sp
433         val contextSpanStyle =
434             SpanStyle(
435                 color = contextColor,
436                 fontWeight = contextFontWeight,
437                 fontStyle = contextFontStyle,
438                 fontSize = contextFontSize
439             )
440         val spanStyle1 = SpanStyle(fontWeight = fontWeight)
441         val spanStyle2 = SpanStyle(fontStyle = fontStyle)
442         val spanStyle3 = SpanStyle(fontSize = fontSize)
443 
444         // There will be 5 ranges:
445         //   [2, 4)   contextColor, fontWeight,        contextFontStyle, contextFontSize
446         //   [4, 6)   contextColor, fontWeight,        fontStyle,        contextFontSize
447         //   [6, 8)   contextColor, fontWeight,        fontStyle,        fontSize
448         //   [8, 10)  contextColor, contextFontWeight, fontStyle,        fontSize
449         //   [10, 12) contextColor, contextFontWeight, contextFontStyle, fontSize
450         val spanStyles =
451             listOf(
452                 AnnotatedString.Range(spanStyle1, 2, 8),
453                 AnnotatedString.Range(spanStyle2, 4, 10),
454                 AnnotatedString.Range(spanStyle3, 6, 12),
455             )
456         val block = mock<(SpanStyle, Int, Int) -> Unit>()
457         flattenFontStylesAndApply(
458             contextFontSpanStyle = contextSpanStyle,
459             spanStyles = spanStyles,
460             block = block
461         )
462 
463         inOrder(block) {
464             verify(block)
465                 .invoke(
466                     argThat { this == contextSpanStyle.copy(fontWeight = fontWeight) },
467                     eq(2),
468                     eq(4)
469                 )
470             verify(block)
471                 .invoke(
472                     argThat {
473                         this ==
474                             contextSpanStyle.copy(fontWeight = fontWeight, fontStyle = fontStyle)
475                     },
476                     eq(4),
477                     eq(6)
478                 )
479             verify(block)
480                 .invoke(
481                     argThat {
482                         this ==
483                             contextSpanStyle.copy(
484                                 fontWeight = fontWeight,
485                                 fontStyle = fontStyle,
486                                 fontSize = fontSize
487                             )
488                     },
489                     eq(6),
490                     eq(8)
491                 )
492             verify(block)
493                 .invoke(
494                     argThat {
495                         this == contextSpanStyle.copy(fontStyle = fontStyle, fontSize = fontSize)
496                     },
497                     eq(8),
498                     eq(10)
499                 )
500             verify(block)
501                 .invoke(
502                     argThat { this == contextSpanStyle.copy(fontSize = fontSize) },
503                     eq(10),
504                     eq(12)
505                 )
506             verifyNoMoreInteractions()
507         }
508     }
509 
510     @Test
511     fun shaderBrush_shouldAdd_shaderBrushSpan_whenApplied() {
512         val text = "abcde abcde"
513         val brush = Brush.linearGradient(listOf(Color.Red, Color.Blue))
514         val spanStyle = SpanStyle(brush = brush)
515         val spannable = SpannableStringBuilder().apply { append(text) }
516         spannable.setSpanStyles(
517             contextTextStyle = TextStyle(),
518             annotations = listOf(AnnotatedString.Range(spanStyle, 0, text.length)),
519             density = Density(1f, 1f),
520             resolveTypeface = { _, _, _, _ -> Typeface.DEFAULT }
521         )
522 
523         assertThat(spannable).hasSpan(ShaderBrushSpan::class, 0, text.length) {
524             it.shaderBrush == brush && it.alpha.isNaN()
525         }
526     }
527 
528     @Test
529     fun shaderBrush_shouldAdd_shaderBrushSpan_whenApplied_withSpecifiedAlpha() {
530         val text = "abcde abcde"
531         val brush = Brush.linearGradient(listOf(Color.Red, Color.Blue))
532         val spanStyle = SpanStyle(brush = brush, alpha = 0.6f)
533         val spannable = SpannableStringBuilder().apply { append(text) }
534         spannable.setSpanStyles(
535             contextTextStyle = TextStyle(),
536             annotations = listOf(AnnotatedString.Range(spanStyle, 0, text.length)),
537             density = Density(1f, 1f),
538             resolveTypeface = { _, _, _, _ -> Typeface.DEFAULT }
539         )
540 
541         assertThat(spannable).hasSpan(ShaderBrushSpan::class, 0, text.length) {
542             it.shaderBrush == brush && it.alpha == 0.6f
543         }
544     }
545 
546     @Test
547     fun solidColorBrush_shouldAdd_ForegroundColorSpan_whenApplied() {
548         val text = "abcde abcde"
549         val spanStyle = SpanStyle(brush = SolidColor(Color.Red))
550         val spannable = SpannableStringBuilder().apply { append(text) }
551         spannable.setSpanStyles(
552             contextTextStyle = TextStyle(),
553             annotations = listOf(AnnotatedString.Range(spanStyle, 0, text.length)),
554             density = Density(1f, 1f),
555             resolveTypeface = { _, _, _, _ -> Typeface.DEFAULT }
556         )
557     }
558 
559     @Test
560     fun whenColorAndShaderBrushSpansCollide_bothShouldApply() {
561         val text = "abcde abcde"
562         val brush = Brush.linearGradient(listOf(Color.Red, Color.Blue))
563         val brushStyle = SpanStyle(brush = brush)
564         val colorStyle = SpanStyle(color = Color.Red)
565         val spannable = SpannableStringBuilder().apply { append(text) }
566         spannable.setSpanStyles(
567             contextTextStyle = TextStyle(),
568             annotations =
569                 listOf(
570                     AnnotatedString.Range(brushStyle, 0, text.length),
571                     AnnotatedString.Range(colorStyle, 0, text.length)
572                 ),
573             density = Density(1f, 1f),
574             resolveTypeface = { _, _, _, _ -> Typeface.DEFAULT }
575         )
576 
577         assertThat(spannable).hasSpan(ShaderBrushSpan::class, 0, text.length) {
578             it.shaderBrush == brush
579         }
580         assertThat(spannable).hasSpan(ForegroundColorSpan::class, 0, text.length)
581     }
582 
583     @Test
584     fun whenColorAndSolidColorBrushSpansCollide_bothShouldApply() {
585         val text = "abcde abcde"
586         val brush = SolidColor(Color.Blue)
587         val brushStyle = SpanStyle(brush = brush)
588         val colorStyle = SpanStyle(color = Color.Red)
589         val spannable = SpannableStringBuilder().apply { append(text) }
590         spannable.setSpanStyles(
591             contextTextStyle = TextStyle(),
592             annotations =
593                 listOf(
594                     AnnotatedString.Range(brushStyle, 0, text.length),
595                     AnnotatedString.Range(colorStyle, 0, text.length)
596                 ),
597             density = Density(1f, 1f),
598             resolveTypeface = { _, _, _, _ -> Typeface.DEFAULT }
599         )
600 
601         assertThat(spannable).hasSpan(ForegroundColorSpan::class, 0, text.length) {
602             it.foregroundColor == Color.Blue.toArgb()
603         }
604         assertThat(spannable).hasSpan(ForegroundColorSpan::class, 0, text.length) {
605             it.foregroundColor == Color.Red.toArgb()
606         }
607     }
608 }
609