blob: f21c2442b2183b1cea57843f9d61d6f7dadb7d0d [file] [log] [blame]
/*
* 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.
*/
@file:JvmName("TextDrawStyleKt")
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
import androidx.compose.ui.util.lerp
/**
* 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 TextForegroundStyle {
val color: Color
val brush: Brush?
val alpha: Float
fun merge(other: TextForegroundStyle): TextForegroundStyle {
// This control prevents Color or Unspecified TextForegroundStyle 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 is BrushStyle && this is BrushStyle ->
BrushStyle(other.value, other.alpha.takeOrElse { this.alpha })
other is BrushStyle && this !is BrushStyle -> other
other !is BrushStyle && this is BrushStyle -> this
else -> other.takeOrElse { this }
}
}
fun takeOrElse(other: () -> TextForegroundStyle): TextForegroundStyle {
return if (this != Unspecified) this else other()
}
object Unspecified : TextForegroundStyle {
override val color: Color
get() = Color.Unspecified
override val brush: Brush?
get() = null
override val alpha: Float
get() = Float.NaN
}
companion object {
fun from(color: Color): TextForegroundStyle {
return if (color.isSpecified) ColorStyle(color) else Unspecified
}
fun from(brush: Brush?, alpha: Float): TextForegroundStyle {
return when (brush) {
null -> Unspecified
is SolidColor -> from(brush.value.modulate(alpha))
is ShaderBrush -> BrushStyle(brush, alpha)
}
}
}
}
private data class ColorStyle(
val value: Color
) : TextForegroundStyle {
init {
require(value.isSpecified) {
"ColorStyle value must be specified, use TextForegroundStyle.Unspecified instead."
}
}
override val color: Color
get() = value
override val brush: Brush?
get() = null
override val alpha: Float
get() = color.alpha
}
private data class BrushStyle(
val value: ShaderBrush,
override val alpha: Float
) : TextForegroundStyle {
override val color: Color
get() = Color.Unspecified
override val brush: Brush
get() = value
}
/**
* If both TextForegroundStyles do not represent a Brush, lerp the color values. Otherwise, lerp
* start to end discretely.
*/
internal fun lerp(
start: TextForegroundStyle,
stop: TextForegroundStyle,
fraction: Float
): TextForegroundStyle {
return if ((start !is BrushStyle && stop !is BrushStyle)) {
TextForegroundStyle.from(lerpColor(start.color, stop.color, fraction))
} else if (start is BrushStyle && stop is BrushStyle) {
TextForegroundStyle.from(
lerpDiscrete(start.brush, stop.brush, fraction),
lerp(start.alpha, stop.alpha, fraction)
)
} else {
lerpDiscrete(start, stop, fraction)
}
}
internal fun Color.modulate(alpha: Float): Color = when {
alpha.isNaN() || alpha >= 1f -> this
else -> this.copy(alpha = this.alpha * alpha)
}
private fun Float.takeOrElse(block: () -> Float): Float {
return if (this.isNaN()) block() else this
}