MultiParagraphIntrinsics.kt

/*
 * 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.text.font.Font
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.unit.Density

/**
 * Calculates and provides the intrinsic width and height of text that contains [ParagraphStyle].
 *
 * @param annotatedString the text to be laid out
 * @param style the [TextStyle] to be applied to the whole text
 * @param placeholders a list of [Placeholder]s that specify ranges of text which will be
 * skipped during layout and replaced with [Placeholder]. It's required that the range of each
 * [Placeholder] doesn't cross paragraph boundary, otherwise [IllegalArgumentException] is thrown.
 * @param density density of the device
 * @param resourceLoader [Font.ResourceLoader] to be used to load the font given in [SpanStyle]s

 * @see MultiParagraph
 * @see Placeholder
 *
 * @throws IllegalArgumentException if [ParagraphStyle.textDirection] is not set, or any
 * of the [placeholders] crosses paragraph boundary.
 */
class MultiParagraphIntrinsics(
    val annotatedString: AnnotatedString,
    style: TextStyle,
    val placeholders: List<AnnotatedString.Range<Placeholder>>,
    density: Density,
    resourceLoader: Font.ResourceLoader
) : ParagraphIntrinsics {

    override val minIntrinsicWidth: Float by lazy {
        infoList.maxByOrNull {
            it.intrinsics.minIntrinsicWidth
        }?.intrinsics?.minIntrinsicWidth ?: 0f
    }

    override val maxIntrinsicWidth: Float by lazy {
        infoList.maxByOrNull {
            it.intrinsics.maxIntrinsicWidth
        }?.intrinsics?.maxIntrinsicWidth ?: 0f
    }

    /**
     * [ParagraphIntrinsics] for each paragraph included in the [buildAnnotatedString]. For empty string
     * there will be a single empty paragraph intrinsics info.
     */
    internal val infoList: List<ParagraphIntrinsicInfo>

    init {
        val paragraphStyle = style.toParagraphStyle()
        infoList = annotatedString
            .mapEachParagraphStyle(paragraphStyle) { annotatedString, paragraphStyleItem ->
                val currentParagraphStyle = resolveTextDirection(
                    paragraphStyleItem.item,
                    paragraphStyle
                )

                ParagraphIntrinsicInfo(
                    intrinsics = ParagraphIntrinsics(
                        text = annotatedString.text,
                        style = style.merge(currentParagraphStyle),
                        spanStyles = annotatedString.spanStyles,
                        placeholders = placeholders.getLocalPlaceholders(
                            paragraphStyleItem.start,
                            paragraphStyleItem.end
                        ),
                        density = density,
                        resourceLoader = resourceLoader
                    ),
                    startIndex = paragraphStyleItem.start,
                    endIndex = paragraphStyleItem.end
                )
            }
    }

    /**
     * if the [style] does `not` have [TextDirection] set, it will return a new
     * [ParagraphStyle] where [TextDirection] is set using the [defaultStyle]. Otherwise
     * returns the same [style] object.
     *
     * @param style ParagraphStyle to be checked for [TextDirection]
     * @param defaultStyle [ParagraphStyle] passed to [MultiParagraphIntrinsics] as the main style
     */
    private fun resolveTextDirection(
        style: ParagraphStyle,
        defaultStyle: ParagraphStyle
    ): ParagraphStyle {
        return style.textDirection?.let { style } ?: style.copy(
            textDirection = defaultStyle.textDirection
        )
    }
}

private fun List<AnnotatedString.Range<Placeholder>>.getLocalPlaceholders(start: Int, end: Int) =
    filter { intersect(start, end, it.start, it.end) }.map {
        require(start <= it.start && it.end <= end) {
            "placeholder can not overlap with paragraph."
        }
        AnnotatedString.Range(it.item, it.start - start, it.end - start)
    }

internal data class ParagraphIntrinsicInfo(
    val intrinsics: ParagraphIntrinsics,
    val startIndex: Int,
    val endIndex: Int
)