AndroidParagraphIntrinsics.android.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.platform

import android.graphics.Paint
import android.graphics.Typeface
import androidx.compose.runtime.State
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.EmojiSupportMatch
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.ParagraphIntrinsics
import androidx.compose.ui.text.Placeholder
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.android.InternalPlatformTextApi
import androidx.compose.ui.text.android.LayoutCompat
import androidx.compose.ui.text.android.LayoutIntrinsics
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontSynthesis
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.font.TypefaceResult
import androidx.compose.ui.text.intl.AndroidLocale
import androidx.compose.ui.text.intl.LocaleList
import androidx.compose.ui.text.platform.extensions.applySpanStyle
import androidx.compose.ui.text.platform.extensions.setTextMotion
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.unit.Density
import androidx.core.text.TextUtilsCompat
import androidx.core.view.ViewCompat
import java.util.Locale

@OptIn(InternalPlatformTextApi::class, ExperimentalTextApi::class)
internal class AndroidParagraphIntrinsics constructor(
    val text: String,
    val style: TextStyle,
    val spanStyles: List<AnnotatedString.Range<SpanStyle>>,
    val placeholders: List<AnnotatedString.Range<Placeholder>>,
    val fontFamilyResolver: FontFamily.Resolver,
    val density: Density
) : ParagraphIntrinsics {

    internal val textPaint = AndroidTextPaint(Paint.ANTI_ALIAS_FLAG, density.density)

    internal val charSequence: CharSequence

    internal val layoutIntrinsics: LayoutIntrinsics

    override val maxIntrinsicWidth: Float
        get() = layoutIntrinsics.maxIntrinsicWidth

    override val minIntrinsicWidth: Float
        get() = layoutIntrinsics.minIntrinsicWidth

    private var resolvedTypefaces: TypefaceDirtyTrackerLinkedList? = null

    /**
     * If emojiCompat is used in the making of this Paragraph
     *
     * This value will never change
     */
    private val emojiCompatProcessed: Boolean =
        if (!style.hasEmojiCompat) { false } else { EmojiCompatStatus.fontLoaded.value }

    override val hasStaleResolvedFonts: Boolean
        get() = (resolvedTypefaces?.isStaleResolvedFont ?: false) ||
            (!emojiCompatProcessed && style.hasEmojiCompat &&
                /* short-circuit this state read */ EmojiCompatStatus.fontLoaded.value)

    internal val textDirectionHeuristic = resolveTextDirectionHeuristics(
        style.textDirection,
        style.localeList
    )

    init {
        val resolveTypeface: (FontFamily?, FontWeight, FontStyle, FontSynthesis) -> Typeface =
            { fontFamily, fontWeight, fontStyle, fontSynthesis ->
                val result = fontFamilyResolver.resolve(
                    fontFamily,
                    fontWeight,
                    fontStyle,
                    fontSynthesis
                )
                if (result !is TypefaceResult.Immutable) {
                    val newHead = TypefaceDirtyTrackerLinkedList(result, resolvedTypefaces)
                    resolvedTypefaces = newHead
                    newHead.typeface
                } else {
                    result.value as Typeface
                }
            }

        textPaint.setTextMotion(style.textMotion)

        val notAppliedStyle = textPaint.applySpanStyle(
            style = style.toSpanStyle(),
            resolveTypeface = resolveTypeface,
            density = density,
            requiresLetterSpacing = spanStyles.isNotEmpty(),
        )

        val finalSpanStyles = if (notAppliedStyle != null) {
            // This is just a prepend operation, written in a lower alloc way
            // equivalent to: `AnnotatedString.Range(...) + spanStyles`
            List(spanStyles.size + 1) { position ->
                when (position) {
                    0 -> AnnotatedString.Range(
                        item = notAppliedStyle,
                        start = 0,
                        end = text.length
                    )

                    else -> spanStyles[position - 1]
                }
            }
        } else {
            spanStyles
        }
        charSequence = createCharSequence(
            text = text,
            contextFontSize = textPaint.textSize,
            contextTextStyle = style,
            spanStyles = finalSpanStyles,
            placeholders = placeholders,
            density = density,
            resolveTypeface = resolveTypeface,
            useEmojiCompat = emojiCompatProcessed
        )

        layoutIntrinsics = LayoutIntrinsics(charSequence, textPaint, textDirectionHeuristic)
    }
}

/**
 * For a given [TextDirection] return [TextLayout] constants for text direction
 * heuristics.
 */
@OptIn(InternalPlatformTextApi::class)
internal fun resolveTextDirectionHeuristics(
    textDirection: TextDirection? = null,
    localeList: LocaleList? = null
): Int {
    return when (textDirection ?: TextDirection.Content) {
        TextDirection.ContentOrLtr -> LayoutCompat.TEXT_DIRECTION_FIRST_STRONG_LTR
        TextDirection.ContentOrRtl -> LayoutCompat.TEXT_DIRECTION_FIRST_STRONG_RTL
        TextDirection.Ltr -> LayoutCompat.TEXT_DIRECTION_LTR
        TextDirection.Rtl -> LayoutCompat.TEXT_DIRECTION_RTL
        TextDirection.Content -> {
            val currentLocale = localeList?.let {
                (it[0].platformLocale as AndroidLocale).javaLocale
            } ?: Locale.getDefault()
            when (TextUtilsCompat.getLayoutDirectionFromLocale(currentLocale)) {
                ViewCompat.LAYOUT_DIRECTION_LTR -> LayoutCompat.TEXT_DIRECTION_FIRST_STRONG_LTR
                ViewCompat.LAYOUT_DIRECTION_RTL -> LayoutCompat.TEXT_DIRECTION_FIRST_STRONG_RTL
                else -> LayoutCompat.TEXT_DIRECTION_FIRST_STRONG_LTR
            }
        }
        else -> error("Invalid TextDirection.")
    }
}

@OptIn(InternalPlatformTextApi::class)
internal actual fun ActualParagraphIntrinsics(
    text: String,
    style: TextStyle,
    spanStyles: List<AnnotatedString.Range<SpanStyle>>,
    placeholders: List<AnnotatedString.Range<Placeholder>>,
    density: Density,
    fontFamilyResolver: FontFamily.Resolver
): ParagraphIntrinsics = AndroidParagraphIntrinsics(
    text = text,
    style = style,
    placeholders = placeholders,
    fontFamilyResolver = fontFamilyResolver,
    spanStyles = spanStyles,
    density = density
)

private class TypefaceDirtyTrackerLinkedList(
    private val resolveResult: State<Any>,
    private val next: TypefaceDirtyTrackerLinkedList? = null
) {
    val initial = resolveResult.value
    val typeface: Typeface
        get() = initial as Typeface

    val isStaleResolvedFont: Boolean
        get() = resolveResult.value !== initial || (next != null && next.isStaleResolvedFont)
}

private val TextStyle.hasEmojiCompat: Boolean
    get() = platformStyle?.paragraphStyle?.emojiSupportMatch != EmojiSupportMatch.None