All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.androidx.compose.ui.text.TextLayoutResult.kt Maven / Gradle / Ivy

/*
 * 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.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>,

    /**
     * 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>,
        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>,
        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> = 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()
        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 = 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
     * 
     * ┌────┐
     * │abcd│
     * │efg │
     * └────┘
     * 
* * @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 *
     * ┌────┐
     * │abcd│
     * │efg │
     * └────┘
     * 
* * 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 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 * . */ 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" + ")" } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy