package eu.kanade.presentation.components import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.VectorConverter import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.interaction.FocusInteraction import androidx.compose.foundation.interaction.HoverInteraction import androidx.compose.foundation.interaction.Interaction import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.Button import androidx.compose.material3.ColorScheme import androidx.compose.material3.ElevatedButton import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Immutable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import eu.kanade.presentation.util.animateElevation import androidx.compose.material3.ButtonDefaults as M3ButtonDefaults /** * TextButton with additional onLongClick functionality. * * @see androidx.compose.material3.TextButton */ @Composable fun TextButton( onClick: () -> Unit, modifier: Modifier = Modifier, onLongClick: (() -> Unit)? = null, enabled: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, elevation: ButtonElevation? = null, shape: Shape = M3ButtonDefaults.textShape, border: BorderStroke? = null, colors: ButtonColors = ButtonColors( containerColor = Color.Transparent, contentColor = MaterialTheme.colorScheme.primary, disabledContainerColor = Color.Transparent, disabledContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f), ), contentPadding: PaddingValues = M3ButtonDefaults.TextButtonContentPadding, content: @Composable RowScope.() -> Unit, ) = Button( onClick = onClick, modifier = modifier, onLongClick = onLongClick, enabled = enabled, interactionSource = interactionSource, elevation = elevation, shape = shape, border = border, colors = colors, contentPadding = contentPadding, content = content, ) /** * Button with additional onLongClick functionality. * * @see androidx.compose.material3.TextButton */ @Composable fun Button( onClick: () -> Unit, modifier: Modifier = Modifier, onLongClick: (() -> Unit)? = null, enabled: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, elevation: ButtonElevation? = ButtonDefaults.buttonElevation(), shape: Shape = M3ButtonDefaults.textShape, border: BorderStroke? = null, colors: ButtonColors = ButtonDefaults.buttonColors(), contentPadding: PaddingValues = M3ButtonDefaults.ContentPadding, content: @Composable RowScope.() -> Unit, ) { val containerColor = colors.containerColor(enabled).value val contentColor = colors.contentColor(enabled).value val shadowElevation = elevation?.shadowElevation(enabled, interactionSource)?.value ?: 0.dp val tonalElevation = elevation?.tonalElevation(enabled, interactionSource)?.value ?: 0.dp Surface( onClick = onClick, modifier = modifier, onLongClick = onLongClick, shape = shape, color = containerColor, contentColor = contentColor, tonalElevation = tonalElevation, shadowElevation = shadowElevation, border = border, interactionSource = interactionSource, enabled = enabled, ) { CompositionLocalProvider(LocalContentColor provides contentColor) { ProvideTextStyle(value = MaterialTheme.typography.labelLarge) { Row( Modifier.defaultMinSize( minWidth = M3ButtonDefaults.MinWidth, minHeight = M3ButtonDefaults.MinHeight, ) .padding(contentPadding), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, content = content, ) } } } } object ButtonDefaults { /** * Creates a [ButtonColors] that represents the default container and content colors used in a * [Button]. * * @param containerColor the container color of this [Button] when enabled. * @param contentColor the content color of this [Button] when enabled. * @param disabledContainerColor the container color of this [Button] when not enabled. * @param disabledContentColor the content color of this [Button] when not enabled. */ @Composable fun buttonColors( containerColor: Color = MaterialTheme.colorScheme.primary, contentColor: Color = MaterialTheme.colorScheme.onPrimary, disabledContainerColor: Color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f), disabledContentColor: Color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f), ): ButtonColors = ButtonColors( containerColor = containerColor, contentColor = contentColor, disabledContainerColor = disabledContainerColor, disabledContentColor = disabledContentColor, ) /** * Creates a [ButtonElevation] that will animate between the provided values according to the * Material specification for a [Button]. * * @param defaultElevation the elevation used when the [Button] is enabled, and has no other * [Interaction]s. * @param pressedElevation the elevation used when this [Button] is enabled and pressed. * @param focusedElevation the elevation used when the [Button] is enabled and focused. * @param hoveredElevation the elevation used when the [Button] is enabled and hovered. * @param disabledElevation the elevation used when the [Button] is not enabled. */ @Composable fun buttonElevation( defaultElevation: Dp = 0.dp, pressedElevation: Dp = 0.dp, focusedElevation: Dp = 0.dp, hoveredElevation: Dp = 1.dp, disabledElevation: Dp = 0.dp, ): ButtonElevation = ButtonElevation( defaultElevation = defaultElevation, pressedElevation = pressedElevation, focusedElevation = focusedElevation, hoveredElevation = hoveredElevation, disabledElevation = disabledElevation, ) } /** * Represents the elevation for a button in different states. * * - See [M3ButtonDefaults.buttonElevation] for the default elevation used in a [Button]. * - See [M3ButtonDefaults.elevatedButtonElevation] for the default elevation used in a * [ElevatedButton]. */ @Stable class ButtonElevation internal constructor( private val defaultElevation: Dp, private val pressedElevation: Dp, private val focusedElevation: Dp, private val hoveredElevation: Dp, private val disabledElevation: Dp, ) { /** * Represents the tonal elevation used in a button, depending on its [enabled] state and * [interactionSource]. This should typically be the same value as the [shadowElevation]. * * Tonal elevation is used to apply a color shift to the surface to give the it higher emphasis. * When surface's color is [ColorScheme.surface], a higher elevation will result in a darker * color in light theme and lighter color in dark theme. * * See [shadowElevation] which controls the elevation of the shadow drawn around the button. * * @param enabled whether the button is enabled * @param interactionSource the [InteractionSource] for this button */ @Composable internal fun tonalElevation(enabled: Boolean, interactionSource: InteractionSource): State { return animateElevation(enabled = enabled, interactionSource = interactionSource) } /** * Represents the shadow elevation used in a button, depending on its [enabled] state and * [interactionSource]. This should typically be the same value as the [tonalElevation]. * * Shadow elevation is used to apply a shadow around the button to give it higher emphasis. * * See [tonalElevation] which controls the elevation with a color shift to the surface. * * @param enabled whether the button is enabled * @param interactionSource the [InteractionSource] for this button */ @Composable internal fun shadowElevation( enabled: Boolean, interactionSource: InteractionSource, ): State { return animateElevation(enabled = enabled, interactionSource = interactionSource) } @Composable private fun animateElevation( enabled: Boolean, interactionSource: InteractionSource, ): State { val interactions = remember { mutableStateListOf() } LaunchedEffect(interactionSource) { interactionSource.interactions.collect { interaction -> when (interaction) { is HoverInteraction.Enter -> { interactions.add(interaction) } is HoverInteraction.Exit -> { interactions.remove(interaction.enter) } is FocusInteraction.Focus -> { interactions.add(interaction) } is FocusInteraction.Unfocus -> { interactions.remove(interaction.focus) } is PressInteraction.Press -> { interactions.add(interaction) } is PressInteraction.Release -> { interactions.remove(interaction.press) } is PressInteraction.Cancel -> { interactions.remove(interaction.press) } } } } val interaction = interactions.lastOrNull() val target = if (!enabled) { disabledElevation } else { when (interaction) { is PressInteraction.Press -> pressedElevation is HoverInteraction.Enter -> hoveredElevation is FocusInteraction.Focus -> focusedElevation else -> defaultElevation } } val animatable = remember { Animatable(target, Dp.VectorConverter) } if (!enabled) { // No transition when moving to a disabled state LaunchedEffect(target) { animatable.snapTo(target) } } else { LaunchedEffect(target) { val lastInteraction = when (animatable.targetValue) { pressedElevation -> PressInteraction.Press(Offset.Zero) hoveredElevation -> HoverInteraction.Enter() focusedElevation -> FocusInteraction.Focus() else -> null } animatable.animateElevation( from = lastInteraction, to = interaction, target = target, ) } } return animatable.asState() } override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || other !is ButtonElevation) return false if (defaultElevation != other.defaultElevation) return false if (pressedElevation != other.pressedElevation) return false if (focusedElevation != other.focusedElevation) return false if (hoveredElevation != other.hoveredElevation) return false if (disabledElevation != other.disabledElevation) return false return true } override fun hashCode(): Int { var result = defaultElevation.hashCode() result = 31 * result + pressedElevation.hashCode() result = 31 * result + focusedElevation.hashCode() result = 31 * result + hoveredElevation.hashCode() result = 31 * result + disabledElevation.hashCode() return result } } /** * Represents the container and content colors used in a button in different states. * * - See [M3ButtonDefaults.buttonColors] for the default colors used in a [Button]. * - See [M3ButtonDefaults.elevatedButtonColors] for the default colors used in a [ElevatedButton]. * - See [M3ButtonDefaults.textButtonColors] for the default colors used in a [TextButton]. */ @Immutable class ButtonColors internal constructor( private val containerColor: Color, private val contentColor: Color, private val disabledContainerColor: Color, private val disabledContentColor: Color, ) { /** * Represents the container color for this button, depending on [enabled]. * * @param enabled whether the button is enabled */ @Composable internal fun containerColor(enabled: Boolean): State { return rememberUpdatedState(if (enabled) containerColor else disabledContainerColor) } /** * Represents the content color for this button, depending on [enabled]. * * @param enabled whether the button is enabled */ @Composable internal fun contentColor(enabled: Boolean): State { return rememberUpdatedState(if (enabled) contentColor else disabledContentColor) } override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || other !is ButtonColors) return false if (containerColor != other.containerColor) return false if (contentColor != other.contentColor) return false if (disabledContainerColor != other.disabledContainerColor) return false if (disabledContentColor != other.disabledContentColor) return false return true } override fun hashCode(): Int { var result = containerColor.hashCode() result = 31 * result + contentColor.hashCode() result = 31 * result + disabledContainerColor.hashCode() result = 31 * result + disabledContentColor.hashCode() return result } }