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