NavigationDrawerItem.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.animation.AnimatedVisibility
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.layout
import androidx.compose.ui.unit.Dp

/**
 * TV Material Design navigation drawer item.
 *
 * A [NavigationDrawerItem] represents a destination within drawers, either [NavigationDrawer] or
 * [ModalNavigationDrawer]
 *
 * @sample androidx.tv.samples.SampleNavigationDrawer
 * @sample androidx.tv.samples.SampleModalNavigationDrawerWithSolidScrim
 * @sample androidx.tv.samples.SampleModalNavigationDrawerWithGradientScrim
 *
 * @param selected defines whether this composable is selected or not
 * @param onClick called when this composable is clicked
 * @param leadingContent the leading content of the list item
 * @param modifier to be applied to the list item
 * @param enabled controls the enabled state of this composable. When `false`, this component will
 * not respond to user input, and it will appear visually disabled and disabled to accessibility
 * services
 * @param onLongClick called when this composable is long clicked (long-pressed)
 * @param supportingContent the content displayed below the headline content
 * @param trailingContent the trailing meta badge or icon
 * @param tonalElevation the tonal elevation of this composable
 * @param shape defines the shape of Composable's container in different interaction states
 * @param colors defines the background and content colors used in the composable
 * for different interaction states
 * @param scale defines the size of the composable relative to its original size in
 * different interaction states
 * @param border defines a border around the composable in different interaction states
 * @param glow defines a shadow to be shown behind the composable for different interaction states
 * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
 * emitting [Interaction]s for this composable. You can use this to change the composable's
 * appearance or preview the composable in different states. Note that if `null` is provided,
 * interactions will still happen internally.
 * @param content main content of this composable
 */
@Composable
fun NavigationDrawerScope.NavigationDrawerItem(
    selected: Boolean,
    onClick: () -> Unit,
    leadingContent: @Composable () -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    onLongClick: (() -> Unit)? = null,
    supportingContent: (@Composable () -> Unit)? = null,
    trailingContent: (@Composable () -> Unit)? = null,
    tonalElevation: Dp = NavigationDrawerItemDefaults.NavigationDrawerItemElevation,
    shape: NavigationDrawerItemShape = NavigationDrawerItemDefaults.shape(),
    colors: NavigationDrawerItemColors = NavigationDrawerItemDefaults.colors(),
    scale: NavigationDrawerItemScale = NavigationDrawerItemScale.None,
    border: NavigationDrawerItemBorder = NavigationDrawerItemDefaults.border(),
    glow: NavigationDrawerItemGlow = NavigationDrawerItemDefaults.glow(),
    interactionSource: MutableInteractionSource? = null,
    content: @Composable () -> Unit,
) {
    val animatedWidth by animateDpAsState(
        targetValue = if (hasFocus) {
            NavigationDrawerItemDefaults.ExpandedDrawerItemWidth
        } else {
            NavigationDrawerItemDefaults.CollapsedDrawerItemWidth
        },
        label = "NavigationDrawerItem width open/closed state of the drawer item"
    )
    val navDrawerItemHeight = if (supportingContent == null) {
        NavigationDrawerItemDefaults.ContainerHeightOneLine
    } else {
        NavigationDrawerItemDefaults.ContainerHeightTwoLine
    }
    ListItem(
        selected = selected,
        onClick = onClick,
        headlineContent = {
            AnimatedVisibility(
                visible = hasFocus,
                enter = NavigationDrawerItemDefaults.ContentAnimationEnter,
                exit = NavigationDrawerItemDefaults.ContentAnimationExit,
            ) {
                content()
            }
        },
        leadingContent = {
            Box(Modifier.size(NavigationDrawerItemDefaults.IconSize)) {
                leadingContent()
            }
        },
        trailingContent = trailingContent?.let {
            {
                AnimatedVisibility(
                    visible = hasFocus,
                    enter = NavigationDrawerItemDefaults.ContentAnimationEnter,
                    exit = NavigationDrawerItemDefaults.ContentAnimationExit,
                ) {
                    it()
                }
            }
        },
        supportingContent = supportingContent?.let {
            {
                AnimatedVisibility(
                    visible = hasFocus,
                    enter = NavigationDrawerItemDefaults.ContentAnimationEnter,
                    exit = NavigationDrawerItemDefaults.ContentAnimationExit,
                ) {
                    it()
                }
            }
        },
        modifier = modifier
            .layout { measurable, constraints ->
                val width = animatedWidth.roundToPx()
                val height = navDrawerItemHeight.roundToPx()
                val placeable = measurable.measure(
                    constraints.copy(
                        minWidth = width,
                        maxWidth = width,
                        minHeight = height,
                        maxHeight = height,
                    )
                )
                layout(placeable.width, placeable.height) {
                    placeable.place(0, 0)
                }
            },
        enabled = enabled,
        onLongClick = onLongClick,
        tonalElevation = tonalElevation,
        shape = shape.toToggleableListItemShape(),
        colors = colors.toToggleableListItemColors(hasFocus),
        scale = scale.toToggleableListItemScale(),
        border = border.toToggleableListItemBorder(),
        glow = glow.toToggleableListItemGlow(),
        interactionSource = interactionSource,
    )
}

@Composable
private fun NavigationDrawerItemShape.toToggleableListItemShape() =
    ListItemDefaults.shape(
        shape = shape,
        focusedShape = focusedShape,
        pressedShape = pressedShape,
        selectedShape = selectedShape,
        disabledShape = disabledShape,
        focusedSelectedShape = focusedSelectedShape,
        focusedDisabledShape = focusedDisabledShape,
        pressedSelectedShape = pressedSelectedShape,
    )

@Composable
private fun NavigationDrawerItemColors.toToggleableListItemColors(
    doesNavigationDrawerHaveFocus: Boolean
) =
    ListItemDefaults.colors(
        containerColor = containerColor,
        contentColor = if (doesNavigationDrawerHaveFocus) contentColor else inactiveContentColor,
        focusedContainerColor = focusedContainerColor,
        focusedContentColor = focusedContentColor,
        pressedContainerColor = pressedContainerColor,
        pressedContentColor = pressedContentColor,
        selectedContainerColor = selectedContainerColor,
        selectedContentColor = selectedContentColor,
        disabledContainerColor = disabledContainerColor,
        disabledContentColor =
        if (doesNavigationDrawerHaveFocus) disabledContentColor else disabledInactiveContentColor,
        focusedSelectedContainerColor = focusedSelectedContainerColor,
        focusedSelectedContentColor = focusedSelectedContentColor,
        pressedSelectedContainerColor = pressedSelectedContainerColor,
        pressedSelectedContentColor = pressedSelectedContentColor,
    )

@Composable
private fun NavigationDrawerItemScale.toToggleableListItemScale() =
    ListItemDefaults.scale(
        scale = scale,
        focusedScale = focusedScale,
        pressedScale = pressedScale,
        selectedScale = selectedScale,
        disabledScale = disabledScale,
        focusedSelectedScale = focusedSelectedScale,
        focusedDisabledScale = focusedDisabledScale,
        pressedSelectedScale = pressedSelectedScale,
    )

@Composable
private fun NavigationDrawerItemBorder.toToggleableListItemBorder() =
    ListItemDefaults.border(
        border = border,
        focusedBorder = focusedBorder,
        pressedBorder = pressedBorder,
        selectedBorder = selectedBorder,
        disabledBorder = disabledBorder,
        focusedSelectedBorder = focusedSelectedBorder,
        focusedDisabledBorder = focusedDisabledBorder,
        pressedSelectedBorder = pressedSelectedBorder,
    )

@Composable
private fun NavigationDrawerItemGlow.toToggleableListItemGlow() =
    ListItemDefaults.glow(
        glow = glow,
        focusedGlow = focusedGlow,
        pressedGlow = pressedGlow,
        selectedGlow = selectedGlow,
        focusedSelectedGlow = focusedSelectedGlow,
        pressedSelectedGlow = pressedSelectedGlow,
    )