CardContainer.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.tv.material3

import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color

/**
 * [StandardCardContainer] is an opinionated TV Material Card layout with an image and text content
 * to show information about a subject.
 *
 * It provides a vertical layout with an image card slot at the top. And below that, there are
 * slots for the title, subtitle and description.
 *
 * ![Standard Card](https://developer.android.com/static/design/ui/tv/guides/components/images/cards/standard-card.webp)
 *
 * Checkout TV design guidelines to learn more about <a href="https://developer.android.com/design/ui/tv/guides/components/cards#standard-card" class="external" target="_blank">Material Standard Card</a>.
 *
 * @sample androidx.tv.samples.StandardCardContainerSample
 *
 * @param imageCard defines the [Composable] to be used for the image card.
 * @param title defines the [Composable] title placed below the image card in the CardContainer.
 * @param modifier the [Modifier] to be applied to this CardContainer.
 * @param subtitle defines the [Composable] supporting text placed below the title in CardContainer.
 * @param description defines the [Composable] description placed below the subtitle in CardContainer.
 * @param contentColor [CardContainerColors] defines the content color used in the CardContainer
 * for different interaction states. See [CardContainerDefaults.contentColor].
 * @param interactionSource a hoisted [MutableInteractionSource] for observing and emitting
 * [Interaction]s for this CardContainer.
 * This interaction source param would also be forwarded to be used with the `imageCard` composable.
 */
@Composable
fun StandardCardContainer(
    imageCard: @Composable (interactionSource: MutableInteractionSource) -> Unit,
    title: @Composable () -> Unit,
    modifier: Modifier = Modifier,
    subtitle: @Composable () -> Unit = {},
    description: @Composable () -> Unit = {},
    contentColor: CardContainerColors = CardContainerDefaults.contentColor(),
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
) {
    val focused by interactionSource.collectIsFocusedAsState()
    val pressed by interactionSource.collectIsPressedAsState()

    Column(
        modifier = modifier
    ) {
        Box(
            contentAlignment = CardDefaults.ContentImageAlignment,
        ) {
            imageCard(interactionSource)
        }
        Column(
            modifier = Modifier
                .align(Alignment.CenterHorizontally),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            CardContainerContent(
                title = title,
                subtitle = subtitle,
                description = description,
                contentColor = contentColor.color(
                    focused = focused,
                    pressed = pressed
                )
            )
        }
    }
}

/**
 * [WideCardContainer] is an opinionated TV Material Card layout with an image and text content
 * to show information about a subject.
 *
 * It provides a horizontal layout with an image card slot at the start, followed by the title,
 * subtitle and description at the end.
 *
 * ![Wide Card Container](https://developer.android.com/static/design/ui/tv/guides/components/images/cards/wide-card.webp)
 *
 * Checkout TV design guidelines to learn more about <a href="https://developer.android.com/design/ui/tv/guides/components/cards#wide-standard-card" class="external" target="_blank">Material Wide Standard Card</a>.
 *
 * @sample androidx.tv.samples.WideCardContainerSample
 *
 * @param imageCard defines the [Composable] to be used for the image card.
 * @param title defines the [Composable] title placed below the image card in the CardContainer.
 * @param modifier the [Modifier] to be applied to this CardContainer.
 * @param subtitle defines the [Composable] supporting text placed below the title in CardContainer.
 * @param description defines the [Composable] description placed below the subtitle in CardContainer.
 * @param contentColor [CardContainerColors] defines the content color used in the CardContainer
 * for different interaction states. See [CardContainerDefaults.contentColor].
 * @param interactionSource a hoisted [MutableInteractionSource] for observing and emitting
 * [Interaction]s for this CardContainer.
 * This interaction source param would also be forwarded to be used with the `imageCard` composable.
 */
@Composable
fun WideCardContainer(
    imageCard: @Composable (interactionSource: MutableInteractionSource) -> Unit,
    title: @Composable () -> Unit,
    modifier: Modifier = Modifier,
    subtitle: @Composable () -> Unit = {},
    description: @Composable () -> Unit = {},
    contentColor: CardContainerColors = CardContainerDefaults.contentColor(),
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
) {
    val focused by interactionSource.collectIsFocusedAsState()
    val pressed by interactionSource.collectIsPressedAsState()

    Row(
        modifier = modifier
    ) {
        Box(
            contentAlignment = CardDefaults.ContentImageAlignment
        ) {
            imageCard(interactionSource)
        }
        Column {
            CardContainerContent(
                title = title,
                subtitle = subtitle,
                description = description,
                contentColor = contentColor.color(
                    focused = focused,
                    pressed = pressed
                )
            )
        }
    }
}

@Composable
internal fun CardContainerContent(
    title: @Composable () -> Unit,
    subtitle: @Composable () -> Unit = {},
    description: @Composable () -> Unit = {},
    contentColor: Color
) {
    CompositionLocalProvider(LocalContentColor provides contentColor) {
        CardContent(title, subtitle, description)
    }
}

object CardContainerDefaults {
    /**
     * Creates [CardContainerColors] that represents the default content colors used in a
     * CardContainer.
     *
     * @param contentColor the default content color of this CardContainer.
     * @param focusedContentColor the content color of this CardContainer when focused.
     * @param pressedContentColor the content color of this CardContainer when pressed.
     */
    @ReadOnlyComposable
    @Composable
    fun contentColor(
        contentColor: Color = MaterialTheme.colorScheme.onSurface,
        focusedContentColor: Color = contentColor,
        pressedContentColor: Color = focusedContentColor
    ) = CardContainerColors(
        contentColor = contentColor,
        focusedContentColor = focusedContentColor,
        pressedContentColor = pressedContentColor
    )
}

/**
 * Represents the [Color] of content in a CardContainer for different interaction states.
 */
@Immutable
class CardContainerColors internal constructor(
    internal val contentColor: Color,
    internal val focusedContentColor: Color,
    internal val pressedContentColor: Color,
) {
    /**
     * Returns the content color [Color] for different interaction states.
     */
    internal fun color(
        focused: Boolean,
        pressed: Boolean
    ): Color {
        return when {
            focused -> focusedContentColor
            pressed -> pressedContentColor
            else -> contentColor
        }
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other == null || this::class != other::class) return false

        other as CardContainerColors

        if (contentColor != other.contentColor) return false
        if (focusedContentColor != other.focusedContentColor) return false
        if (pressedContentColor != other.pressedContentColor) return false

        return true
    }

    override fun hashCode(): Int {
        var result = contentColor.hashCode()
        result = 31 * result + focusedContentColor.hashCode()
        result = 31 * result + pressedContentColor.hashCode()
        return result
    }

    override fun toString(): String {
        return "CardContainerContentColor(" +
            "contentColor=$contentColor, " +
            "focusedContentColor=$focusedContentColor, " +
            "pressedContentColor=$pressedContentColor)"
    }
}