Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ import androidx.compose.animation.core.updateTransition
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsHoveredAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
Expand All @@ -24,14 +27,19 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.dropShadow
import androidx.compose.ui.draw.innerShadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.graphics.shadow.Shadow
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import com.hedvig.android.compose.ui.LayoutWithoutPlacement
import com.hedvig.android.compose.ui.withoutPlacement
Expand All @@ -43,6 +51,9 @@ import com.hedvig.android.design.system.hedvig.tokens.MiniSizeButtonTokens
import com.hedvig.android.design.system.hedvig.tokens.PrimaryAltStyleButtonTokens
import com.hedvig.android.design.system.hedvig.tokens.PrimaryStyleButtonTokens
import com.hedvig.android.design.system.hedvig.tokens.RedStyleButtonTokens
import com.hedvig.android.design.system.hedvig.tokens.RoundedLargeSizeButtonTokens
import com.hedvig.android.design.system.hedvig.tokens.RoundedLiquidGlassStyleButtonTokens
import com.hedvig.android.design.system.hedvig.tokens.RoundedPrimaryStyleButtonTokens
import com.hedvig.android.design.system.hedvig.tokens.SecondaryAltStyleButtonTokens
import com.hedvig.android.design.system.hedvig.tokens.SecondaryStyleButtonTokens
import com.hedvig.android.design.system.hedvig.tokens.SmallSizeButtonTokens
Expand Down Expand Up @@ -119,7 +130,9 @@ fun HedvigButton(
) {
@Suppress("NAME_SHADOWING")
val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
val buttonColors = buttonStyle.style.buttonColors
val style = buttonStyle.style
val size = buttonSize.sizeIn(style)
val buttonColors = style.buttonColors
val containerColor = buttonColors.containerColor(enabled)
val contentColor = buttonColors.contentColor(enabled)
val isHovered by interactionSource.collectIsHoveredAsState()
Expand All @@ -131,21 +144,31 @@ fun HedvigButton(
containerColor
},
)

@Suppress("NAME_SHADOWING")
val shape = shape ?: size.shape
val glass = style.glassMaterial.takeIf { enabled }
Surface(
onClick = onClick,
modifier = modifier,
modifier = if (glass == null) {
modifier
} else {
modifier.glassMaterial(glass, color, shape)
},
onClickLabel = onClickLabel,
role = Role.Button,
enabled = enabled,
shape = shape ?: buttonSize.size.shape,
shape = shape,
border = border,
color = color,
// The rim shadows have to sit on top of the container fill, which is only reachable from outside
// this Surface, so the fill moves into the modifier above and the Surface itself stays see-through.
color = if (glass == null) color else Color.Transparent,
contentColor = contentColor,
interactionSource = interactionSource,
) {
ProvideTextStyle(buttonSize.size.textStyle) {
ProvideTextStyle(size.textStyle) {
Row(
modifier = Modifier.padding(buttonSize.size.contentPadding),
modifier = Modifier.padding(size.contentPadding),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
content = content,
Expand Down Expand Up @@ -246,6 +269,31 @@ fun HedvigButtonGhostWithBorder(
)
}

@HedvigPreview
@Composable
private fun PreviewRoundedButtons() {
HedvigTheme {
// A tinted backdrop, so that the translucent liquid glass container is distinguishable from it.
Surface(color = HedvigTheme.colorScheme.surfaceSecondary) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(16.dp),
) {
for (buttonStyle in listOf(
ButtonDefaults.ButtonStyle.RoundedPrimary,
ButtonDefaults.ButtonStyle.RoundedLiquidGlass,
)) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
HedvigButton("Make a claim", {}, enabled = true, buttonStyle = buttonStyle)
HedvigButton("Disabled", {}, enabled = false, buttonStyle = buttonStyle)
HedvigButton("Loading", {}, enabled = true, buttonStyle = buttonStyle, isLoading = true)
}
}
}
}
}
}

object ButtonDefaults {
internal val buttonStyle: ButtonStyle = ButtonStyle.Primary
internal val buttonSize: ButtonSize = ButtonSize.Large
Expand All @@ -257,6 +305,12 @@ object ButtonDefaults {
SecondaryAlt,
Ghost,
Red,

/** A filled pill with the sheen and drop shadow of the iOS glass material. */
RoundedPrimary,

/** A translucent pill of the iOS glass material, letting the backdrop show through. */
RoundedLiquidGlass,
}

enum class ButtonSize {
Expand All @@ -275,6 +329,8 @@ private val ButtonDefaults.ButtonStyle.style: Style
ButtonDefaults.ButtonStyle.SecondaryAlt -> Style.SecondaryAlt
ButtonDefaults.ButtonStyle.Ghost -> Style.Ghost
ButtonDefaults.ButtonStyle.Red -> Style.Red
ButtonDefaults.ButtonStyle.RoundedPrimary -> Style.RoundedPrimary
ButtonDefaults.ButtonStyle.RoundedLiquidGlass -> Style.RoundedLiquidGlass
}

private val ButtonSize.size: Size
Expand All @@ -285,6 +341,16 @@ private val ButtonSize.size: Size
ButtonSize.Mini -> Size.Mini
}

