Typography.kt

/*
 * Copyright 2023 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.wear.compose.material3

import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.text.PlatformTextStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.wear.compose.material3.tokens.TypographyKeyTokens
import androidx.wear.compose.material3.tokens.TypographyTokens

/**
 * Class holding typography definitions as defined by the Wear Material typography specification.
 *
 * The text styles in this typography are scaled according to the user's preferred font size in
 * the system settings. Larger font sizes can be fixed if necessary in order to avoid pressure on
 * screen space, because they are already sufficiently accessible.
 * Here is an example of fixing the font size for DisplayLarge:
 * @sample androidx.wear.compose.material3.samples.FixedFontSize
 *
 * TODO(b/273526150) Review documentation for typography, add examples for each size.
 *
 * @property displayLarge DisplayLarge is the largest headline. Displays are the largest text
 * on the screen, reserved for short, important text or numerals.
 *
 * @property displayMedium DisplayMedium is the second largest headline. Displays are the
 * largest text on the screen, reserved for short, important text or numerals.
 *
 * @property displaySmall DisplaySmall is the smallest headline. Displays are the largest
 * text on the screen, reserved for short, important text or numerals.
 *
 * @property titleLarge TitleLarge is the largest title. Titles are smaller than Displays. They are
 * typically reserved for medium-emphasis text that is shorter in length.
 *
 * @property titleMedium TitleMedium is the medium title. Titles are smaller than Displays. They are
 * typically reserved for medium-emphasis text that is shorter in length.
 *
 * @property titleSmall TitleSmall is the smallest title. Titles are smaller than Displays. They are
 * typically reserved for medium-emphasis text that is shorter in length.
 *
 * @property labelLarge LabelLarge is the largest label. They are used for displaying prominent
 * texts like label on title buttons.
 *
 * @property labelMedium LabelMedium is the medium label. They are used for displaying texts like
 * primary label on buttons.
 *
 * @property labelSmall LabelSmall is the small label. They are used for displaying texts like
 * secondary label on buttons, labels on compact buttons.
 *
 * @property bodyLarge BodyLarge is the largest body. Body texts are typically used for long-form
 * writing as it works well for small text sizes. For longer sections of text, a serif or
 * sans serif typeface is recommended.
 *
 * @property bodyMedium BodyMedium is second largest body. Body texts are typically used for
 * long-form writing as it works well for small text sizes. For longer sections of text, a serif
 * or sans serif typeface is recommended.
 *
 * @property bodySmall BodySmall is third largest body. Body texts are typically used for long-form
 * writing as it works well for small text sizes. For longer sections of text, a serif or sans serif
 * typeface is recommended.
 *
 * @property bodyExtraSmall BodyExtraSmall is the smallest body. Body texts are typically used for
 * long-form writing as it works well for small text sizes. For longer sections of text, a serif
 * or sans serif typeface is recommended.
 */
