TextDrawStyle.kt

/*
 * Copyright 2022 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.style

import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ShaderBrush
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.isSpecified
import androidx.compose.ui.graphics.lerp as lerpColor
import androidx.compose.ui.text.lerpDiscrete

/**
 * An internal interface to represent possible ways to draw Text e.g. color, brush. This interface
 * aims to unify unspecified versions of complementary drawing styles. There are some guarantees
 * as following;
 *
 * - If [color] is not [Color.Unspecified], brush is null.
 * - If [brush] is not null, color is [Color.Unspecified].
 * - Both [color] can be [Color.Unspecified] and [brush] null, indicating that nothing is specified.
 * - [SolidColor] brushes are stored as regular [Color].
 */
internal interface TextDrawStyle {
    val color: Color

    val brush: Brush?

    fun merge(other: TextDrawStyle): TextDrawStyle {
        // This control prevents Color or Unspecified TextDrawStyle to override an existing Brush.
        // It is a temporary measure to prevent Material Text composables to remove given Brush
        // from a TextStyle.
        // TODO(b/230787077): Just return other.takeOrElse { this } when Brush is stable.
        return when {
            other.brush != null -> other
            brush != null -> this
            else -> other.takeOrElse { this }
        }
    }

    fun takeOrElse(other: () -> TextDrawStyle): TextDrawStyle {
        return if (this != Unspecified) this else other()
    }

    object Unspecified : TextDrawStyle {
        override val color: Color
            get() = Color.Unspecified

        override val brush: Brush?
            get() = null
    }

    companion object {
        fun from(color: Color): TextDrawStyle {
            return if (color.isSpecified) ColorStyle(color) else Unspecified
        }

        fun from(brush: Brush?): TextDrawStyle {
            return when (brush) {
                null -> Unspecified
                is SolidColor -> from(brush.value)
                is ShaderBrush -> BrushStyle(brush)
            }
        }
    }
}

private data class ColorStyle(private val value: Color) : TextDrawStyle {
    init {
        require(value.isSpecified) {
            "ColorStyle value must be specified, use TextDrawStyle.Unspecified instead."
        }
    }

    override val color: Color
        get() = value

    override val brush: Brush?
        get() = null
}

private data class BrushStyle(private val value: ShaderBrush) : TextDrawStyle {
    override val color: Color
        get() = Color.Unspecified

    override val brush: Brush
        get() = value
}

/**
 * If both TextDrawStyles do not represent a Brush, lerp the color values. Otherwise, lerp
 * start to end discretely.
 */
internal fun lerp(start: TextDrawStyle, stop: TextDrawStyle, fraction: Float): TextDrawStyle {
    return if ((start !is BrushStyle && stop !is BrushStyle)) {
        TextDrawStyle.from(lerpColor(start.color, stop.color, fraction))
    } else {
        lerpDiscrete(start, stop, fraction)
    }
}