/**
* The metrics this size takes on within [style]. The rounded styles get their own pill metrics at
* [ButtonSize.Large]; at every smaller size they fall back to the standard button metrics.
*/
@Composable
private fun ButtonSize.sizeIn(style: Style): Size = when {
style.glassMaterial != null && this == ButtonSize.Large -> Size.LargeRounded
else -> size
}

@Immutable
private data class ButtonColors(
val containerColor: Color,
Expand Down Expand Up @@ -343,6 +409,25 @@ private sealed interface Size {
get() = LargeSizeButtonTokens.ContainerShape.value
}

object LargeRounded : Size {
override val contentPadding: PaddingValues = PaddingValues(
top = RoundedLargeSizeButtonTokens.TopPadding,
bottom = RoundedLargeSizeButtonTokens.BottomPadding,
start = RoundedLargeSizeButtonTokens.HorizontalPadding,
end = RoundedLargeSizeButtonTokens.HorizontalPadding,
)

override val textStyle: TextStyle
@Composable
@ReadOnlyComposable
get() = RoundedLargeSizeButtonTokens.LabelTextFont.value

override val shape: Shape
@Composable
@ReadOnlyComposable
get() = RoundedLargeSizeButtonTokens.ContainerShape.value
}

object Medium : Size {
override val contentPadding: PaddingValues = PaddingValues(
top = MediumSizeButtonTokens.TopPadding,
Expand Down Expand Up @@ -405,6 +490,11 @@ private sealed interface Style {
@get:Composable
val buttonColors: ButtonColors

/** Non-null on the styles that render the iOS glass material. */
val glassMaterial: GlassMaterial?
@Composable
get() = null

data object Primary : Style {
override val buttonColors: ButtonColors
@Composable
Expand Down Expand Up @@ -505,6 +595,56 @@ private sealed interface Style {
}
}

data object RoundedPrimary : Style {
// The fill inverts between themes: near-black on light, opaque white on dark.
override val glassMaterial: GlassMaterial
@Composable
get() = if (HedvigTheme.colorScheme.isLight) opaqueDarkGlassMaterial else regularGlassMaterial

override val buttonColors: ButtonColors
@Composable
get() = with(HedvigTheme.colorScheme) {
remember(this) {
ButtonColors(
containerColor = fromToken(RoundedPrimaryStyleButtonTokens.ContainerColor),
contentColor = fromToken(RoundedPrimaryStyleButtonTokens.ContentColor),
disabledContainerColor = fromToken(RoundedPrimaryStyleButtonTokens.DisabledContainerColor),
disabledContentColor = fromToken(RoundedPrimaryStyleButtonTokens.DisabledContentColor),
hoverContainerColor = fromToken(RoundedPrimaryStyleButtonTokens.HoverContainerColor),
hoverContentColor = fromToken(RoundedPrimaryStyleButtonTokens.HoverContentColor),
activeLoadingIndicatorColor = fromToken(RoundedPrimaryStyleButtonTokens.ActiveLoadingIndicatorColor),
inactiveLoadingIndicatorColor = fromToken(RoundedPrimaryStyleButtonTokens.InactiveLoadingIndicatorColor),
redTextColor = fromToken(RoundedPrimaryStyleButtonTokens.RedContentColor),
)
}
}
}

data object RoundedLiquidGlass : Style {
override val glassMaterial: GlassMaterial
@Composable
get() = liquidGlassMaterial

override val buttonColors: ButtonColors
@Composable
get() = with(HedvigTheme.colorScheme) {
remember(this) {
ButtonColors(
containerColor = fromToken(RoundedLiquidGlassStyleButtonTokens.ContainerColor),
contentColor = fromToken(RoundedLiquidGlassStyleButtonTokens.ContentColor),
disabledContainerColor = fromToken(RoundedLiquidGlassStyleButtonTokens.DisabledContainerColor),
disabledContentColor = fromToken(RoundedLiquidGlassStyleButtonTokens.DisabledContentColor),
hoverContainerColor = fromToken(RoundedLiquidGlassStyleButtonTokens.HoverContainerColor),
hoverContentColor = fromToken(RoundedLiquidGlassStyleButtonTokens.HoverContentColor),
activeLoadingIndicatorColor = fromToken(RoundedLiquidGlassStyleButtonTokens.ActiveLoadingIndicatorColor),
inactiveLoadingIndicatorColor =
fromToken(RoundedLiquidGlassStyleButtonTokens.InactiveLoadingIndicatorColor),
redTextColor = fromToken(RoundedLiquidGlassStyleButtonTokens.RedContentColor),
)
}
}
}

data object Red : Style {
override val buttonColors: ButtonColors
@Composable
Expand Down
Loading
Loading