@Immutable
public class Typography internal constructor(
    public val displayLarge: TextStyle,
    public val displayMedium: TextStyle,
    public val displaySmall: TextStyle,
    public val titleLarge: TextStyle,
    public val titleMedium: TextStyle,
    public val titleSmall: TextStyle,
    public val labelLarge: TextStyle,
    public val labelMedium: TextStyle,
    public val labelSmall: TextStyle,
    public val bodyLarge: TextStyle,
    public val bodyMedium: TextStyle,
    public val bodySmall: TextStyle,
    public val bodyExtraSmall: TextStyle
) {
    public constructor (
        defaultFontFamily: FontFamily = FontFamily.Default,
        displayLarge: TextStyle = TypographyTokens.DisplayLarge,
        displayMedium: TextStyle = TypographyTokens.DisplayMedium,
        displaySmall: TextStyle = TypographyTokens.DisplaySmall,
        titleLarge: TextStyle = TypographyTokens.TitleLarge,
        titleMedium: TextStyle = TypographyTokens.TitleMedium,
        titleSmall: TextStyle = TypographyTokens.TitleSmall,
        labelLarge: TextStyle = TypographyTokens.LabelLarge,
        labelMedium: TextStyle = TypographyTokens.LabelMedium,
        labelSmall: TextStyle = TypographyTokens.LabelSmall,
        bodyLarge: TextStyle = TypographyTokens.BodyLarge,
        bodyMedium: TextStyle = TypographyTokens.BodyMedium,
        bodySmall: TextStyle = TypographyTokens.BodySmall,
        bodyExtraSmall: TextStyle = TypographyTokens.BodyExtraSmall
    ) : this(
        displayLarge = displayLarge.withDefaultFontFamily(defaultFontFamily),
        displayMedium = displayMedium.withDefaultFontFamily(defaultFontFamily),
        displaySmall = displaySmall.withDefaultFontFamily(defaultFontFamily),
        titleLarge = titleLarge.withDefaultFontFamily(defaultFontFamily),
        titleMedium = titleMedium.withDefaultFontFamily(defaultFontFamily),
        titleSmall = titleSmall.withDefaultFontFamily(defaultFontFamily),
        labelLarge = labelLarge.withDefaultFontFamily(defaultFontFamily),
        labelMedium = labelMedium.withDefaultFontFamily(defaultFontFamily),
        labelSmall = labelSmall.withDefaultFontFamily(defaultFontFamily),
        bodyLarge = bodyLarge.withDefaultFontFamily(defaultFontFamily),
        bodyMedium = bodyMedium.withDefaultFontFamily(defaultFontFamily),
        bodySmall = bodySmall.withDefaultFontFamily(defaultFontFamily),
        bodyExtraSmall = bodyExtraSmall.withDefaultFontFamily(defaultFontFamily)
    )

    /**
     * Returns a copy of this Typography, optionally overriding some of the values.
     */
    public fun copy(
        displayLarge: TextStyle = this.displayLarge,
        displayMedium: TextStyle = this.displayMedium,
        displaySmall: TextStyle = this.displaySmall,
        titleLarge: TextStyle = this.titleLarge,
        titleMedium: TextStyle = this.titleMedium,
        titleSmall: TextStyle = this.titleSmall,
        labelLarge: TextStyle = this.labelLarge,
        labelMedium: TextStyle = this.labelMedium,
        labelSmall: TextStyle = this.labelSmall,
        bodyLarge: TextStyle = this.bodyLarge,
        bodyMedium: TextStyle = this.bodyMedium,
        bodySmall: TextStyle = this.bodySmall,
        bodyExtraSmall: TextStyle = this.bodyExtraSmall
    ): Typography = Typography(
        displayLarge,
        displayMedium,
        displaySmall,
        titleLarge,
        titleMedium,
        titleSmall,
        labelLarge,
        labelMedium,
        labelSmall,
        bodyLarge,
        bodyMedium,
        bodySmall,
        bodyExtraSmall
    )

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is Typography) return false

        if (displayLarge != other.displayLarge) return false
        if (displayMedium != other.displayMedium) return false
        if (displaySmall != other.displaySmall) return false
        if (titleLarge != other.titleLarge) return false
        if (titleMedium != other.titleMedium) return false
        if (titleSmall != other.titleSmall) return false
        if (labelLarge != other.labelLarge) return false
        if (labelMedium != other.labelMedium) return false
        if (labelSmall != other.labelSmall) return false
        if (bodyLarge != other.bodyLarge) return false
        if (bodyMedium != other.bodyMedium) return false
        if (bodySmall != other.bodySmall) return false
        if (bodyExtraSmall != other.bodyExtraSmall) return false

        return true
    }

    override fun hashCode(): Int {
        var result = displayLarge.hashCode()
        result = 31 * result + displayMedium.hashCode()
        result = 31 * result + displaySmall.hashCode()
        result = 31 * result + titleLarge.hashCode()
        result = 31 * result + titleMedium.hashCode()
        result = 31 * result + titleSmall.hashCode()
        result = 31 * result + labelLarge.hashCode()
        result = 31 * result + labelMedium.hashCode()
        result = 31 * result + labelSmall.hashCode()
        result = 31 * result + bodyLarge.hashCode()
        result = 31 * result + bodyMedium.hashCode()
        result = 31 * result + bodySmall.hashCode()
        result = 31 * result + bodyExtraSmall.hashCode()
        return result
    }

    override fun toString(): String {
        return "Typography(displayLarge=$displayLarge, displayMedium=$displayMedium, " +
            "displaySmall=$displaySmall, titleLarge=$titleLarge, titleMedium=$titleMedium, " +
            "titleSmall=$titleSmall, labelLarge=$labelLarge, labelMedium=$labelMedium, " +
            "labelSmall=$labelSmall, bodyLarge=$bodyLarge, bodyMedium=$bodyMedium, " +
            "bodySmall=$bodySmall, bodyExtraSmall=$bodyExtraSmall)"
    }
}

/**
 * @return [this] if there is a [FontFamily] defined, otherwise copies [this] with [default] as
 * the [FontFamily].
 */
private fun TextStyle.withDefaultFontFamily(default: FontFamily): TextStyle {
    return if (fontFamily != null) this else copy(fontFamily = default)
}

private const val DefaultIncludeFontPadding = false

/**
 * Returns theme default [TextStyle] with default [PlatformTextStyle].
 */
internal val DefaultTextStyle = TextStyle.Default.copy(
    platformStyle = PlatformTextStyle(
        includeFontPadding = DefaultIncludeFontPadding
    )
)

/**
 * Helper function for typography tokens.
 */
internal fun Typography.fromToken(value: TypographyKeyTokens): TextStyle {
    return when (value) {
        TypographyKeyTokens.DisplayLarge -> displayLarge
        TypographyKeyTokens.DisplayMedium -> displayMedium
        TypographyKeyTokens.DisplaySmall -> displaySmall
        TypographyKeyTokens.TitleLarge -> titleLarge
        TypographyKeyTokens.TitleMedium -> titleMedium
        TypographyKeyTokens.TitleSmall -> titleSmall
        TypographyKeyTokens.LabelLarge -> labelLarge
        TypographyKeyTokens.LabelMedium -> labelMedium
        TypographyKeyTokens.LabelSmall -> labelSmall
        TypographyKeyTokens.BodyLarge -> bodyLarge
        TypographyKeyTokens.BodyMedium -> bodyMedium
        TypographyKeyTokens.BodySmall -> bodySmall
        TypographyKeyTokens.BodyExtraSmall -> bodyExtraSmall
    }
}

/**
 * Helper function to convert [TypographyKeyTokens] to [TextStyle].
 */
@Composable
@ReadOnlyComposable
internal fun TypographyKeyTokens.toTextStyle(): TextStyle {
    return MaterialTheme.typography.fromToken(this)
}

/**
 * This Ambient holds on to the current definition of typography for this application as described
 * by the Wear Material spec. You can read the values in it when creating custom components that
 * want to use Wear Material types, as well as override the values when you want to re-style a part
 * of your hierarchy. Material components related to text such as Button will use this Ambient
 * to set values with which to style children text components.
 *
 * To access values within this ambient, use [MaterialTheme.typography].
 */
internal val LocalTypography = staticCompositionLocalOf { Typography() }