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.ui.text 18 19 import androidx.compose.ui.geometry.Offset 20 import androidx.compose.ui.geometry.Rect 21 import androidx.compose.ui.graphics.Path 22 import androidx.compose.ui.text.font.Font 23 import androidx.compose.ui.text.font.FontFamily 24 import androidx.compose.ui.text.font.createFontFamilyResolver 25 import androidx.compose.ui.text.font.toFontFamily 26 import androidx.compose.ui.text.platform.SynchronizedObject 27 import androidx.compose.ui.text.platform.makeSynchronizedObject 28 import androidx.compose.ui.text.platform.synchronized 29 import androidx.compose.ui.text.style.ResolvedTextDirection 30 import androidx.compose.ui.text.style.TextOverflow 31 import androidx.compose.ui.unit.Constraints 32 import androidx.compose.ui.unit.Density 33 import androidx.compose.ui.unit.IntSize 34 import androidx.compose.ui.unit.LayoutDirection 35 36 /** The data class which holds the set of parameters of the text layout computation. */ 37 class TextLayoutInput 38 private constructor( 39 /** The text used for computing text layout. */ 40 val text: AnnotatedString, 41 42 /** The text layout used for computing this text layout. */ 43 val style: TextStyle, 44 45 /** 46 * A list of [Placeholder]s inserted into text layout that reserves space to embed icons or 47 * custom emojis. A list of bounding boxes will be returned in 48 * [TextLayoutResult.placeholderRects] that corresponds to this input. 49 * 50 * @see TextLayoutResult.placeholderRects 51 * @see MultiParagraph 52 * @see MultiParagraphIntrinsics 53 */ 54 val placeholders: List<AnnotatedString.Range<Placeholder>>, 55 56 /** The maxLines param used for computing this text layout. */ 57 val maxLines: Int, 58 59 /** The maxLines param used for computing this text layout. */ 60 val softWrap: Boolean, 61 62 /** The overflow param used for computing this text layout */ 63 val overflow: TextOverflow, 64 65 /** The density param used for computing this text layout. */ 66 val density: Density, 67 68 /** The layout direction used for computing this text layout. */ 69 val layoutDirection: LayoutDirection, 70 71 /** 72 * The font resource loader used for computing this text layout. 73 * 74 * This is no longer used. 75 * 76 * @see fontFamilyResolver 77 */ 78 @Suppress("DEPRECATION") resourceLoader: Font.ResourceLoader?, 79 80 /** The font resolver used for computing this text layout. */ 81 val fontFamilyResolver: FontFamily.Resolver, 82 83 /** The minimum width provided while calculating this text layout. */ 84 val constraints: Constraints 85 ) { 86 87 private var _developerSuppliedResourceLoader = resourceLoader 88 @Deprecated( 89 "Replaced with FontFamily.Resolver", 90 replaceWith = ReplaceWith("fontFamilyResolver"), 91 ) 92 @Suppress("DEPRECATION") 93 val resourceLoader: Font.ResourceLoader 94 get() { 95 return _developerSuppliedResourceLoader 96 ?: DeprecatedBridgeFontResourceLoader.from(fontFamilyResolver) 97 } 98 99 @Deprecated( 100 "Font.ResourceLoader is replaced with FontFamily.Resolver", 101 replaceWith = 102 ReplaceWith( 103 "TextLayoutInput(text, style, placeholders, " + 104 "maxLines, softWrap, overflow, density, layoutDirection, fontFamilyResolver, " + 105 "constraints" 106 ) 107 ) 108 @Suppress("DEPRECATION") 109 constructor( 110 text: AnnotatedString, 111 style: TextStyle, 112 placeholders: List<AnnotatedString.Range<Placeholder>>, 113 maxLines: Int, 114 softWrap: Boolean, 115 overflow: TextOverflow, 116 density: Density, 117 layoutDirection: LayoutDirection, 118 resourceLoader: Font.ResourceLoader, 119 constraints: Constraints 120 ) : this( 121 text, 122 style, 123 placeholders, 124 maxLines, 125 softWrap, 126 overflow, 127 density, 128 layoutDirection, 129 resourceLoader, 130 createFontFamilyResolver(resourceLoader), 131 constraints 132 ) 133 134 constructor( 135 text: AnnotatedString, 136 style: TextStyle, 137 placeholders: List<AnnotatedString.Range<Placeholder>>, 138 maxLines: Int, 139 softWrap: Boolean, 140 overflow: TextOverflow, 141 density: Density, 142 layoutDirection: LayoutDirection, 143 fontFamilyResolver: FontFamily.Resolver, 144 constraints: Constraints 145 ) : this( 146 text, 147 style, 148 placeholders, 149 maxLines, 150 softWrap, 151 overflow, 152 density, 153 layoutDirection, 154 @Suppress("DEPRECATION") null, 155 fontFamilyResolver, 156 constraints 157 ) 158 159 @Deprecated( 160 "Font.ResourceLoader is deprecated", 161 replaceWith = 162 ReplaceWith( 163 "TextLayoutInput(text, style, placeholders," + 164 " maxLines, softWrap, overFlow, density, layoutDirection, fontFamilyResolver, " + 165 "constraints)" 166 ) 167 ) 168 // Unfortunately, there's no way to deprecate and add a parameter to a copy chain such that the 169 // resolution is valid. 170 // 171 // However, as this was never intended to be a public function we will not replace it. There is 172 // no use case for calling this method directly. copynull173 fun copy( 174 text: AnnotatedString = this.text, 175 style: TextStyle = this.style, 176 placeholders: List<AnnotatedString.Range<Placeholder>> = this.placeholders, 177 maxLines: Int = this.maxLines, 178 softWrap: Boolean = this.softWrap, 179 overflow: TextOverflow = this.overflow, 180 density: Density = this.density, 181 layoutDirection: LayoutDirection = this.layoutDirection, 182 @Suppress("DEPRECATION") resourceLoader: Font.ResourceLoader = this.resourceLoader, 183 constraints: Constraints = this.constraints 184 ): TextLayoutInput { 185 return TextLayoutInput( 186 text = text, 187 style = style, 188 placeholders = placeholders, 189 maxLines = maxLines, 190 softWrap = softWrap, 191 overflow = overflow, 192 density = density, 193 layoutDirection = layoutDirection, 194 resourceLoader = resourceLoader, 195 fontFamilyResolver = fontFamilyResolver, 196 constraints = constraints 197 ) 198 } 199 equalsnull200 override fun equals(other: Any?): Boolean { 201 if (this === other) return true 202 if (other !is TextLayoutInput) return false 203 204 if (text != other.text) return false 205 if (style != other.style) return false 206 if (placeholders != other.placeholders) return false 207 if (maxLines != other.maxLines) return false 208 if (softWrap != other.softWrap) return false 209 if (overflow != other.overflow) return false 210 if (density != other.density) return false 211 if (layoutDirection != other.layoutDirection) return false 212 if (fontFamilyResolver != other.fontFamilyResolver) return false 213 if (constraints != other.constraints) return false 214 215 return true 216 } 217 hashCodenull218 override fun hashCode(): Int { 219 var result = text.hashCode() 220 result = 31 * result + style.hashCode() 221 result = 31 * result + placeholders.hashCode() 222 result = 31 * result + maxLines 223 result = 31 * result + softWrap.hashCode() 224 result = 31 * result + overflow.hashCode() 225 result = 31 * result + density.hashCode() 226 result = 31 * result + layoutDirection.hashCode() 227 result = 31 * result + fontFamilyResolver.hashCode() 228 result = 31 * result + constraints.hashCode() 229 return result 230 } 231 toStringnull232 override fun toString(): String { 233 return "TextLayoutInput(" + 234 "text=$text, " + 235 "style=$style, " + 236 "placeholders=$placeholders, " + 237 "maxLines=$maxLines, " + 238 "softWrap=$softWrap, " + 239 "overflow=$overflow, " + 240 "density=$density, " + 241 "layoutDirection=$layoutDirection, " + 242 "fontFamilyResolver=$fontFamilyResolver, " + 243 "constraints=$constraints" + 244 ")" 245 } 246 } 247 248 @Suppress("DEPRECATION") 249 private class DeprecatedBridgeFontResourceLoader 250 private constructor(private val fontFamilyResolver: FontFamily.Resolver) : Font.ResourceLoader { 251 @Deprecated( 252 "Replaced by FontFamily.Resolver, this method should not be called", 253 ReplaceWith("FontFamily.Resolver.resolve(font, )"), 254 ) loadnull255 override fun load(font: Font): Any { 256 return fontFamilyResolver.resolve(font.toFontFamily(), font.weight, font.style).value 257 } 258 259 companion object { 260 // In normal usage will be a map of size 1. 261 // 262 // To fill this map with a large number of entries an app must: 263 // 264 // 1. Repeatedly change FontFamily.Resolver 265 // 2. Call the deprecated method getFontResourceLoader on TextLayoutInput 266 // 267 // If this map is found to be large in profiling of an app, please modify your code to not 268 // call getFontResourceLoader, and evaluate if FontFamily.Resolver is being correctly cached 269 // (via e.g. remember) 270 var cache = mutableMapOf<FontFamily.Resolver, Font.ResourceLoader>() 271 val lock: SynchronizedObject = makeSynchronizedObject() 272 fromnull273 fun from(fontFamilyResolver: FontFamily.Resolver): Font.ResourceLoader { 274 synchronized(lock) { 275 // the same resolver to return the same ResourceLoader 276 cache[fontFamilyResolver]?.let { 277 return it 278 } 279 280 val deprecatedBridgeFontResourceLoader = 281 DeprecatedBridgeFontResourceLoader(fontFamilyResolver) 282 cache[fontFamilyResolver] = deprecatedBridgeFontResourceLoader 283 return deprecatedBridgeFontResourceLoader 284 } 285 } 286 } 287 } 288 289 /** The data class which holds text layout result. */ 290 class TextLayoutResult 291 constructor( 292 /** The parameters used for computing this text layout result. */ 293 val layoutInput: TextLayoutInput, 294 295 /** 296 * The multi paragraph object. 297 * 298 * This is the result of the text layout computation. 299 */ 300 val multiParagraph: MultiParagraph, 301 302 /** The amount of space required to paint this text in Int. */ 303 val size: IntSize 304 ) { 305 /** The distance from the top to the alphabetic baseline of the first line. */ 306 val firstBaseline: Float = multiParagraph.firstBaseline 307 308 /** The distance from the top to the alphabetic baseline of the last line. */ 309 val lastBaseline: Float = multiParagraph.lastBaseline 310 311 /** Returns true if the text is too tall and couldn't fit with given height. */ 312 val didOverflowHeight: Boolean 313 get() = multiParagraph.didExceedMaxLines || size.height < multiParagraph.height 314 315 /** Returns true if the text is too wide and couldn't fit with given width. */ 316 val didOverflowWidth: Boolean 317 get() = size.width < multiParagraph.width 318 319 /** Returns true if either vertical overflow or horizontal overflow happens. */ 320 val hasVisualOverflow: Boolean 321 get() = didOverflowWidth || didOverflowHeight 322 323 /** 324 * Returns a list of bounding boxes that is reserved for [TextLayoutInput.placeholders]. Each 325 * [Rect] in this list corresponds to the [Placeholder] passed to [TextLayoutInput.placeholders] 326 * and it will have the height and width specified in the [Placeholder]. It's guaranteed that 327 * [TextLayoutInput.placeholders] and [TextLayoutResult.placeholderRects] will have same length 328 * and order. 329 * 330 * @see TextLayoutInput.placeholders 331 * @see Placeholder 332 */ 333 val placeholderRects: List<Rect?> = multiParagraph.placeholderRects 334 335 /** Returns a number of lines of this text layout */ 336 val lineCount: Int 337 get() = multiParagraph.lineCount 338 339 /** 340 * Returns the start offset of the given line, inclusive. 341 * 342 * The start offset represents a position in text before the first character in the given line. 343 * For example, `getLineStart(1)` will return 4 for the text below 344 * <pre> 345 * ┌────┐ 346 * │abcd│ 347 * │efg │ 348 * └────┘ 349 * </pre> 350 * 351 * @param lineIndex the line number 352 * @return the start offset of the line 353 */ getLineStartnull354 fun getLineStart(lineIndex: Int): Int = multiParagraph.getLineStart(lineIndex) 355 356 /** 357 * Returns the end offset of the given line. 358 * 359 * The end offset represents a position in text after the last character in the given line. For 360 * example, `getLineEnd(0)` will return 4 for the text below 361 * <pre> 362 * ┌────┐ 363 * │abcd│ 364 * │efg │ 365 * └────┘ 366 * </pre> 367 * 368 * Characters being ellipsized are treated as invisible characters. So that if visibleEnd is 369 * false, it will return line end including the ellipsized characters and vice versa. 370 * 371 * @param lineIndex the line number 372 * @param visibleEnd if true, the returned line end will not count trailing whitespaces or 373 * linefeed characters. Otherwise, this function will return the logical line end. By default 374 * it's false. 375 * @return an exclusive end offset of the line. 376 */ 377 fun getLineEnd(lineIndex: Int, visibleEnd: Boolean = false): Int = 378 multiParagraph.getLineEnd(lineIndex, visibleEnd) 379 380 /** 381 * Returns true if the given line is ellipsized, otherwise returns false. 382 * 383 * @param lineIndex a 0 based line index 384 * @return true if the given line is ellipsized, otherwise false 385 */ 386 fun isLineEllipsized(lineIndex: Int): Boolean = multiParagraph.isLineEllipsized(lineIndex) 387 388 /** 389 * Returns the top y coordinate of the given line. 390 * 391 * @param lineIndex the line number 392 * @return the line top y coordinate 393 */ 394 fun getLineTop(lineIndex: Int): Float = multiParagraph.getLineTop(lineIndex) 395 396 /** 397 * Returns the distance in pixels from the top of the text layout to the alphabetic baseline of 398 * the line at index [lineIndex]. 399 */ 400 fun getLineBaseline(lineIndex: Int): Float = multiParagraph.getLineBaseline(lineIndex) 401 402 /** 403 * Returns the bottom y coordinate of the given line. 404 * 405 * @param lineIndex the line number 406 * @return the line bottom y coordinate 407 */ 408 fun getLineBottom(lineIndex: Int): Float = multiParagraph.getLineBottom(lineIndex) 409 410 /** 411 * Returns the left x coordinate of the given line. 412 * 413 * @param lineIndex the line number 414 * @return the line left x coordinate 415 */ 416 fun getLineLeft(lineIndex: Int): Float = multiParagraph.getLineLeft(lineIndex) 417 418 /** 419 * Returns the right x coordinate of the given line. 420 * 421 * @param lineIndex the line number 422 * @return the line right x coordinate 423 */ 424 fun getLineRight(lineIndex: Int): Float = multiParagraph.getLineRight(lineIndex) 425 426 /** 427 * Returns the line number on which the specified text offset appears. 428 * 429 * If you ask for a position before 0, you get 0; if you ask for a position beyond the end of 430 * the text, you get the last line. 431 * 432 * @param offset a character offset 433 * @return the 0 origin line number. 434 */ 435 fun getLineForOffset(offset: Int): Int = multiParagraph.getLineForOffset(offset) 436 437 /** 438 * Returns line number closest to the given graphical vertical position. 439 * 440 * If you ask for a vertical position before 0, you get 0; if you ask for a vertical position 441 * beyond the last line, you get the last line. 442 * 443 * @param vertical the vertical position 444 * @return the 0 origin line number. 445 */ 446 fun getLineForVerticalPosition(vertical: Float): Int = 447 multiParagraph.getLineForVerticalPosition(vertical) 448 449 /** 450 * Get the horizontal position for the specified text [offset]. 451 * 452 * Returns the relative distance from the text starting offset. For example, if the paragraph 453 * direction is Left-to-Right, this function returns positive value as a distance from the 454 * left-most edge. If the paragraph direction is Right-to-Left, this function returns negative 455 * value as a distance from the right-most edge. 456 * 457 * [usePrimaryDirection] argument is taken into account only when the offset is in the BiDi 458 * directional transition point. [usePrimaryDirection] is true means use the primary direction 459 * run's coordinate, and use the secondary direction's run's coordinate if false. 460 * 461 * @param offset a character offset 462 * @param usePrimaryDirection true for using the primary run's coordinate if the given offset is 463 * in the BiDi directional transition point. 464 * @return the relative distance from the text starting edge. 465 * @see MultiParagraph.getHorizontalPosition 466 */ 467 fun getHorizontalPosition(offset: Int, usePrimaryDirection: Boolean): Float = 468 multiParagraph.getHorizontalPosition(offset, usePrimaryDirection) 469 470 /** 471 * Get the text direction of the paragraph containing the given offset. 472 * 473 * @param offset a character offset 474 * @return the paragraph direction 475 */ 476 fun getParagraphDirection(offset: Int): ResolvedTextDirection = 477 multiParagraph.getParagraphDirection(offset) 478 479 /** 480 * Get the text direction of the resolved BiDi run that the character at the given offset 481 * associated with. 482 * 483 * @param offset a character offset 484 * @return the direction of the BiDi run of the given character offset. 485 */ 486 fun getBidiRunDirection(offset: Int): ResolvedTextDirection = 487 multiParagraph.getBidiRunDirection(offset) 488 489 /** 490 * Returns the character offset closest to the given graphical position. 491 * 492 * @param position a graphical position in this text layout 493 * @return a character offset that is closest to the given graphical position. 494 */ 495 fun getOffsetForPosition(position: Offset): Int = multiParagraph.getOffsetForPosition(position) 496 497 /** 498 * Returns the bounding box of the character for given character offset. 499 * 500 * @param offset a character offset 501 * @return a bounding box for the character in pixels. 502 */ 503 fun getBoundingBox(offset: Int): Rect = multiParagraph.getBoundingBox(offset) 504 505 /** 506 * Returns the text range of the word at the given character offset. 507 * 508 * Characters not part of a word, such as spaces, symbols, and punctuation, have word breaks on 509 * both sides. In such cases, this method will return a text range that contains the given 510 * character offset. 511 * 512 * Word boundaries are defined more precisely in Unicode Standard Annex #29 513 * <http://www.unicode.org/reports/tr29/#Word_Boundaries>. 514 */ 515 fun getWordBoundary(offset: Int): TextRange = multiParagraph.getWordBoundary(offset) 516 517 /** 518 * Returns the rectangle of the cursor area 519 * 520 * @param offset An character offset of the cursor 521 * @return a rectangle of cursor region 522 */ 523 fun getCursorRect(offset: Int): Rect = multiParagraph.getCursorRect(offset) 524 525 /** 526 * Returns path that enclose the given text range. 527 * 528 * @param start an inclusive start character offset 529 * @param end an exclusive end character offset 530 * @return a drawing path 531 */ 532 fun getPathForRange(start: Int, end: Int): Path = multiParagraph.getPathForRange(start, end) 533 534 fun copy( 535 layoutInput: TextLayoutInput = this.layoutInput, 536 size: IntSize = this.size 537 ): TextLayoutResult { 538 return TextLayoutResult( 539 layoutInput = layoutInput, 540 multiParagraph = multiParagraph, 541 size = size 542 ) 543 } 544 equalsnull545 override fun equals(other: Any?): Boolean { 546 if (this === other) return true 547 if (other !is TextLayoutResult) return false 548 549 if (layoutInput != other.layoutInput) return false 550 if (multiParagraph != other.multiParagraph) return false 551 if (size != other.size) return false 552 if (firstBaseline != other.firstBaseline) return false 553 if (lastBaseline != other.lastBaseline) return false 554 if (placeholderRects != other.placeholderRects) return false 555 556 return true 557 } 558 hashCodenull559 override fun hashCode(): Int { 560 var result = layoutInput.hashCode() 561 result = 31 * result + multiParagraph.hashCode() 562 result = 31 * result + size.hashCode() 563 result = 31 * result + firstBaseline.hashCode() 564 result = 31 * result + lastBaseline.hashCode() 565 result = 31 * result + placeholderRects.hashCode() 566 return result 567 } 568 toStringnull569 override fun toString(): String { 570 return "TextLayoutResult(" + 571 "layoutInput=$layoutInput, " + 572 "multiParagraph=$multiParagraph, " + 573 "size=$size, " + 574 "firstBaseline=$firstBaseline, " + 575 "lastBaseline=$lastBaseline, " + 576 "placeholderRects=$placeholderRects" + 577 ")" 578 } 579 } 580