| /* |
| * Copyright 2019 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package androidx.compose.ui.text |
| |
| import androidx.compose.ui.geometry.Offset |
| import androidx.compose.ui.geometry.Rect |
| import androidx.compose.ui.graphics.Path |
| import androidx.compose.ui.text.font.Font |
| import androidx.compose.ui.text.font.FontFamily |
| import androidx.compose.ui.text.font.createFontFamilyResolver |
| import androidx.compose.ui.text.font.toFontFamily |
| import androidx.compose.ui.text.platform.SynchronizedObject |
| import androidx.compose.ui.text.platform.createSynchronizedObject |
| import androidx.compose.ui.text.platform.synchronized |
| import androidx.compose.ui.text.style.ResolvedTextDirection |
| import androidx.compose.ui.text.style.TextOverflow |
| import androidx.compose.ui.unit.Constraints |
| import androidx.compose.ui.unit.Density |
| import androidx.compose.ui.unit.IntSize |
| import androidx.compose.ui.unit.LayoutDirection |
| |
| /** |
| * The data class which holds the set of parameters of the text layout computation. |
| */ |
| class TextLayoutInput private constructor( |
| /** |
| * The text used for computing text layout. |
| */ |
| val text: AnnotatedString, |
| |
| /** |
| * The text layout used for computing this text layout. |
| */ |
| val style: TextStyle, |
| |
| /** |
| * A list of [Placeholder]s inserted into text layout that reserves space to embed icons or |
| * custom emojis. A list of bounding boxes will be returned in |
| * [TextLayoutResult.placeholderRects] that corresponds to this input. |
| * |
| * @see TextLayoutResult.placeholderRects |
| * @see MultiParagraph |
| * @see MultiParagraphIntrinsics |
| */ |
| val placeholders: List<AnnotatedString.Range<Placeholder>>, |
| |
| /** |
| * The maxLines param used for computing this text layout. |
| */ |
| val maxLines: Int, |
| |
| /** |
| * The maxLines param used for computing this text layout. |
| */ |
| val softWrap: Boolean, |
| |
| /** |
| * The overflow param used for computing this text layout |
| */ |
| val overflow: TextOverflow, |
| |
| /** |
| * The density param used for computing this text layout. |
| */ |
| val density: Density, |
| |
| /** |
| * The layout direction used for computing this text layout. |
| */ |
| val layoutDirection: LayoutDirection, |
| |
| /** |
| * The font resource loader used for computing this text layout. |
| * |
| * This is no longer used. |
| * |
| * @see fontFamilyResolver |
| */ |
| |
| @Suppress("DEPRECATION") resourceLoader: Font.ResourceLoader?, |
| |
| /** |
| * The font resolver used for computing this text layout. |
| */ |
| val fontFamilyResolver: FontFamily.Resolver, |
| |
| /** |
| * The minimum width provided while calculating this text layout. |
| */ |
| val constraints: Constraints |
| ) { |
| |
| private var _developerSuppliedResourceLoader = resourceLoader |
| @Deprecated("Replaced with FontFamily.Resolver", |
| replaceWith = ReplaceWith("fontFamilyResolver"), |
| ) |
| @Suppress("DEPRECATION") |
| val resourceLoader: Font.ResourceLoader |
| get() { |
| return _developerSuppliedResourceLoader |
| ?: DeprecatedBridgeFontResourceLoader.from(fontFamilyResolver) |
| } |
| |
| @Deprecated( |
| "Font.ResourceLoader is replaced with FontFamily.Resolver", |
| replaceWith = ReplaceWith("TextLayoutInput(text, style, placeholders, " + |
| "maxLines, softWrap, overflow, density, layoutDirection, fontFamilyResolver, " + |
| "constraints") |
| ) |
| @Suppress("DEPRECATION") |
| constructor( |
| text: AnnotatedString, |
| style: TextStyle, |
| placeholders: List<AnnotatedString.Range<Placeholder>>, |
| maxLines: Int, |
| softWrap: Boolean, |
| overflow: TextOverflow, |
| density: Density, |
| layoutDirection: LayoutDirection, |
| resourceLoader: Font.ResourceLoader, |
| constraints: Constraints |
| ) : this( |
| text, |
| style, |
| placeholders, |
| maxLines, |
| softWrap, |
| overflow, |
| density, |
| layoutDirection, |
| resourceLoader, |
| createFontFamilyResolver(resourceLoader), |
| constraints |
| ) |
| |
| constructor( |
| text: AnnotatedString, |
| style: TextStyle, |
| placeholders: List<AnnotatedString.Range<Placeholder>>, |
| maxLines: Int, |
| softWrap: Boolean, |
| overflow: TextOverflow, |
| density: Density, |
| layoutDirection: LayoutDirection, |
| fontFamilyResolver: FontFamily.Resolver, |
| constraints: Constraints |
| ) : this( |
| text, |
| style, |
| placeholders, |
| maxLines, |
| softWrap, |
| overflow, |
| density, |
| layoutDirection, |
| @Suppress("DEPRECATION") null, |
| fontFamilyResolver, |
| constraints |
| ) |
| |
| @Deprecated("Font.ResourceLoader is deprecated", |
| replaceWith = ReplaceWith("TextLayoutInput(text, style, placeholders," + |
| " maxLines, softWrap, overFlow, density, layoutDirection, fontFamilyResolver, " + |
| "constraints)") |
| ) |
| // Unfortunately, there's no way to deprecate and add a parameter to a copy chain such that the |
| // resolution is valid. |
| // |
| // However, as this was never intended to be a public function we will not replace it. There is |
| // no use case for calling this method directly. |
| fun copy( |
| text: AnnotatedString = this.text, |
| style: TextStyle = this.style, |
| placeholders: List<AnnotatedString.Range<Placeholder>> = this.placeholders, |
| maxLines: Int = this.maxLines, |
| softWrap: Boolean = this.softWrap, |
| overflow: TextOverflow = this.overflow, |
| density: Density = this.density, |
| layoutDirection: LayoutDirection = this.layoutDirection, |
| @Suppress("DEPRECATION") resourceLoader: Font.ResourceLoader = this.resourceLoader, |
| constraints: Constraints = this.constraints |
| ): TextLayoutInput { |
| return TextLayoutInput( |
| text = text, |
| style = style, |
| placeholders = placeholders, |
| maxLines = maxLines, |
| softWrap = softWrap, |
| overflow = overflow, |
| density = density, |
| layoutDirection = layoutDirection, |
| resourceLoader = resourceLoader, |
| fontFamilyResolver = fontFamilyResolver, |
| constraints = constraints |
| ) |
| } |
| |
| override fun equals(other: Any?): Boolean { |
| if (this === other) return true |
| if (other !is TextLayoutInput) return false |
| |
| if (text != other.text) return false |
| if (style != other.style) return false |
| if (placeholders != other.placeholders) return false |
| if (maxLines != other.maxLines) return false |
| if (softWrap != other.softWrap) return false |
| if (overflow != other.overflow) return false |
| if (density != other.density) return false |
| if (layoutDirection != other.layoutDirection) return false |
| if (fontFamilyResolver != other.fontFamilyResolver) return false |
| if (constraints != other.constraints) return false |
| |
| return true |
| } |
| |
| override fun hashCode(): Int { |
| var result = text.hashCode() |
| result = 31 * result + style.hashCode() |
| result = 31 * result + placeholders.hashCode() |
| result = 31 * result + maxLines |
| result = 31 * result + softWrap.hashCode() |
| result = 31 * result + overflow.hashCode() |
| result = 31 * result + density.hashCode() |
| result = 31 * result + layoutDirection.hashCode() |
| result = 31 * result + fontFamilyResolver.hashCode() |
| result = 31 * result + constraints.hashCode() |
| return result |
| } |
| |
| override fun toString(): String { |
| return "TextLayoutInput(" + |
| "text=$text, " + |
| "style=$style, " + |
| "placeholders=$placeholders, " + |
| "maxLines=$maxLines, " + |
| "softWrap=$softWrap, " + |
| "overflow=$overflow, " + |
| "density=$density, " + |
| "layoutDirection=$layoutDirection, " + |
| "fontFamilyResolver=$fontFamilyResolver, " + |
| "constraints=$constraints" + |
| ")" |
| } |
| } |
| |
| @Suppress("DEPRECATION") |
| private class DeprecatedBridgeFontResourceLoader private constructor( |
| private val fontFamilyResolver: FontFamily.Resolver |
| ) : Font.ResourceLoader { |
| @Deprecated( |
| "Replaced by FontFamily.Resolver, this method should not be called", |
| ReplaceWith("FontFamily.Resolver.resolve(font, )"), |
| ) |
| override fun load(font: Font): Any { |
| return fontFamilyResolver.resolve( |
| font.toFontFamily(), |
| font.weight, |
| font.style |
| ).value |
| } |
| |
| companion object { |
| // In normal usage will be a map of size 1. |
| // |
| // To fill this map with a large number of entries an app must: |
| // |
| // 1. Repeatedly change FontFamily.Resolver |
| // 2. Call the deprecated method getFontResourceLoader on TextLayoutInput |
| // |
| // If this map is found to be large in profiling of an app, please modify your code to not |
| // call getFontResourceLoader, and evaluate if FontFamily.Resolver is being correctly cached |
| // (via e.g. remember) |
| var cache = mutableMapOf<FontFamily.Resolver, Font.ResourceLoader>() |
| val lock: SynchronizedObject = createSynchronizedObject() |
| fun from(fontFamilyResolver: FontFamily.Resolver): Font.ResourceLoader { |
| synchronized(lock) { |
| // the same resolver to return the same ResourceLoader |
| cache[fontFamilyResolver]?.let { return it } |
| |
| val deprecatedBridgeFontResourceLoader = DeprecatedBridgeFontResourceLoader( |
| fontFamilyResolver |
| ) |
| cache[fontFamilyResolver] = deprecatedBridgeFontResourceLoader |
| return deprecatedBridgeFontResourceLoader |
| } |
| } |
| } |
| } |
| |
| /** |
| * The data class which holds text layout result. |
| */ |
| class TextLayoutResult constructor( |
| /** |
| * The parameters used for computing this text layout result. |
| */ |
| val layoutInput: TextLayoutInput, |
| |
| /** |
| * The multi paragraph object. |
| * |
| * This is the result of the text layout computation. |
| */ |
| val multiParagraph: MultiParagraph, |
| |
| /** |
| * The amount of space required to paint this text in Int. |
| */ |
| val size: IntSize |
| ) { |
| /** |
| * The distance from the top to the alphabetic baseline of the first line. |
| */ |
| val firstBaseline: Float = multiParagraph.firstBaseline |
| |
| /** |
| * The distance from the top to the alphabetic baseline of the last line. |
| */ |
| val lastBaseline: Float = multiParagraph.lastBaseline |
| |
| /** |
| * Returns true if the text is too tall and couldn't fit with given height. |
| */ |
| val didOverflowHeight: Boolean get() = multiParagraph.didExceedMaxLines || |
| size.height < multiParagraph.height |
| |
| /** |
| * Returns true if the text is too wide and couldn't fit with given width. |
| */ |
| val didOverflowWidth: Boolean get() = size.width < multiParagraph.width |
| |
| /** |
| * Returns true if either vertical overflow or horizontal overflow happens. |
| */ |
| val hasVisualOverflow: Boolean get() = didOverflowWidth || didOverflowHeight |
| |
| /** |
| * Returns a list of bounding boxes that is reserved for [TextLayoutInput.placeholders]. |
| * Each [Rect] in this list corresponds to the [Placeholder] passed to |
| * [TextLayoutInput.placeholders] and it will have the height and width specified in the |
| * [Placeholder]. It's guaranteed that [TextLayoutInput.placeholders] and |
| * [TextLayoutResult.placeholderRects] will have same length and order. |
| * |
| * @see TextLayoutInput.placeholders |
| * @see Placeholder |
| */ |
| val placeholderRects: List<Rect?> = multiParagraph.placeholderRects |
| |
| /** |
| * Returns a number of lines of this text layout |
| */ |
| val lineCount: Int get() = multiParagraph.lineCount |
| |
| /** |
| * Returns the start offset of the given line, inclusive. |
| * |
| * The start offset represents a position in text before the first character in the given line. |
| * For example, `getLineStart(1)` will return 4 for the text below |
| * <pre> |
| * ┌────┐ |
| * │abcd│ |
| * │efg │ |
| * └────┘ |
| * </pre> |
| * |
| * @param lineIndex the line number |
| * @return the start offset of the line |
| */ |
| fun getLineStart(lineIndex: Int): Int = multiParagraph.getLineStart(lineIndex) |
| |
| /** |
| * Returns the end offset of the given line. |
| * |
| * The end offset represents a position in text after the last character in the given line. |
| * For example, `getLineEnd(0)` will return 4 for the text below |
| * <pre> |
| * ┌────┐ |
| * │abcd│ |
| * │efg │ |
| * └────┘ |
| * </pre> |
| * |
| * Characters being ellipsized are treated as invisible characters. So that if visibleEnd is |
| * false, it will return line end including the ellipsized characters and vice versa. |
| * |
| * @param lineIndex the line number |
| * @param visibleEnd if true, the returned line end will not count trailing whitespaces or |
| * linefeed characters. Otherwise, this function will return the logical line end. By default |
| * it's false. |
| * @return an exclusive end offset of the line. |
| */ |
| fun getLineEnd(lineIndex: Int, visibleEnd: Boolean = false): Int = |
| multiParagraph.getLineEnd(lineIndex, visibleEnd) |
| |
| /** |
| * Returns true if the given line is ellipsized, otherwise returns false. |
| * |
| * @param lineIndex a 0 based line index |
| * @return true if the given line is ellipsized, otherwise false |
| */ |
| fun isLineEllipsized(lineIndex: Int): Boolean = multiParagraph.isLineEllipsized(lineIndex) |
| |
| /** |
| * Returns the top y coordinate of the given line. |
| * |
| * @param lineIndex the line number |
| * @return the line top y coordinate |
| */ |
| fun getLineTop(lineIndex: Int): Float = multiParagraph.getLineTop(lineIndex) |
| |
| /** |
| * Returns the distance in pixels from the top of the text layout to the alphabetic baseline of |
| * the line at index [lineIndex]. |
| */ |
| fun getLineBaseline(lineIndex: Int): Float = multiParagraph.getLineBaseline(lineIndex) |
| |
| /** |
| * Returns the bottom y coordinate of the given line. |
| * |
| * @param lineIndex the line number |
| * @return the line bottom y coordinate |
| */ |
| fun getLineBottom(lineIndex: Int): Float = multiParagraph.getLineBottom(lineIndex) |
| |
| /** |
| * Returns the left x coordinate of the given line. |
| * |
| * @param lineIndex the line number |
| * @return the line left x coordinate |
| */ |
| fun getLineLeft(lineIndex: Int): Float = multiParagraph.getLineLeft(lineIndex) |
| |
| /** |
| * Returns the right x coordinate of the given line. |
| * |
| * @param lineIndex the line number |
| * @return the line right x coordinate |
| */ |
| fun getLineRight(lineIndex: Int): Float = multiParagraph.getLineRight(lineIndex) |
| |
| /** |
| * Returns the line number on which the specified text offset appears. |
| * |
| * If you ask for a position before 0, you get 0; if you ask for a position |
| * beyond the end of the text, you get the last line. |
| * |
| * @param offset a character offset |
| * @return the 0 origin line number. |
| */ |
| fun getLineForOffset(offset: Int): Int = multiParagraph.getLineForOffset(offset) |
| |
| /** |
| * Returns line number closest to the given graphical vertical position. |
| * |
| * If you ask for a vertical position before 0, you get 0; if you ask for a vertical position |
| * beyond the last line, you get the last line. |
| * |
| * @param vertical the vertical position |
| * @return the 0 origin line number. |
| */ |
| fun getLineForVerticalPosition(vertical: Float): Int = |
| multiParagraph.getLineForVerticalPosition(vertical) |
| |
| /** |
| * Get the horizontal position for the specified text [offset]. |
| * |
| * Returns the relative distance from the text starting offset. For example, if the paragraph |
| * direction is Left-to-Right, this function returns positive value as a distance from the |
| * left-most edge. If the paragraph direction is Right-to-Left, this function returns negative |
| * value as a distance from the right-most edge. |
| * |
| * [usePrimaryDirection] argument is taken into account only when the offset is in the BiDi |
| * directional transition point. [usePrimaryDirection] is true means use the primary |
| * direction run's coordinate, and use the secondary direction's run's coordinate if false. |
| * |
| * @param offset a character offset |
| * @param usePrimaryDirection true for using the primary run's coordinate if the given |
| * offset is in the BiDi directional transition point. |
| * @return the relative distance from the text starting edge. |
| * @see MultiParagraph.getHorizontalPosition |
| */ |
| fun getHorizontalPosition(offset: Int, usePrimaryDirection: Boolean): Float = |
| multiParagraph.getHorizontalPosition(offset, usePrimaryDirection) |
| |
| /** |
| * Get the text direction of the paragraph containing the given offset. |
| * |
| * @param offset a character offset |
| * @return the paragraph direction |
| */ |
| fun getParagraphDirection(offset: Int): ResolvedTextDirection = |
| multiParagraph.getParagraphDirection(offset) |
| |
| /** |
| * Get the text direction of the resolved BiDi run that the character at the given offset |
| * associated with. |
| * |
| * @param offset a character offset |
| * @return the direction of the BiDi run of the given character offset. |
| */ |
| fun getBidiRunDirection(offset: Int): ResolvedTextDirection = |
| multiParagraph.getBidiRunDirection(offset) |
| |
| /** |
| * Returns the character offset closest to the given graphical position. |
| * |
| * @param position a graphical position in this text layout |
| * @return a character offset that is closest to the given graphical position. |
| */ |
| fun getOffsetForPosition(position: Offset): Int = |
| multiParagraph.getOffsetForPosition(position) |
| |
| /** |
| * Returns the bounding box of the character for given character offset. |
| * |
| * @param offset a character offset |
| * @return a bounding box for the character in pixels. |
| */ |
| fun getBoundingBox(offset: Int): Rect = multiParagraph.getBoundingBox(offset) |
| |
| /** |
| * Returns the text range of the word at the given character offset. |
| * |
| * Characters not part of a word, such as spaces, symbols, and punctuation, have word breaks on |
| * both sides. In such cases, this method will return a text range that contains the given |
| * character offset. |
| * |
| * Word boundaries are defined more precisely in Unicode Standard Annex #29 |
| * <http://www.unicode.org/reports/tr29/#Word_Boundaries>. |
| */ |
| fun getWordBoundary(offset: Int): TextRange = multiParagraph.getWordBoundary(offset) |
| |
| /** |
| * Returns the rectangle of the cursor area |
| * |
| * @param offset An character offset of the cursor |
| * @return a rectangle of cursor region |
| */ |
| fun getCursorRect(offset: Int): Rect = multiParagraph.getCursorRect(offset) |
| |
| /** |
| * Returns path that enclose the given text range. |
| * |
| * @param start an inclusive start character offset |
| * @param end an exclusive end character offset |
| * @return a drawing path |
| */ |
| fun getPathForRange(start: Int, end: Int): Path = multiParagraph.getPathForRange(start, end) |
| |
| fun copy( |
| layoutInput: TextLayoutInput = this.layoutInput, |
| size: IntSize = this.size |
| ): TextLayoutResult { |
| return TextLayoutResult( |
| layoutInput = layoutInput, |
| multiParagraph = multiParagraph, |
| size = size |
| ) |
| } |
| |
| override fun equals(other: Any?): Boolean { |
| if (this === other) return true |
| if (other !is TextLayoutResult) return false |
| |
| if (layoutInput != other.layoutInput) return false |
| if (multiParagraph != other.multiParagraph) return false |
| if (size != other.size) return false |
| if (firstBaseline != other.firstBaseline) return false |
| if (lastBaseline != other.lastBaseline) return false |
| if (placeholderRects != other.placeholderRects) return false |
| |
| return true |
| } |
| |
| override fun hashCode(): Int { |
| var result = layoutInput.hashCode() |
| result = 31 * result + multiParagraph.hashCode() |
| result = 31 * result + size.hashCode() |
| result = 31 * result + firstBaseline.hashCode() |
| result = 31 * result + lastBaseline.hashCode() |
| result = 31 * result + placeholderRects.hashCode() |
| return result |
| } |
| |
| override fun toString(): String { |
| return "TextLayoutResult(" + |
| "layoutInput=$layoutInput, " + |
| "multiParagraph=$multiParagraph, " + |
| "size=$size, " + |
| "firstBaseline=$firstBaseline, " + |
| "lastBaseline=$lastBaseline, " + |
| "placeholderRects=$placeholderRects" + |
| ")" |
| } |
| } |