/* * Copyright 2021 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.compose.material3 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.tokens.ElevatedButtonTokens import androidx.compose.material3.tokens.FilledButtonTokens import androidx.compose.material3.tokens.FilledTonalButtonTokens import androidx.compose.material3.tokens.OutlinedButtonTokens import androidx.compose.material3.tokens.TextButtonTokens 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 /** * Material Design button. * * Buttons help people initiate actions, from sending an email, to sharing a document, to liking a * post. * * ![Filled button image](https://developer.android.com/images/reference/androidx/compose/material3/filled-button.png) * * Filled buttons are high-emphasis buttons. Filled buttons have the most visual impact after the * [FloatingActionButton], and should be used for important, final actions that complete a flow, * like "Save", "Join now", or "Confirm". * * @sample androidx.compose.material3.samples.ButtonSample * @sample androidx.compose.material3.samples.ButtonWithIconSample * * Choose the best button for an action based on the amount of emphasis it needs. The more important * an action is, the higher emphasis its button should be. * * - See [OutlinedButton] for a medium-emphasis button with a border. * - See [ElevatedButton] for an [OutlinedButton] with a shadow. * - See [TextButton] for a low-emphasis button with no border. * - See [FilledTonalButton] for a middle ground between [OutlinedButton] and [Button]. * * The default text style for internal [Text] components will be set to [Typography.labelLarge]. * * @param onClick called when this button is clicked * @param modifier the [Modifier] to be applied to this button * @param enabled controls the enabled state of this button. When `false`, this component will not * respond to user input, and it will appear visually disabled and disabled to accessibility * services. * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s * for this button. You can create and pass in your own `remember`ed instance to observe * [Interaction]s and customize the appearance / behavior of this button in different states. * @param elevation [ButtonElevation] used to resolve the elevation for this button in different * states. This controls the size of the shadow below the button. Additionally, when the container * color is [ColorScheme.surface], this controls the amount of primary color applied as an overlay. * See [ButtonElevation.shadowElevation] and [ButtonElevation.tonalElevation]. * @param shape defines the shape of this button's container, border (when [border] is not null), * and shadow (when using [elevation]) * @param border the border to draw around the container of this button * @param colors [ButtonColors] that will be used to resolve the colors for this button in different * states. See [ButtonDefaults.buttonColors]. * @param contentPadding the spacing values to apply internally between the container and the * content */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun Button( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, elevation: ButtonElevation? = ButtonDefaults.buttonElevation(), shape: Shape = ButtonDefaults.shape, border: BorderStroke? = null, colors: ButtonColors = ButtonDefaults.buttonColors(), contentPadding: PaddingValues = ButtonDefaults.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 // TODO(b/202880001): Apply shadow color from token (will not be possibly any time soon, if ever). Surface( onClick = onClick, modifier = modifier, 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 = ButtonDefaults.MinWidth, minHeight = ButtonDefaults.MinHeight ) .padding(contentPadding), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, content = content ) } } } } /** * Material Design elevated button. * * Buttons help people initiate actions, from sending an email, to sharing a document, to liking a * post. * * ![Elevated button image](https://developer.android.com/images/reference/androidx/compose/material3/elevated-button.png) * * Elevated buttons are high-emphasis buttons that are essentially [FilledTonalButton]s with a * shadow. To prevent shadow creep, only use them when absolutely necessary, such as when the button * requires visual separation from patterned container. * * @sample androidx.compose.material3.samples.ElevatedButtonSample * * Choose the best button for an action based on the amount of emphasis it needs. The more important * an action is, the higher emphasis its button should be. * * - See [Button] for a high-emphasis button without a shadow, also known as a filled button. * - See [FilledTonalButton] for a middle ground between [OutlinedButton] and [Button]. * - See [OutlinedButton] for a medium-emphasis button with a border. * - See [TextButton] for a low-emphasis button with no border. * * The default text style for internal [Text] components will be set to [Typography.labelLarge]. * * @param onClick called when this button is clicked * @param modifier the [Modifier] to be applied to this button * @param enabled controls the enabled state of this button. When `false`, this component will not * respond to user input, and it will appear visually disabled and disabled to accessibility * services. * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s * for this button. You can create and pass in your own `remember`ed instance to observe * [Interaction]s and customize the appearance / behavior of this button in different states. * @param elevation [ButtonElevation] used to resolve the elevation for this button in different * states. This controls the size of the shadow below the button. Additionally, when the container * color is [ColorScheme.surface], this controls the amount of primary color applied as an overlay. * See [ButtonDefaults.elevatedButtonElevation]. * @param shape defines the shape of this button's container, border (when [border] is not null), * and shadow (when using [elevation]) * @param border the border to draw around the container of this button * @param colors [ButtonColors] that will be used to resolve the colors for this button in different * states. See [ButtonDefaults.elevatedButtonColors]. * @param contentPadding the spacing values to apply internally between the container and the * content */ @Composable fun ElevatedButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, elevation: ButtonElevation? = ButtonDefaults.elevatedButtonElevation(), shape: Shape = ButtonDefaults.elevatedShape, border: BorderStroke? = null, colors: ButtonColors = ButtonDefaults.elevatedButtonColors(), contentPadding: PaddingValues = ButtonDefaults.ContentPadding, content: @Composable RowScope.() -> Unit ) = Button( onClick = onClick, modifier = modifier, enabled = enabled, interactionSource = interactionSource, elevation = elevation, shape = shape, border = border, colors = colors, contentPadding = contentPadding, content = content ) /** * Material Design filled tonal button. * * Buttons help people initiate actions, from sending an email, to sharing a document, to liking a * post. * * ![Filled tonal button image](https://developer.android.com/images/reference/androidx/compose/material3/filled-tonal-button.png) * * Filled tonal buttons are medium-emphasis buttons that is an alternative middle ground between * default [Button]s (filled) and [OutlinedButton]s. They can be used in contexts where * lower-priority button requires slightly more emphasis than an outline would give, such as "Next" * in an onboarding flow. Tonal buttons use the secondary color mapping. * * @sample androidx.compose.material3.samples.FilledTonalButtonSample * * Choose the best button for an action based on the amount of emphasis it needs. The more important * an action is, the higher emphasis its button should be. * * - See [Button] for a high-emphasis button without a shadow, also known as a filled button. * - See [ElevatedButton] for a [FilledTonalButton] with a shadow. * - See [OutlinedButton] for a medium-emphasis button with a border. * - See [TextButton] for a low-emphasis button with no border. * * The default text style for internal [Text] components will be set to [Typography.labelLarge]. * * @param onClick called when this button is clicked * @param modifier the [Modifier] to be applied to this button * @param enabled controls the enabled state of this button. When `false`, this component will not * respond to user input, and it will appear visually disabled and disabled to accessibility * services. * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s * for this button. You can create and pass in your own `remember`ed instance to observe * [Interaction]s and customize the appearance / behavior of this button in different states. * @param elevation [ButtonElevation] used to resolve the elevation for this button in different * states. This controls the size of the shadow below the button. Additionally, when the container * color is [ColorScheme.surface], this controls the amount of primary color applied as an overlay. * @param shape defines the shape of this button's container, border (when [border] is not null), * and shadow (when using [elevation]) * @param border the border to draw around the container of this button * @param colors [ButtonColors] that will be used to resolve the colors for this button in different * states. See [ButtonDefaults.filledTonalButtonColors]. * @param contentPadding the spacing values to apply internally between the container and the * content */ @Composable fun FilledTonalButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, elevation: ButtonElevation? = ButtonDefaults.filledTonalButtonElevation(), shape: Shape = ButtonDefaults.filledTonalShape, border: BorderStroke? = null, colors: ButtonColors = ButtonDefaults.filledTonalButtonColors(), contentPadding: PaddingValues = ButtonDefaults.ContentPadding, content: @Composable RowScope.() -> Unit ) = Button( onClick = onClick, modifier = modifier, enabled = enabled, interactionSource = interactionSource, elevation = elevation, shape = shape, border = border, colors = colors, contentPadding = contentPadding, content = content ) /** * Material Design outlined button. * * Buttons help people initiate actions, from sending an email, to sharing a document, to liking a * post. * * ![Outlined button image](https://developer.android.com/images/reference/androidx/compose/material3/outlined-button.png) * * Outlined buttons are medium-emphasis buttons. They contain actions that are important, but are * not the primary action in an app. Outlined buttons pair well with [Button]s to indicate an * alternative, secondary action. * * @sample androidx.compose.material3.samples.OutlinedButtonSample * * Choose the best button for an action based on the amount of emphasis it needs. The more important * an action is, the higher emphasis its button should be. * * - See [Button] for a high-emphasis button without a shadow, also known as a filled button. * - See [FilledTonalButton] for a middle ground between [OutlinedButton] and [Button]. * - See [OutlinedButton] for a medium-emphasis button with a border. * - See [TextButton] for a low-emphasis button with no border. * * The default text style for internal [Text] components will be set to [Typography.labelLarge]. * * @param onClick called when this button is clicked * @param modifier the [Modifier] to be applied to this button * @param enabled controls the enabled state of this button. When `false`, this component will not * respond to user input, and it will appear visually disabled and disabled to accessibility * services. * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s * for this button. You can create and pass in your own `remember`ed instance to observe * [Interaction]s and customize the appearance / behavior of this button in different states. * @param elevation [ButtonElevation] used to resolve the elevation for this button in different * states. This controls the size of the shadow below the button. Additionally, when the container * color is [ColorScheme.surface], this controls the amount of primary color applied as an overlay. * @param shape defines the shape of this button's container, border (when [border] is not null), * and shadow (when using [elevation]). * @param border the border to draw around the container of this button. Pass `null` for no border. * @param colors [ButtonColors] that will be used to resolve the colors for this button in different * states. See [ButtonDefaults.outlinedButtonColors]. * @param contentPadding the spacing values to apply internally between the container and the * content */ @Composable fun OutlinedButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, elevation: ButtonElevation? = null, shape: Shape = ButtonDefaults.outlinedShape, border: BorderStroke? = ButtonDefaults.outlinedButtonBorder, colors: ButtonColors = ButtonDefaults.outlinedButtonColors(), contentPadding: PaddingValues = ButtonDefaults.ContentPadding, content: @Composable RowScope.() -> Unit ) = Button( onClick = onClick, modifier = modifier, enabled = enabled, interactionSource = interactionSource, elevation = elevation, shape = shape, border = border, colors = colors, contentPadding = contentPadding, content = content ) /** * Material Design text button. * * Buttons help people initiate actions, from sending an email, to sharing a document, to liking a * post. * * ![Text button image](https://developer.android.com/images/reference/androidx/compose/material3/text-button.png) * * Text buttons are typically used for less-pronounced actions, including those located in dialogs * and cards. In cards, text buttons help maintain an emphasis on card content. Text buttons are * used for the lowest priority actions, especially when presenting multiple options. * * @sample androidx.compose.material3.samples.TextButtonSample * * Choose the best button for an action based on the amount of emphasis it needs. The more important * an action is, the higher emphasis its button should be. * * - See [Button] for a high-emphasis button without a shadow, also known as a filled button. * - See [ElevatedButton] for a [FilledTonalButton] with a shadow. * - See [FilledTonalButton] for a middle ground between [OutlinedButton] and [Button]. * - See [OutlinedButton] for a medium-emphasis button with a border. * * The default text style for internal [Text] components will be set to [Typography.labelLarge]. * * @param onClick called when this button is clicked * @param modifier the [Modifier] to be applied to this button * @param enabled controls the enabled state of this button. When `false`, this component will not * respond to user input, and it will appear visually disabled and disabled to accessibility * services. * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s * for this button. You can create and pass in your own `remember`ed instance to observe * [Interaction]s and customize the appearance / behavior of this button in different states. * @param elevation [ButtonElevation] used to resolve the elevation for this button in different * states. This controls the size of the shadow below the button. Additionally, when the container * color is [ColorScheme.surface], this controls the amount of primary color applied as an overlay. * A TextButton typically has no elevation, and the default value is `null`. See [ElevatedButton] * for a button with elevation. * @param shape defines the shape of this button's container, border (when [border] is not null), * and shadow (when using [elevation]) * @param border the border to draw around the container of this button * @param colors [ButtonColors] that will be used to resolve the colors for this button in different * states. See [ButtonDefaults.textButtonColors]. * @param contentPadding the spacing values to apply internally between the container and the * content */ @Composable fun TextButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, elevation: ButtonElevation? = null, shape: Shape = ButtonDefaults.textShape, border: BorderStroke? = null, colors: ButtonColors = ButtonDefaults.textButtonColors(), contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding, content: @Composable RowScope.() -> Unit ) = Button( onClick = onClick, modifier = modifier, enabled = enabled, interactionSource = interactionSource, elevation = elevation, shape = shape, border = border, colors = colors, contentPadding = contentPadding, content = content ) // TODO(b/201341237): Use token values for 0 elevation? // TODO(b/201341237): Use token values for null border? // TODO(b/201341237): Use token values for no color (transparent)? /** * Contains the default values used by all 5 button types. * * Default values that apply to all buttons types are [MinWidth], [MinHeight], [IconSize], and * [IconSpacing]. * * A default value that applies only to [Button], [ElevatedButton], [FilledTonalButton], and * [OutlinedButton] is [ContentPadding]. * * Default values that apply only to [Button] are [buttonColors] and [buttonElevation]. * Default values that apply only to [ElevatedButton] are [elevatedButtonColors] and [elevatedButtonElevation]. * Default values that apply only to [FilledTonalButton] are [filledTonalButtonColors] and [filledTonalButtonElevation]. * A default value that applies only to [OutlinedButton] is [outlinedButtonColors]. * Default values that apply only to [TextButton] are [TextButtonContentPadding] and [textButtonColors]. */ object ButtonDefaults { // TODO(b/201344013): Make sure these values stay up to date until replaced with tokens. private val ButtonHorizontalPadding = 24.dp // TODO(b/202453316): There is no current vertical padding in the spec. // Instead, the height is const 40dp, and the content is vertically center-aligned. private val ButtonVerticalPadding = 8.dp /** * The default content padding used by [Button], [ElevatedButton], [FilledTonalButton], and * [OutlinedButton] buttons. * * - See [TextButtonContentPadding] for content padding used by [TextButton]. * - See [ButtonWithIconContentPadding] for content padding used by [Button] that contains [Icon]. */ // TODO(b/201343537): Use tokens. val ContentPadding = PaddingValues( start = ButtonHorizontalPadding, top = ButtonVerticalPadding, end = ButtonHorizontalPadding, bottom = ButtonVerticalPadding ) private val ButtonWithIconHorizontalStartPadding = 16.dp /** The default content padding used by [Button] that contains an [Icon]. */ val ButtonWithIconContentPadding = PaddingValues( start = ButtonWithIconHorizontalStartPadding, top = ButtonVerticalPadding, end = ButtonHorizontalPadding, bottom = ButtonVerticalPadding ) // TODO(b/201344013): Make sure these values stay up to date until replaced with tokens. private val TextButtonHorizontalPadding = 12.dp /** The default content padding used by [TextButton] */ val TextButtonContentPadding = PaddingValues( start = TextButtonHorizontalPadding, top = ContentPadding.calculateTopPadding(), end = TextButtonHorizontalPadding, bottom = ContentPadding.calculateBottomPadding() ) /** * The default min width applied for all buttons. Note that you can override it by applying * Modifier.widthIn directly on the button composable. */ // TODO(b/202453316): Make sure this value stays up to date until replaced with a token. val MinWidth = 58.dp /** * The default min height applied for all buttons. Note that you can override it by applying * Modifier.heightIn directly on the button composable. */ // TODO(b/202453316): Make sure this value stays up to date until replaced with a token. val MinHeight = 40.dp /** The default size of the icon when used inside any button. */ // TODO(b/201344013): Make sure this value stays up to date until replaced with a token. val IconSize = 18.dp /** * The default size of the spacing between an icon and a text when they used inside any button. */ // TODO(b/201344013): Make sure this value stays up to date until replaced with a token. val IconSpacing = 8.dp /** Default shape for a button. */ val shape: Shape @Composable get() = FilledButtonTokens.ContainerShape.toShape() /** Default shape for an elevated button. */ val elevatedShape: Shape @Composable get() = ElevatedButtonTokens.ContainerShape.toShape() /** Default shape for a filled tonal button. */ val filledTonalShape: Shape @Composable get() = FilledTonalButtonTokens.ContainerShape.toShape() /** Default shape for an outlined button. */ val outlinedShape: Shape @Composable get() = OutlinedButtonTokens.ContainerShape.toShape() /** Default shape for a text button. */ val textShape: Shape @Composable get() = TextButtonTokens.ContainerShape.toShape() /** * 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 = FilledButtonTokens.ContainerColor.toColor(), contentColor: Color = FilledButtonTokens.LabelTextColor.toColor(), disabledContainerColor: Color = FilledButtonTokens.DisabledContainerColor.toColor() .copy(alpha = FilledButtonTokens.DisabledContainerOpacity), disabledContentColor: Color = FilledButtonTokens.DisabledLabelTextColor.toColor() .copy(alpha = FilledButtonTokens.DisabledLabelTextOpacity), ): ButtonColors = DefaultButtonColors( containerColor = containerColor, contentColor = contentColor, disabledContainerColor = disabledContainerColor, disabledContentColor = disabledContentColor ) /** * Creates a [ButtonColors] that represents the default container and content colors used in an * [ElevatedButton]. * * @param containerColor the container color of this [ElevatedButton] when enabled * @param contentColor the content color of this [ElevatedButton] when enabled * @param disabledContainerColor the container color of this [ElevatedButton] when not enabled * @param disabledContentColor the content color of this [ElevatedButton] when not enabled */ @Composable fun elevatedButtonColors( containerColor: Color = ElevatedButtonTokens.ContainerColor.toColor(), contentColor: Color = ElevatedButtonTokens.LabelTextColor.toColor(), disabledContainerColor: Color = ElevatedButtonTokens.DisabledContainerColor .toColor() .copy(alpha = ElevatedButtonTokens.DisabledContainerOpacity), disabledContentColor: Color = ElevatedButtonTokens.DisabledLabelTextColor .toColor() .copy(alpha = ElevatedButtonTokens.DisabledLabelTextOpacity), ): ButtonColors = DefaultButtonColors( containerColor = containerColor, contentColor = contentColor, disabledContainerColor = disabledContainerColor, disabledContentColor = disabledContentColor ) /** * Creates a [ButtonColors] that represents the default container and content colors used in an * [FilledTonalButton]. * * @param containerColor the container color of this [FilledTonalButton] when enabled * @param contentColor the content color of this [FilledTonalButton] when enabled * @param disabledContainerColor the container color of this [FilledTonalButton] when not enabled * @param disabledContentColor the content color of this [FilledTonalButton] when not enabled */ @Composable fun filledTonalButtonColors( containerColor: Color = FilledTonalButtonTokens.ContainerColor.toColor(), contentColor: Color = FilledTonalButtonTokens.LabelTextColor.toColor(), disabledContainerColor: Color = FilledTonalButtonTokens.DisabledContainerColor .toColor() .copy(alpha = FilledTonalButtonTokens.DisabledContainerOpacity), disabledContentColor: Color = FilledTonalButtonTokens.DisabledLabelTextColor .toColor() .copy(alpha = FilledTonalButtonTokens.DisabledLabelTextOpacity), ): ButtonColors = DefaultButtonColors( containerColor = containerColor, contentColor = contentColor, disabledContainerColor = disabledContainerColor, disabledContentColor = disabledContentColor ) /** * Creates a [ButtonColors] that represents the default container and content colors used in an * [OutlinedButton]. * * @param containerColor the container color of this [OutlinedButton] when enabled * @param contentColor the content color of this [OutlinedButton] when enabled * @param disabledContainerColor the container color of this [OutlinedButton] when not enabled * @param disabledContentColor the content color of this [OutlinedButton] when not enabled */ @Composable fun outlinedButtonColors( containerColor: Color = Color.Transparent, contentColor: Color = OutlinedButtonTokens.LabelTextColor.toColor(), disabledContainerColor: Color = Color.Transparent, disabledContentColor: Color = OutlinedButtonTokens.DisabledLabelTextColor .toColor() .copy(alpha = OutlinedButtonTokens.DisabledLabelTextOpacity), ): ButtonColors = DefaultButtonColors( containerColor = containerColor, contentColor = contentColor, disabledContainerColor = disabledContainerColor, disabledContentColor = disabledContentColor ) /** * Creates a [ButtonColors] that represents the default container and content colors used in a * [TextButton]. * * @param containerColor the container color of this [TextButton] when enabled * @param contentColor the content color of this [TextButton] when enabled * @param disabledContainerColor the container color of this [TextButton] when not enabled * @param disabledContentColor the content color of this [TextButton] when not enabled */ @Composable fun textButtonColors( containerColor: Color = Color.Transparent, contentColor: Color = TextButtonTokens.LabelTextColor.toColor(), disabledContainerColor: Color = Color.Transparent, disabledContentColor: Color = TextButtonTokens.DisabledLabelTextColor .toColor() .copy(alpha = TextButtonTokens.DisabledLabelTextOpacity), ): ButtonColors = DefaultButtonColors( 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 = FilledButtonTokens.ContainerElevation, pressedElevation: Dp = FilledButtonTokens.PressedContainerElevation, focusedElevation: Dp = FilledButtonTokens.FocusContainerElevation, hoveredElevation: Dp = FilledButtonTokens.HoverContainerElevation, disabledElevation: Dp = FilledButtonTokens.DisabledContainerElevation, ): ButtonElevation { return remember( defaultElevation, pressedElevation, focusedElevation, hoveredElevation, disabledElevation ) { DefaultButtonElevation( defaultElevation = defaultElevation, pressedElevation = pressedElevation, focusedElevation = focusedElevation, hoveredElevation = hoveredElevation, disabledElevation = disabledElevation, ) } } /** * Creates a [ButtonElevation] that will animate between the provided values according to the * Material specification for a [ElevatedButton]. * * @param defaultElevation the elevation used when the [ElevatedButton] is enabled, and has no * other [Interaction]s. * @param pressedElevation the elevation used when this [ElevatedButton] is enabled and pressed. * @param focusedElevation the elevation used when the [ElevatedButton] is enabled and focused. * @param hoveredElevation the elevation used when the [ElevatedButton] is enabled and hovered. * @param disabledElevation the elevation used when the [ElevatedButton] is not enabled. */ @Composable fun elevatedButtonElevation( defaultElevation: Dp = ElevatedButtonTokens.ContainerElevation, pressedElevation: Dp = ElevatedButtonTokens.PressedContainerElevation, focusedElevation: Dp = ElevatedButtonTokens.FocusContainerElevation, hoveredElevation: Dp = ElevatedButtonTokens.HoverContainerElevation, disabledElevation: Dp = ElevatedButtonTokens.DisabledContainerElevation ): ButtonElevation { return remember( defaultElevation, pressedElevation, focusedElevation, hoveredElevation, disabledElevation ) { DefaultButtonElevation( defaultElevation = defaultElevation, pressedElevation = pressedElevation, focusedElevation = focusedElevation, hoveredElevation = hoveredElevation, disabledElevation = disabledElevation ) } } /** * Creates a [ButtonElevation] that will animate between the provided values according to the * Material specification for a [FilledTonalButton]. * * @param defaultElevation the elevation used when the [FilledTonalButton] is enabled, and has no * other [Interaction]s. * @param pressedElevation the elevation used when this [FilledTonalButton] is enabled and * pressed. * @param focusedElevation the elevation used when the [FilledTonalButton] is enabled and focused. * @param hoveredElevation the elevation used when the [FilledTonalButton] is enabled and hovered. * @param disabledElevation the elevation used when the [FilledTonalButton] is not enabled. */ @Composable fun filledTonalButtonElevation( defaultElevation: Dp = FilledTonalButtonTokens.ContainerElevation, pressedElevation: Dp = FilledTonalButtonTokens.PressedContainerElevation, focusedElevation: Dp = FilledTonalButtonTokens.FocusContainerElevation, hoveredElevation: Dp = FilledTonalButtonTokens.HoverContainerElevation, disabledElevation: Dp = 0.dp ): ButtonElevation { return remember( defaultElevation, pressedElevation, focusedElevation, hoveredElevation, disabledElevation ) { DefaultButtonElevation( defaultElevation = defaultElevation, pressedElevation = pressedElevation, focusedElevation = focusedElevation, hoveredElevation = hoveredElevation, disabledElevation = disabledElevation ) } } /** The default [BorderStroke] used by [OutlinedButton]. */ val outlinedButtonBorder: BorderStroke @Composable get() = BorderStroke( width = OutlinedButtonTokens.OutlineWidth, color = OutlinedButtonTokens.OutlineColor.toColor(), ) } /** * Represents the elevation for a button in different states. * * - See [ButtonDefaults.buttonElevation] for the default elevation used in a [Button]. * - See [ButtonDefaults.elevatedButtonElevation] for the default elevation used in a * [ElevatedButton]. */ @Stable interface ButtonElevation { /** * 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 fun tonalElevation(enabled: Boolean, interactionSource: InteractionSource): State /** * 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 fun shadowElevation(enabled: Boolean, interactionSource: InteractionSource): State } /** * Represents the container and content colors used in a button in different states. * * - See [ButtonDefaults.buttonColors] for the default colors used in a [Button]. * - See [ButtonDefaults.elevatedButtonColors] for the default colors used in a [ElevatedButton]. * - See [ButtonDefaults.textButtonColors] for the default colors used in a [TextButton]. */ @Stable interface ButtonColors { /** * Represents the container color for this button, depending on [enabled]. * * @param enabled whether the button is enabled */ @Composable fun containerColor(enabled: Boolean): State /** * Represents the content color for this button, depending on [enabled]. * * @param enabled whether the button is enabled */ @Composable fun contentColor(enabled: Boolean): State } /** Default [ButtonElevation] implementation. */ @Stable private class DefaultButtonElevation( private val defaultElevation: Dp, private val pressedElevation: Dp, private val focusedElevation: Dp, private val hoveredElevation: Dp, private val disabledElevation: Dp, ) : ButtonElevation { @Composable override fun tonalElevation(enabled: Boolean, interactionSource: InteractionSource): State { return animateElevation(enabled = enabled, interactionSource = interactionSource) } @Composable override 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() } } /** Default [ButtonColors] implementation. */ @Immutable private class DefaultButtonColors( private val containerColor: Color, private val contentColor: Color, private val disabledContainerColor: Color, private val disabledContentColor: Color, ) : ButtonColors { @Composable override fun containerColor(enabled: Boolean): State { return rememberUpdatedState(if (enabled) containerColor else disabledContainerColor) } @Composable override 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 || this::class != other::class) return false other as DefaultButtonColors 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 } }