|
@@ -1,26 +1,45 @@
|
|
|
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.ButtonColors
|
|
|
-import androidx.compose.material3.ButtonDefaults
|
|
|
-import androidx.compose.material3.ButtonElevation
|
|
|
+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
|
|
|
|
|
|
@Composable
|
|
|
fun TextButton(
|
|
@@ -30,10 +49,15 @@ fun TextButton(
|
|
|
enabled: Boolean = true,
|
|
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
|
|
elevation: ButtonElevation? = null,
|
|
|
- shape: Shape = ButtonDefaults.textShape,
|
|
|
+ shape: Shape = M3ButtonDefaults.textShape,
|
|
|
border: BorderStroke? = null,
|
|
|
- colors: ButtonColors = ButtonDefaults.textButtonColors(),
|
|
|
- contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding,
|
|
|
+ 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(
|
|
@@ -58,10 +82,10 @@ fun Button(
|
|
|
enabled: Boolean = true,
|
|
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
|
|
elevation: ButtonElevation? = ButtonDefaults.buttonElevation(),
|
|
|
- shape: Shape = ButtonDefaults.textShape,
|
|
|
+ shape: Shape = M3ButtonDefaults.textShape,
|
|
|
border: BorderStroke? = null,
|
|
|
colors: ButtonColors = ButtonDefaults.buttonColors(),
|
|
|
- contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
|
|
|
+ contentPadding: PaddingValues = M3ButtonDefaults.ContentPadding,
|
|
|
content: @Composable RowScope.() -> Unit,
|
|
|
) {
|
|
|
val containerColor = colors.containerColor(enabled).value
|
|
@@ -86,8 +110,8 @@ fun Button(
|
|
|
ProvideTextStyle(value = MaterialTheme.typography.labelLarge) {
|
|
|
Row(
|
|
|
Modifier.defaultMinSize(
|
|
|
- minWidth = ButtonDefaults.MinWidth,
|
|
|
- minHeight = ButtonDefaults.MinHeight,
|
|
|
+ minWidth = M3ButtonDefaults.MinWidth,
|
|
|
+ minHeight = M3ButtonDefaults.MinHeight,
|
|
|
)
|
|
|
.padding(contentPadding),
|
|
|
horizontalArrangement = Arrangement.Center,
|
|
@@ -98,3 +122,255 @@ fun Button(
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+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<Dp> {
|
|
|
+ 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<Dp> {
|
|
|
+ return animateElevation(enabled = enabled, interactionSource = interactionSource)
|
|
|
+ }
|
|
|
+
|
|
|
+ @Composable
|
|
|
+ private fun animateElevation(
|
|
|
+ enabled: Boolean,
|
|
|
+ interactionSource: InteractionSource,
|
|
|
+ ): State<Dp> {
|
|
|
+ val interactions = remember { mutableStateListOf<Interaction>() }
|
|
|
+ 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<Color> {
|
|
|
+ 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<Color> {
|
|
|
+ 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
|
|
|
+ }
|
|
|
+}
|