feat(surface): implement Frutiger-Aero themed surfaces with glass and acrylic styles

This commit is contained in:
Kingkor Roy Tirtho 2026-10-07 15:29:25 +06:00
parent fa9ad9981c
commit 4da5b25a5c
4 changed files with 743 additions and 440 deletions

View File

@ -0,0 +1,465 @@
/*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package dev.krtirtho.spotube.core.ui.base
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlin.random.Random
/**
* The Frutiger-Aero material families available to [BaseSurface].
*
* - [Watery]: deep aqua water with caustic light and bubbles.
* - [Icy]: pale frosted ice with crystalline facets and frost.
* - [Chrome]: brushed metal with a hard specular reflection.
* - [Acrylic]: translucent tinted panel with fine grain.
* - [Glass]: clear, glossy glass with a strong sheen.
*/
enum class SurfaceStyle {
Watery,
Icy,
Chrome,
Acrylic,
Glass,
}
/**
* A themed surface panel rendered in one of the Frutiger-Aero [SurfaceStyle] materials.
*
* Distinct from [androidx.compose.material3.Surface] — this draws decorative material
* backgrounds (gradients, gloss, grain, bubbles, facets) and provides a matching
* [LocalContentColor] so text/content inside stays legible.
*/
@Composable
fun BaseSurface(
modifier: Modifier = Modifier,
style: SurfaceStyle = SurfaceStyle.Glass,
shape: Shape = RectangleShape,
elevation: Dp? = null,
contentColor: Color? = null,
contentPadding: PaddingValues = PaddingValues.Zero,
contentAlignment: Alignment = Alignment.TopStart,
content: @Composable BoxScope.() -> Unit,
) {
val isLight = MaterialTheme.colorScheme.surface.luminance() > 0.5f
val onSurface = MaterialTheme.colorScheme.onSurface
val resolvedElevation = elevation ?: when (style) {
SurfaceStyle.Watery -> 14.dp
SurfaceStyle.Icy -> 12.dp
SurfaceStyle.Chrome -> 10.dp
SurfaceStyle.Acrylic -> 16.dp
SurfaceStyle.Glass -> 12.dp
}
val resolvedContentColor = contentColor ?: when (style) {
SurfaceStyle.Watery -> Color.White
SurfaceStyle.Icy -> if (isLight) Color(0xFF0B3A55) else Color(0xFFDCEBF7)
SurfaceStyle.Chrome -> Color(0xFF1B2733)
SurfaceStyle.Acrylic, SurfaceStyle.Glass -> onSurface
}
val ambient = when (style) {
SurfaceStyle.Watery -> Color(0xFF0E6FB0).copy(alpha = 0.38f)
SurfaceStyle.Icy -> Color(0xFF3FA9E0).copy(alpha = 0.3f)
SurfaceStyle.Chrome -> Color.Black.copy(alpha = 0.3f)
SurfaceStyle.Acrylic -> onSurface.copy(alpha = 0.22f)
SurfaceStyle.Glass -> onSurface.copy(alpha = 0.18f)
}
val spot = ambient.copy(alpha = (ambient.alpha + 0.08f).coerceAtMost(1f))
val borderBrush = when (style) {
SurfaceStyle.Watery -> Brush.verticalGradient(
listOf(Color.White.copy(alpha = 0.7f), Color.White.copy(alpha = 0.12f)),
)
SurfaceStyle.Icy -> Brush.verticalGradient(
listOf(Color.White.copy(alpha = 0.95f), Color.White.copy(alpha = 0.35f)),
)
SurfaceStyle.Chrome -> Brush.verticalGradient(
listOf(Color.White.copy(alpha = 0.95f), Color.Black.copy(alpha = 0.28f)),
)
SurfaceStyle.Acrylic -> Brush.verticalGradient(
listOf(Color.White.copy(alpha = 0.5f), Color.White.copy(alpha = 0.12f)),
)
SurfaceStyle.Glass -> Brush.verticalGradient(
listOf(Color.White.copy(alpha = 0.9f), Color.White.copy(alpha = 0.3f)),
)
}
// Translucent materials (Glass, Acrylic) must not use a drop shadow: it is drawn
// behind the node and, because the panel is see-through, shows up as a dark inset
// shape. Their depth comes from the edge, sheen and bevel instead.
val castsShadow = style != SurfaceStyle.Glass && style != SurfaceStyle.Acrylic
CompositionLocalProvider(LocalContentColor provides resolvedContentColor) {
Box(
modifier = modifier
.then(
if (castsShadow) {
Modifier.shadow(
elevation = resolvedElevation,
shape = shape,
ambientColor = ambient,
spotColor = spot,
)
} else {
Modifier
}
)
.clip(shape)
.drawWithCache {
onDrawBehind { drawSurfaceMaterial(style, isLight) }
}
.border(BorderStroke(1.dp, borderBrush), shape)
.padding(contentPadding),
contentAlignment = contentAlignment,
content = content,
)
}
}
private fun DrawScope.drawSurfaceMaterial(style: SurfaceStyle, isLight: Boolean) {
when (style) {
SurfaceStyle.Watery -> drawWatery(isLight)
SurfaceStyle.Icy -> drawIcy(isLight)
SurfaceStyle.Chrome -> drawChrome()
SurfaceStyle.Acrylic -> drawAcrylic(isLight)
SurfaceStyle.Glass -> drawGlass(isLight)
}
}
private fun DrawScope.drawGlass(isLight: Boolean) {
// See-through glass: a faint blue-tinted fill so whatever is behind shows through,
// defined by its bright edge, sheen and shadow rather than an opaque body.
val tint = if (isLight) Color(0xFFCFE0F2) else Color(0xFF9FB8D6)
drawRect(
brush = Brush.verticalGradient(
listOf(
tint.copy(alpha = if (isLight) 0.4f else 0.2f),
tint.copy(alpha = if (isLight) 0.24f else 0.1f),
),
),
)
drawGloss(if (isLight) 0.55f else 0.16f)
drawBottomReflection(if (isLight) 0.4f else 0.1f)
drawInnerBevel(isLight)
drawBubbles(seed = 11, count = 3, alpha = if (isLight) 0.4f else 0.14f)
}
private fun DrawScope.drawAcrylic(isLight: Boolean) {
// See-through acrylic: a translucent tinted sheet with a fine grain.
val tint = if (isLight) Color(0xFFCFD9E8) else Color(0xFF8FA2B8)
drawRect(
brush = Brush.verticalGradient(
listOf(
tint.copy(alpha = if (isLight) 0.45f else 0.22f),
tint.copy(alpha = if (isLight) 0.28f else 0.12f),
),
),
)
// Fine grain so the panel reads as a translucent acrylic sheet.
val random = Random(23)
repeat(180) {
val light = random.nextBoolean()
drawCircle(
color = if (light) {
Color.White.copy(alpha = if (isLight) 0.16f else 0.06f)
} else {
Color.Black.copy(alpha = if (isLight) 0.06f else 0.08f)
},
radius = 0.7f,
center = Offset(random.nextFloat() * size.width, random.nextFloat() * size.height),
)
}
drawGloss(if (isLight) 0.4f else 0.12f, heightFraction = 0.45f)
drawInnerBevel(isLight)
}
private fun DrawScope.drawChrome() {
drawRect(
brush = Brush.verticalGradient(
0f to Color(0xFFF8FBFF),
0.16f to Color(0xFFD6DEE7),
0.4f to Color(0xFF8E99A8),
0.5f to Color(0xFFEDF2F7),
0.62f to Color(0xFF9AA5B4),
1f to Color(0xFF6C7787),
),
)
// Hard specular reflection near the top.
drawRect(
brush = Brush.verticalGradient(
colors = listOf(Color.White.copy(alpha = 0.85f), Color.Transparent),
startY = size.height * 0.05f,
endY = size.height * 0.22f,
),
)
// Darkened lower edge for the metal bevel.
drawRect(
brush = Brush.verticalGradient(
colors = listOf(Color.Transparent, Color.Black.copy(alpha = 0.25f)),
startY = size.height * 0.78f,
endY = size.height,
),
)
}
private fun DrawScope.drawIcy(isLight: Boolean) {
val colors = if (isLight) {
listOf(Color(0xFFF4FCFF), Color(0xFFCBE9FB), Color(0xFF9BD3F0))
} else {
// Snowy-night ice: dim slate-blue with faint frost.
listOf(Color(0xFF2A3B4D), Color(0xFF1B2A39), Color(0xFF0F1A26))
}
drawRect(brush = Brush.verticalGradient(colors))
val frost = if (isLight) Color.White else Color(0xFFB8CCE0)
// Crystalline facets (kept inset so they are never clipped by the shape).
val random = Random(37)
repeat(4) {
val s = size.minDimension * (0.2f + random.nextFloat() * 0.3f)
val x = random.nextFloat() * (size.width - s).coerceAtLeast(0f)
val y = random.nextFloat() * (size.height - s).coerceAtLeast(0f)
val path = Path().apply {
moveTo(x, y)
lineTo(x + s, y + s * 0.4f)
lineTo(x + s * 0.4f, y + s)
close()
}
drawPath(path, frost.copy(alpha = if (isLight) 0.25f else 0.12f))
}
// Frost speckles.
repeat(90) {
drawCircle(
color = frost.copy(alpha = if (isLight) 0.5f else 0.28f),
radius = 0.8f,
center = Offset(random.nextFloat() * size.width, random.nextFloat() * size.height),
)
}
drawGloss(if (isLight) 0.6f else 0.12f, heightFraction = 0.5f)
}
private fun DrawScope.drawWatery(isLight: Boolean) {
val colors = if (isLight) {
listOf(Color(0xFFA6E3FF), Color(0xFF3FB6EA), Color(0xFF0E6FB0))
} else {
// Moonlit night water: deep blue with a pale silver sheen.
listOf(Color(0xFF1C3A5E), Color(0xFF102942), Color(0xFF071827))
}
drawRect(brush = Brush.verticalGradient(colors))
val sheen = if (isLight) Color.White else Color(0xFFB9D2F0)
// Caustic / moonlight pooling from the surface.
drawRect(
brush = Brush.verticalGradient(
colors = listOf(sheen.copy(alpha = if (isLight) 0.35f else 0.22f), Color.Transparent),
startY = 0f,
endY = size.height * 0.5f,
),
)
val random = Random(53)
repeat(5) {
val rx = size.width * (0.12f + random.nextFloat() * 0.2f)
val cx = rx + random.nextFloat() * (size.width - 2 * rx).coerceAtLeast(0f)
val cy = rx * 0.5f + random.nextFloat() * (size.height * 0.6f - rx).coerceAtLeast(0f)
drawOval(
brush = Brush.radialGradient(
colors = listOf(sheen.copy(alpha = if (isLight) 0.3f else 0.18f), Color.Transparent),
center = Offset(cx, cy),
radius = rx,
),
topLeft = Offset(cx - rx, cy - rx * 0.5f),
size = Size(rx * 2f, rx),
)
}
drawBubbles(seed = 71, count = 6, alpha = if (isLight) 0.4f else 0.22f)
}
private fun DrawScope.drawGloss(alpha: Float, heightFraction: Float = 0.5f) {
drawRect(
brush = Brush.verticalGradient(
colors = listOf(Color.White.copy(alpha = alpha), Color.Transparent),
startY = 0f,
endY = size.height * heightFraction,
),
)
}
private fun DrawScope.drawBottomReflection(alpha: Float) {
drawRect(
brush = Brush.verticalGradient(
colors = listOf(Color.Transparent, Color.White.copy(alpha = alpha)),
startY = size.height * 0.7f,
endY = size.height,
),
)
}
/** A bright inner top edge plus a shaded inner bottom edge, giving the pane a glass bevel. */
private fun DrawScope.drawInnerBevel(isLight: Boolean) {
drawRect(
brush = Brush.verticalGradient(
colors = listOf(Color.White.copy(alpha = if (isLight) 0.85f else 0.22f), Color.Transparent),
startY = 0f,
endY = size.height * 0.1f,
),
)
drawRect(
brush = Brush.verticalGradient(
colors = listOf(Color.Transparent, Color.Black.copy(alpha = if (isLight) 0.08f else 0.28f)),
startY = size.height * 0.9f,
endY = size.height,
),
)
}
private fun DrawScope.drawBubbles(seed: Int, count: Int, alpha: Float) {
val random = Random(seed)
repeat(count) {
val radius = size.minDimension * (0.05f + random.nextFloat() * 0.14f)
// Keep bubbles inset so they are never clipped into hard arcs at the edges.
val cx = radius + random.nextFloat() * (size.width - 2 * radius).coerceAtLeast(0f)
val cy = radius + random.nextFloat() * (size.height - 2 * radius).coerceAtLeast(0f)
val center = Offset(cx, cy)
drawCircle(
brush = Brush.radialGradient(
colors = listOf(Color.White.copy(alpha = alpha), Color.White.copy(alpha = 0f)),
center = center,
radius = radius,
),
radius = radius,
center = center,
)
}
}
@BaseUIPhonePreview
@BaseUIDesktopPreview
@Composable
private fun BaseSurfacePreview() {
BaseUIPreview {
Column(
modifier = Modifier
.fillMaxSize()
.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(14.dp),
) {
SurfaceStyle.entries.forEach { style ->
BaseSurface(
style = style,
modifier = Modifier
.fillMaxWidth()
.height(84.dp),
) {
Text(
text = style.name,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
)
}
}
}
}
}
@BaseUIPhonePreview
@Composable
private fun BaseSurfaceGridPreview() {
BaseUIPreview {
Column(
modifier = Modifier
.fillMaxSize()
.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(14.dp),
) {
SurfaceStyle.entries.chunked(2).forEach { rowStyles ->
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(14.dp),
) {
rowStyles.forEach { style ->
BaseSurface(
style = style,
modifier = Modifier
.weight(1f)
.height(140.dp),
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.SpaceBetween,
) {
Text(
text = style.name,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
)
Text(
text = "Frutiger Aero",
style = MaterialTheme.typography.labelMedium,
)
}
}
}
repeat(2 - rowStyles.size) {
androidx.compose.foundation.layout.Spacer(modifier = Modifier.weight(1f))
}
}
}
}
}
}

View File

@ -30,6 +30,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.luminance import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -219,19 +220,51 @@ fun rememberBaseUITheme(): BaseUITheme {
val scheme = MaterialTheme.colorScheme val scheme = MaterialTheme.colorScheme
val isLight = scheme.surface.luminance() > 0.5f val isLight = scheme.surface.luminance() > 0.5f
val containerLighter = if (isLight) Color.White else scheme.surfaceContainerHigh // ------------------------------------------------------------------
val containerDarker = if (isLight) Color(0xFFF2F2F4) else scheme.surfaceContainer // Frutiger-Aero glass palette.
val containerPressed = if (isLight) Color(0xFFE0E0E3) else scheme.surfaceContainerHighest //
val highlight = if (isLight) Color.White.copy(alpha = 0.9f) else Color.White.copy(alpha = 0.06f) // Surfaces are glossy, top-lit gradients with a strong white specular
// highlight and translucent "glass" edges, lifted by soft, coloured
// shadows — the aqua/glass language of Frutiger-Aero.
// ------------------------------------------------------------------
val glassTop = if (isLight) {
Color.White
} else {
lerp(scheme.surfaceContainerHigh, Color.White, 0.10f)
}
val glassBottom = if (isLight) {
lerp(scheme.surface, Color.White, 0.04f)
} else {
scheme.surfaceContainer
}
val glassPressed = if (isLight) {
lerp(scheme.surfaceContainerHighest, Color.White, 0.06f)
} else {
lerp(scheme.surfaceContainerHighest, Color.Black, 0.08f)
}
val gloss = if (isLight) Color.White.copy(alpha = 0.75f) else Color.White.copy(alpha = 0.18f)
val glossSoft = if (isLight) Color.White.copy(alpha = 0.5f) else Color.White.copy(alpha = 0.12f)
val glassEdge = if (isLight) Color.White.copy(alpha = 0.95f) else Color.White.copy(alpha = 0.22f)
val glassBorder = if (isLight) {
lerp(scheme.outlineVariant, Color.White, 0.55f)
} else {
scheme.outlineVariant.copy(alpha = 0.5f)
}
val shadowColor = scheme.onSurface val shadowColor = scheme.onSurface
val border = scheme.outlineVariant
val secondaryContainer = scheme.secondaryContainer fun glassBrush(top: Color, bottom: Color): Brush =
val secondaryContainerPressed = secondaryContainer.copy(alpha = if (isLight) 0.85f else 0.92f) Brush.verticalGradient(listOf(top, bottom))
val secondaryHighlight =
if (isLight) Color.White.copy(alpha = 0.5f) else Color.White.copy(alpha = 0.08f)
val defaultShape = RoundedCornerShape(14.dp) fun aqua(accent: Color): Brush = Brush.verticalGradient(
listOf(
lerp(accent, Color.White, 0.45f),
accent,
lerp(accent, Color.Black, 0.14f),
)
)
val defaultShape = RoundedCornerShape(16.dp)
val defaultShapeState = val defaultShapeState =
BaseUITheme.InteractionState<Shape>(defaultShape, defaultShape, defaultShape, defaultShape) BaseUITheme.InteractionState<Shape>(defaultShape, defaultShape, defaultShape, defaultShape)
val noBorder = BaseUITheme.Border(Color.Transparent, 0.dp) val noBorder = BaseUITheme.Border(Color.Transparent, 0.dp)
@ -239,231 +272,208 @@ fun rememberBaseUITheme(): BaseUITheme {
val noShadow = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent) val noShadow = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent)
val noShadowState = BaseUITheme.InteractionState(noShadow, noShadow, noShadow, noShadow) val noShadowState = BaseUITheme.InteractionState(noShadow, noShadow, noShadow, noShadow)
val defaultButtonPadding = BaseUITheme.InteractionState( val defaultButtonPadding = BaseUITheme.InteractionState(
PaddingValues(horizontal = 20.dp, vertical = 10.dp), PaddingValues(horizontal = 22.dp, vertical = 10.dp),
PaddingValues(horizontal = 20.dp, vertical = 10.dp), PaddingValues(horizontal = 22.dp, vertical = 10.dp),
PaddingValues(horizontal = 20.dp, vertical = 10.dp), PaddingValues(horizontal = 22.dp, vertical = 10.dp),
PaddingValues(horizontal = 20.dp, vertical = 10.dp), PaddingValues(horizontal = 22.dp, vertical = 10.dp),
) )
val iconButtonPadding = BaseUITheme.InteractionState( val iconButtonPadding = BaseUITheme.InteractionState(
PaddingValues(8.dp), PaddingValues(9.dp),
PaddingValues(8.dp), PaddingValues(9.dp),
PaddingValues(8.dp), PaddingValues(9.dp),
PaddingValues(8.dp), PaddingValues(9.dp),
) )
// --- Glass (outline) -------------------------------------------------
val outlineColors = BaseUITheme.InteractionState( val outlineColors = BaseUITheme.InteractionState(
normal = BaseUITheme.ButtonColors( normal = BaseUITheme.ButtonColors(
background = Brush.verticalGradient(listOf(containerLighter, containerDarker)), background = glassBrush(glassTop, glassBottom),
foreground = scheme.onSurface, foreground = scheme.onSurface,
highlight = highlight, highlight = gloss,
), ),
hovered = BaseUITheme.ButtonColors( hovered = BaseUITheme.ButtonColors(
background = Brush.verticalGradient(listOf(containerLighter, containerDarker)), background = glassBrush(glassTop, glassBottom),
foreground = scheme.onSurface, foreground = scheme.onSurface,
highlight = highlight, highlight = Color.White.copy(alpha = if (isLight) 0.9f else 0.26f),
), ),
pressed = BaseUITheme.ButtonColors( pressed = BaseUITheme.ButtonColors(
background = Brush.verticalGradient(listOf(containerPressed, containerPressed)), background = glassBrush(glassPressed, glassPressed),
foreground = scheme.onSurface, foreground = scheme.onSurface,
highlight = highlight, highlight = glossSoft,
), ),
focused = BaseUITheme.ButtonColors( focused = BaseUITheme.ButtonColors(
background = Brush.verticalGradient(listOf(containerLighter, containerDarker)), background = glassBrush(glassTop, glassBottom),
foreground = scheme.onSurface, foreground = scheme.onSurface,
highlight = highlight, highlight = gloss,
), ),
) )
val outlineShadow = BaseUITheme.InteractionState( val glassShadow = BaseUITheme.InteractionState(
normal = BaseUITheme.Shadow( normal = BaseUITheme.Shadow(
6.dp, 8.dp,
true, true,
shadowColor.copy(alpha = 0.15f), shadowColor.copy(alpha = 0.16f),
shadowColor.copy(alpha = 0.18f) shadowColor.copy(alpha = 0.2f),
), ),
hovered = BaseUITheme.Shadow( hovered = BaseUITheme.Shadow(
9.dp, 14.dp,
true, true,
shadowColor.copy(alpha = 0.22f), shadowColor.copy(alpha = 0.22f),
shadowColor.copy(alpha = 0.26f) shadowColor.copy(alpha = 0.26f),
), ),
pressed = BaseUITheme.Shadow( pressed = BaseUITheme.Shadow(
1.dp, 2.dp,
true, true,
shadowColor.copy(alpha = 0.08f), shadowColor.copy(alpha = 0.1f),
shadowColor.copy(alpha = 0.1f) shadowColor.copy(alpha = 0.12f),
), ),
focused = BaseUITheme.Shadow( focused = BaseUITheme.Shadow(
6.dp, 8.dp,
true, true,
shadowColor.copy(alpha = 0.15f), shadowColor.copy(alpha = 0.16f),
shadowColor.copy(alpha = 0.18f) shadowColor.copy(alpha = 0.2f),
), ),
) )
val outlineBorder = BaseUITheme.InteractionState( val glassBorderState = BaseUITheme.InteractionState(
normal = BaseUITheme.Border(border, 0.5.dp), normal = BaseUITheme.Border(glassBorder, 1.dp),
hovered = BaseUITheme.Border(border, 0.5.dp), hovered = BaseUITheme.Border(glassEdge, 1.dp),
pressed = BaseUITheme.Border(border.copy(alpha = 0.7f), 0.5.dp), pressed = BaseUITheme.Border(glassBorder.copy(alpha = 0.7f), 1.dp),
focused = BaseUITheme.Border(border, 0.5.dp), focused = BaseUITheme.Border(glassEdge, 1.dp),
) )
// --- Aqua accent (primary) -------------------------------------------
val primaryEdge = lerp(scheme.primary, Color.White, 0.5f)
val primaryColors = BaseUITheme.InteractionState( val primaryColors = BaseUITheme.InteractionState(
normal = BaseUITheme.ButtonColors( normal = BaseUITheme.ButtonColors(aqua(scheme.primary), scheme.onPrimary, gloss),
background = Brush.verticalGradient(listOf(scheme.primary, scheme.primary)),
foreground = scheme.onPrimary,
highlight = Color.White.copy(alpha = 0.25f),
),
hovered = BaseUITheme.ButtonColors( hovered = BaseUITheme.ButtonColors(
background = Brush.verticalGradient(listOf(scheme.primary, scheme.primary)), aqua(scheme.primary),
foreground = scheme.onPrimary, scheme.onPrimary,
highlight = Color.White.copy(alpha = 0.25f), Color.White.copy(alpha = if (isLight) 0.9f else 0.3f),
), ),
pressed = BaseUITheme.ButtonColors( pressed = BaseUITheme.ButtonColors(
background = Brush.verticalGradient( glassBrush(
listOf( lerp(scheme.primary, Color.Black, 0.16f),
scheme.primary.copy(alpha = 0.85f), lerp(scheme.primary, Color.Black, 0.24f),
scheme.primary
)
), ),
foreground = scheme.onPrimary, scheme.onPrimary,
highlight = Color.White.copy(alpha = 0.25f), glossSoft,
),
focused = BaseUITheme.ButtonColors(
background = Brush.verticalGradient(listOf(scheme.primary, scheme.primary)),
foreground = scheme.onPrimary,
highlight = Color.White.copy(alpha = 0.25f),
), ),
focused = BaseUITheme.ButtonColors(aqua(scheme.primary), scheme.onPrimary, gloss),
) )
val primaryShadow = BaseUITheme.InteractionState( val primaryShadow = BaseUITheme.InteractionState(
normal = BaseUITheme.Shadow( normal = BaseUITheme.Shadow(
6.dp, 10.dp,
true, true,
scheme.primary.copy(alpha = 0.35f), scheme.primary.copy(alpha = 0.35f),
scheme.primary.copy(alpha = 0.4f) scheme.primary.copy(alpha = 0.42f),
), ),
hovered = BaseUITheme.Shadow( hovered = BaseUITheme.Shadow(
9.dp, 16.dp,
true, true,
scheme.primary.copy(alpha = 0.45f), scheme.primary.copy(alpha = 0.45f),
scheme.primary.copy(alpha = 0.5f) scheme.primary.copy(alpha = 0.5f),
), ),
pressed = BaseUITheme.Shadow( pressed = BaseUITheme.Shadow(
1.dp, 3.dp,
true, true,
scheme.primary.copy(alpha = 0.2f), scheme.primary.copy(alpha = 0.22f),
scheme.primary.copy(alpha = 0.25f) scheme.primary.copy(alpha = 0.28f),
), ),
focused = BaseUITheme.Shadow( focused = BaseUITheme.Shadow(
6.dp, 10.dp,
true, true,
scheme.primary.copy(alpha = 0.35f), scheme.primary.copy(alpha = 0.35f),
scheme.primary.copy(alpha = 0.4f) scheme.primary.copy(alpha = 0.42f),
), ),
) )
val primaryBorder = BaseUITheme.InteractionState( val primaryBorder = BaseUITheme.InteractionState(
normal = BaseUITheme.Border(scheme.primary, 0.5.dp), normal = BaseUITheme.Border(primaryEdge, 1.dp),
hovered = BaseUITheme.Border(scheme.primary, 0.5.dp), hovered = BaseUITheme.Border(primaryEdge, 1.dp),
pressed = BaseUITheme.Border(scheme.primary, 0.5.dp), pressed = BaseUITheme.Border(primaryEdge.copy(alpha = 0.8f), 1.dp),
focused = BaseUITheme.Border(scheme.primary, 0.5.dp), focused = BaseUITheme.Border(primaryEdge, 1.dp),
) )
// --- Secondary (soft aqua glass) -------------------------------------
val secondaryBase = scheme.secondaryContainer
val secondaryColors = BaseUITheme.InteractionState( val secondaryColors = BaseUITheme.InteractionState(
normal = BaseUITheme.ButtonColors( normal = BaseUITheme.ButtonColors(
background = Brush.verticalGradient( glassBrush(lerp(secondaryBase, Color.White, 0.5f), secondaryBase),
listOf( scheme.onSecondaryContainer,
secondaryContainer, gloss,
secondaryContainer.copy(alpha = if (secondaryContainer.luminance() > 0.5f) 0.92f else 1f)
)
),
foreground = scheme.onSecondaryContainer,
highlight = secondaryHighlight,
), ),
hovered = BaseUITheme.ButtonColors( hovered = BaseUITheme.ButtonColors(
background = Brush.verticalGradient( glassBrush(lerp(secondaryBase, Color.White, 0.55f), secondaryBase),
listOf( scheme.onSecondaryContainer,
secondaryContainer, Color.White.copy(alpha = if (isLight) 0.85f else 0.24f),
secondaryContainer.copy(alpha = if (secondaryContainer.luminance() > 0.5f) 0.92f else 1f)
)
),
foreground = scheme.onSecondaryContainer,
highlight = secondaryHighlight,
), ),
pressed = BaseUITheme.ButtonColors( pressed = BaseUITheme.ButtonColors(
background = Brush.verticalGradient( glassBrush(
listOf( lerp(secondaryBase, Color.Black, 0.08f),
secondaryContainerPressed, lerp(secondaryBase, Color.Black, 0.14f),
secondaryContainerPressed
)
), ),
foreground = scheme.onSecondaryContainer, scheme.onSecondaryContainer,
highlight = secondaryHighlight, glossSoft,
), ),
focused = BaseUITheme.ButtonColors( focused = BaseUITheme.ButtonColors(
background = Brush.verticalGradient( glassBrush(lerp(secondaryBase, Color.White, 0.5f), secondaryBase),
listOf( scheme.onSecondaryContainer,
secondaryContainer, gloss,
secondaryContainer.copy(alpha = if (secondaryContainer.luminance() > 0.5f) 0.92f else 1f)
)
),
foreground = scheme.onSecondaryContainer,
highlight = secondaryHighlight,
), ),
) )
val secondaryShadow = BaseUITheme.InteractionState( val secondaryShadow = BaseUITheme.InteractionState(
normal = BaseUITheme.Shadow( normal = BaseUITheme.Shadow(
6.dp, 8.dp,
true, true,
shadowColor.copy(alpha = 0.15f), secondaryBase.copy(alpha = 0.4f),
shadowColor.copy(alpha = 0.18f) secondaryBase.copy(alpha = 0.5f),
), ),
hovered = BaseUITheme.Shadow( hovered = BaseUITheme.Shadow(
9.dp, 14.dp,
true, true,
shadowColor.copy(alpha = 0.22f), secondaryBase.copy(alpha = 0.5f),
shadowColor.copy(alpha = 0.26f) secondaryBase.copy(alpha = 0.6f),
), ),
pressed = BaseUITheme.Shadow( pressed = BaseUITheme.Shadow(
1.dp, 2.dp,
true, true,
shadowColor.copy(alpha = 0.08f), secondaryBase.copy(alpha = 0.3f),
shadowColor.copy(alpha = 0.1f) secondaryBase.copy(alpha = 0.35f),
), ),
focused = BaseUITheme.Shadow( focused = BaseUITheme.Shadow(
6.dp, 8.dp,
true, true,
shadowColor.copy(alpha = 0.15f), secondaryBase.copy(alpha = 0.4f),
shadowColor.copy(alpha = 0.18f) secondaryBase.copy(alpha = 0.5f),
), ),
) )
val secondaryBorder = BaseUITheme.InteractionState( val secondaryBorder = BaseUITheme.InteractionState(
normal = BaseUITheme.Border(secondaryContainer.copy(alpha = 0.5f), 0.5.dp), normal = BaseUITheme.Border(lerp(secondaryBase, Color.White, 0.5f), 1.dp),
hovered = BaseUITheme.Border(secondaryContainer.copy(alpha = 0.5f), 0.5.dp), hovered = BaseUITheme.Border(lerp(secondaryBase, Color.White, 0.6f), 1.dp),
pressed = BaseUITheme.Border(secondaryContainer.copy(alpha = 0.5f), 0.5.dp), pressed = BaseUITheme.Border(lerp(secondaryBase, Color.White, 0.4f), 1.dp),
focused = BaseUITheme.Border(secondaryContainer.copy(alpha = 0.5f), 0.5.dp), focused = BaseUITheme.Border(lerp(secondaryBase, Color.White, 0.5f), 1.dp),
) )
// --- Ghost -----------------------------------------------------------
val ghostColors = BaseUITheme.InteractionState( val ghostColors = BaseUITheme.InteractionState(
normal = BaseUITheme.ButtonColors( normal = BaseUITheme.ButtonColors(
background = Brush.verticalGradient(listOf(Color.Transparent, Color.Transparent)), background = glassBrush(Color.Transparent, Color.Transparent),
foreground = scheme.onSurface, foreground = scheme.onSurface,
highlight = Color.Transparent, highlight = Color.Transparent,
), ),
hovered = BaseUITheme.ButtonColors( hovered = BaseUITheme.ButtonColors(
background = Brush.verticalGradient( background = glassBrush(
listOf( lerp(scheme.surfaceContainerHigh, Color.White, 0.1f),
containerPressed.copy(alpha = 0.5f), scheme.surfaceContainer,
containerPressed.copy(alpha = 0.5f)
)
), ),
foreground = scheme.onSurface, foreground = scheme.onSurface,
highlight = Color.Transparent, highlight = glossSoft,
), ),
pressed = BaseUITheme.ButtonColors( pressed = BaseUITheme.ButtonColors(
background = Brush.verticalGradient(listOf(containerPressed, containerPressed)), background = glassBrush(glassPressed, glassPressed),
foreground = scheme.onSurface, foreground = scheme.onSurface,
highlight = Color.Transparent, highlight = Color.Transparent,
), ),
focused = BaseUITheme.ButtonColors( focused = BaseUITheme.ButtonColors(
background = Brush.verticalGradient(listOf(Color.Transparent, Color.Transparent)), background = glassBrush(Color.Transparent, Color.Transparent),
foreground = scheme.onSurface, foreground = scheme.onSurface,
highlight = Color.Transparent, highlight = Color.Transparent,
), ),
@ -472,8 +482,8 @@ fun rememberBaseUITheme(): BaseUITheme {
val outlineStyle = BaseUITheme.ButtonStyle( val outlineStyle = BaseUITheme.ButtonStyle(
outlineColors, outlineColors,
defaultShapeState, defaultShapeState,
outlineShadow, glassShadow,
outlineBorder, glassBorderState,
defaultButtonPadding defaultButtonPadding
) )
val primaryStyle = BaseUITheme.ButtonStyle( val primaryStyle = BaseUITheme.ButtonStyle(
@ -501,8 +511,8 @@ fun rememberBaseUITheme(): BaseUITheme {
val outlineIconStyle = BaseUITheme.ButtonStyle( val outlineIconStyle = BaseUITheme.ButtonStyle(
outlineColors, outlineColors,
defaultShapeState, defaultShapeState,
outlineShadow, glassShadow,
outlineBorder, glassBorderState,
iconButtonPadding iconButtonPadding
) )
val primaryIconStyle = BaseUITheme.ButtonStyle( val primaryIconStyle = BaseUITheme.ButtonStyle(
@ -534,20 +544,20 @@ fun rememberBaseUITheme(): BaseUITheme {
focused = scheme.onSurface, focused = scheme.onSurface,
) )
val checkBoxShape = RoundedCornerShape(6.dp) val checkBoxShape = RoundedCornerShape(7.dp)
val checkBoxShapeState = val checkBoxShapeState =
BaseUITheme.InteractionState<Shape>(checkBoxShape, checkBoxShape, checkBoxShape, checkBoxShape) BaseUITheme.InteractionState<Shape>(checkBoxShape, checkBoxShape, checkBoxShape, checkBoxShape)
val noPadding = val noPadding =
BaseUITheme.InteractionState(PaddingValues(0.dp), PaddingValues(0.dp), PaddingValues(0.dp), PaddingValues(0.dp)) BaseUITheme.InteractionState(PaddingValues(0.dp), PaddingValues(0.dp), PaddingValues(0.dp), PaddingValues(0.dp))
val chipTabShape = RoundedCornerShape(10.dp) val chipTabShape = RoundedCornerShape(999.dp)
val chipTabShapeState = val chipTabShapeState =
BaseUITheme.InteractionState<Shape>(chipTabShape, chipTabShape, chipTabShape, chipTabShape) BaseUITheme.InteractionState<Shape>(chipTabShape, chipTabShape, chipTabShape, chipTabShape)
val chipTabPadding = BaseUITheme.InteractionState( val chipTabPadding = BaseUITheme.InteractionState(
PaddingValues(horizontal = 14.dp, vertical = 4.dp), PaddingValues(horizontal = 16.dp, vertical = 6.dp),
PaddingValues(horizontal = 14.dp, vertical = 4.dp), PaddingValues(horizontal = 16.dp, vertical = 6.dp),
PaddingValues(horizontal = 14.dp, vertical = 4.dp), PaddingValues(horizontal = 16.dp, vertical = 6.dp),
PaddingValues(horizontal = 14.dp, vertical = 4.dp), PaddingValues(horizontal = 16.dp, vertical = 6.dp),
) )
val toggleShape = RoundedCornerShape(50) val toggleShape = RoundedCornerShape(50)
@ -575,16 +585,16 @@ fun rememberBaseUITheme(): BaseUITheme {
), ),
textField = TextFieldTheme( textField = TextFieldTheme(
background = BaseUITheme.InteractionState( background = BaseUITheme.InteractionState(
normal = Brush.verticalGradient(listOf(containerLighter, containerDarker)), normal = glassBrush(glassTop, glassBottom),
hovered = Brush.verticalGradient(listOf(containerLighter, containerDarker)), hovered = glassBrush(glassTop, glassBottom),
pressed = Brush.verticalGradient(listOf(containerLighter, containerDarker)), pressed = glassBrush(glassPressed, glassPressed),
focused = Brush.verticalGradient(listOf(containerLighter, containerDarker)), focused = glassBrush(glassTop, glassBottom),
), ),
highlight = BaseUITheme.InteractionState( highlight = BaseUITheme.InteractionState(
normal = highlight, normal = gloss,
hovered = highlight, hovered = gloss,
pressed = highlight, pressed = glossSoft,
focused = highlight, focused = Color.White.copy(alpha = if (isLight) 0.85f else 0.22f),
), ),
shape = BaseUITheme.InteractionState( shape = BaseUITheme.InteractionState(
normal = defaultShape, normal = defaultShape,
@ -593,53 +603,48 @@ fun rememberBaseUITheme(): BaseUITheme {
focused = defaultShape, focused = defaultShape,
), ),
border = BaseUITheme.InteractionState( border = BaseUITheme.InteractionState(
normal = BaseUITheme.Border(border.copy(alpha = 0.85f), 0.5.dp), normal = BaseUITheme.Border(glassBorder, 1.dp),
hovered = BaseUITheme.Border(border.copy(alpha = 0.85f), 0.5.dp), hovered = BaseUITheme.Border(glassEdge, 1.dp),
pressed = BaseUITheme.Border(border, 0.5.dp), pressed = BaseUITheme.Border(glassBorder.copy(alpha = 0.7f), 1.dp),
focused = BaseUITheme.Border(scheme.primary, 0.5.dp), focused = BaseUITheme.Border(primaryEdge, 1.5.dp),
), ),
shadow = BaseUITheme.InteractionState( shadow = BaseUITheme.InteractionState(
normal = BaseUITheme.Shadow( normal = BaseUITheme.Shadow(
9.dp, 8.dp,
true, true,
shadowColor.copy(alpha = 0.22f), shadowColor.copy(alpha = 0.14f),
shadowColor.copy(alpha = 0.26f)
),
hovered = BaseUITheme.Shadow(
9.dp,
true,
shadowColor.copy(alpha = 0.22f),
shadowColor.copy(alpha = 0.26f)
),
pressed = BaseUITheme.Shadow(
6.dp,
true,
shadowColor.copy(alpha = 0.15f),
shadowColor.copy(alpha = 0.18f) shadowColor.copy(alpha = 0.18f)
), ),
focused = BaseUITheme.Shadow( hovered = BaseUITheme.Shadow(
9.dp, 10.dp,
true, true,
scheme.primary.copy(alpha = 0.2f), shadowColor.copy(alpha = 0.18f),
scheme.primary.copy(alpha = 0.25f) shadowColor.copy(alpha = 0.22f)
),
pressed = BaseUITheme.Shadow(
4.dp,
true,
shadowColor.copy(alpha = 0.1f),
shadowColor.copy(alpha = 0.12f)
),
focused = BaseUITheme.Shadow(
10.dp,
true,
scheme.primary.copy(alpha = 0.22f),
scheme.primary.copy(alpha = 0.28f)
), ),
), ),
padding = PaddingValues(horizontal = 14.dp, vertical = 10.dp), padding = PaddingValues(horizontal = 16.dp, vertical = 12.dp),
textStyle = TextStyle.Default, textStyle = TextStyle.Default,
cursor = SolidColor(scheme.primary), cursor = SolidColor(scheme.primary),
foreground = textFieldForeground, foreground = textFieldForeground,
), ),
autoCompleteMenuTheme = AutoCompleteMenuTheme( autoCompleteMenuTheme = AutoCompleteMenuTheme(
background = Brush.verticalGradient( background = glassBrush(glassTop, glassBottom),
listOf( shape = RoundedCornerShape(16.dp),
scheme.surfaceContainerHigh, border = BaseUITheme.Border(glassBorder, 1.dp),
scheme.surfaceContainer
)
),
shape = RoundedCornerShape(12.dp),
border = BaseUITheme.Border(border, 0.5.dp),
padding = PaddingValues(0.dp), padding = PaddingValues(0.dp),
shadowElevation = 8.dp, shadowElevation = 16.dp,
), ),
slider = SliderTheme( slider = SliderTheme(
trackActiveColor = BaseUITheme.InteractionState( trackActiveColor = BaseUITheme.InteractionState(
@ -649,10 +654,10 @@ fun rememberBaseUITheme(): BaseUITheme {
focused = scheme.primary, focused = scheme.primary,
), ),
trackInactiveColor = BaseUITheme.InteractionState( trackInactiveColor = BaseUITheme.InteractionState(
normal = border.copy(alpha = 0.6f), normal = glassBorder,
hovered = border.copy(alpha = 0.6f), hovered = glassBorder,
pressed = border.copy(alpha = 0.6f), pressed = glassBorder,
focused = border.copy(alpha = 0.6f), focused = glassBorder,
), ),
thumbColor = BaseUITheme.InteractionState( thumbColor = BaseUITheme.InteractionState(
normal = scheme.primary, normal = scheme.primary,
@ -664,94 +669,44 @@ fun rememberBaseUITheme(): BaseUITheme {
normal = BaseUITheme.Shadow( normal = BaseUITheme.Shadow(
5.dp, 5.dp,
true, true,
scheme.primary.copy(alpha = 0.35f), scheme.primary.copy(alpha = 0.4f),
scheme.primary.copy(alpha = 0.4f) scheme.primary.copy(alpha = 0.45f)
), ),
hovered = BaseUITheme.Shadow( hovered = BaseUITheme.Shadow(
8.dp, 8.dp,
true, true,
scheme.primary.copy(alpha = 0.4f), scheme.primary.copy(alpha = 0.45f),
scheme.primary.copy(alpha = 0.5f) scheme.primary.copy(alpha = 0.5f)
), ),
pressed = BaseUITheme.Shadow( pressed = BaseUITheme.Shadow(
2.dp, 2.dp,
true, true,
scheme.primary.copy(alpha = 0.2f), scheme.primary.copy(alpha = 0.25f),
scheme.primary.copy(alpha = 0.25f) scheme.primary.copy(alpha = 0.3f)
), ),
focused = BaseUITheme.Shadow( focused = BaseUITheme.Shadow(
5.dp, 5.dp,
true, true,
scheme.primary.copy(alpha = 0.35f), scheme.primary.copy(alpha = 0.4f),
scheme.primary.copy(alpha = 0.4f) scheme.primary.copy(alpha = 0.45f)
), ),
), ),
stepActiveColor = scheme.onPrimary.copy(alpha = 0.5f), stepActiveColor = Color.White.copy(alpha = 0.7f),
stepInactiveColor = scheme.onSurface.copy(alpha = 0.25f), stepInactiveColor = scheme.onSurface.copy(alpha = 0.25f),
), ),
checkBox = CheckBoxTheme( checkBox = CheckBoxTheme(
selected = BaseUITheme.ButtonStyle( selected = BaseUITheme.ButtonStyle(
colors = primaryColors, colors = primaryColors,
shape = checkBoxShapeState, shape = checkBoxShapeState,
shadow = BaseUITheme.InteractionState( shadow = primaryShadow,
normal = BaseUITheme.Shadow(
6.dp,
true,
scheme.primary.copy(alpha = 0.3f),
scheme.primary.copy(alpha = 0.35f)
),
hovered = BaseUITheme.Shadow(
8.dp,
true,
scheme.primary.copy(alpha = 0.4f),
scheme.primary.copy(alpha = 0.45f)
),
pressed = BaseUITheme.Shadow(
1.dp,
true,
scheme.primary.copy(alpha = 0.2f),
scheme.primary.copy(alpha = 0.25f)
),
focused = BaseUITheme.Shadow(
6.dp,
true,
scheme.primary.copy(alpha = 0.3f),
scheme.primary.copy(alpha = 0.35f)
),
),
border = primaryBorder, border = primaryBorder,
padding = noPadding, padding = noPadding,
), ),
unselected = BaseUITheme.ButtonStyle( unselected = BaseUITheme.ButtonStyle(
colors = outlineColors, colors = outlineColors,
shape = checkBoxShapeState, shape = checkBoxShapeState,
shadow = BaseUITheme.InteractionState( shadow = glassShadow,
normal = BaseUITheme.Shadow( border = glassBorderState,
3.dp,
true,
shadowColor.copy(alpha = 0.15f),
shadowColor.copy(alpha = 0.18f)
),
hovered = BaseUITheme.Shadow(
5.dp,
true,
shadowColor.copy(alpha = 0.2f),
shadowColor.copy(alpha = 0.24f)
),
pressed = BaseUITheme.Shadow(
1.dp,
true,
shadowColor.copy(alpha = 0.08f),
shadowColor.copy(alpha = 0.1f)
),
focused = BaseUITheme.Shadow(
3.dp,
true,
shadowColor.copy(alpha = 0.15f),
shadowColor.copy(alpha = 0.18f)
),
),
border = outlineBorder,
padding = noPadding, padding = noPadding,
), ),
checkmarkColor = scheme.onPrimary, checkmarkColor = scheme.onPrimary,
@ -767,131 +722,24 @@ fun rememberBaseUITheme(): BaseUITheme {
unselected = BaseUITheme.ButtonStyle( unselected = BaseUITheme.ButtonStyle(
colors = outlineColors, colors = outlineColors,
shape = chipTabShapeState, shape = chipTabShapeState,
shadow = outlineShadow, shadow = glassShadow,
border = outlineBorder, border = glassBorderState,
padding = chipTabPadding, padding = chipTabPadding,
), ),
), ),
toggle = BaseUITheme.ToggleTheme( toggle = BaseUITheme.ToggleTheme(
checked = BaseUITheme.ButtonStyle( checked = BaseUITheme.ButtonStyle(
colors = BaseUITheme.InteractionState( colors = primaryColors,
normal = BaseUITheme.ButtonColors(
background = Brush.verticalGradient(listOf(scheme.primary, scheme.primary)),
foreground = scheme.onPrimary,
highlight = Color.White.copy(alpha = 0.25f),
),
hovered = BaseUITheme.ButtonColors(
background = Brush.verticalGradient(listOf(scheme.primary, scheme.primary)),
foreground = scheme.onPrimary,
highlight = Color.White.copy(alpha = 0.25f),
),
pressed = BaseUITheme.ButtonColors(
background = Brush.verticalGradient(
listOf(
scheme.primary.copy(alpha = 0.85f),
scheme.primary
)
),
foreground = scheme.onPrimary,
highlight = Color.White.copy(alpha = 0.25f),
),
focused = BaseUITheme.ButtonColors(
background = Brush.verticalGradient(listOf(scheme.primary, scheme.primary)),
foreground = scheme.onPrimary,
highlight = Color.White.copy(alpha = 0.25f),
),
),
shape = toggleShapeState, shape = toggleShapeState,
shadow = BaseUITheme.InteractionState( shadow = primaryShadow,
normal = BaseUITheme.Shadow( border = primaryBorder,
4.dp,
true,
scheme.primary.copy(alpha = 0.3f),
scheme.primary.copy(alpha = 0.35f)
),
hovered = BaseUITheme.Shadow(
6.dp,
true,
scheme.primary.copy(alpha = 0.4f),
scheme.primary.copy(alpha = 0.45f)
),
pressed = BaseUITheme.Shadow(
1.dp,
true,
scheme.primary.copy(alpha = 0.2f),
scheme.primary.copy(alpha = 0.25f)
),
focused = BaseUITheme.Shadow(
4.dp,
true,
scheme.primary.copy(alpha = 0.3f),
scheme.primary.copy(alpha = 0.35f)
),
),
border = BaseUITheme.InteractionState(
normal = BaseUITheme.Border(scheme.primary, 0.5.dp),
hovered = BaseUITheme.Border(scheme.primary, 0.5.dp),
pressed = BaseUITheme.Border(scheme.primary, 0.5.dp),
focused = BaseUITheme.Border(scheme.primary, 0.5.dp),
),
padding = togglePadding, padding = togglePadding,
), ),
unchecked = BaseUITheme.ButtonStyle( unchecked = BaseUITheme.ButtonStyle(
colors = BaseUITheme.InteractionState( colors = outlineColors,
normal = BaseUITheme.ButtonColors(
background = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
foreground = scheme.onSurface,
highlight = highlight,
),
hovered = BaseUITheme.ButtonColors(
background = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
foreground = scheme.onSurface,
highlight = highlight,
),
pressed = BaseUITheme.ButtonColors(
background = Brush.verticalGradient(listOf(containerPressed, containerPressed)),
foreground = scheme.onSurface,
highlight = highlight,
),
focused = BaseUITheme.ButtonColors(
background = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
foreground = scheme.onSurface,
highlight = highlight,
),
),
shape = toggleShapeState, shape = toggleShapeState,
shadow = BaseUITheme.InteractionState( shadow = glassShadow,
normal = BaseUITheme.Shadow( border = glassBorderState,
4.dp,
true,
shadowColor.copy(alpha = 0.15f),
shadowColor.copy(alpha = 0.18f)
),
hovered = BaseUITheme.Shadow(
6.dp,
true,
shadowColor.copy(alpha = 0.22f),
shadowColor.copy(alpha = 0.26f)
),
pressed = BaseUITheme.Shadow(
1.dp,
true,
shadowColor.copy(alpha = 0.08f),
shadowColor.copy(alpha = 0.1f)
),
focused = BaseUITheme.Shadow(
4.dp,
true,
shadowColor.copy(alpha = 0.15f),
shadowColor.copy(alpha = 0.18f)
),
),
border = BaseUITheme.InteractionState(
normal = BaseUITheme.Border(border, 0.5.dp),
hovered = BaseUITheme.Border(border, 0.5.dp),
pressed = BaseUITheme.Border(border.copy(alpha = 0.7f), 0.5.dp),
focused = BaseUITheme.Border(border, 0.5.dp),
),
padding = togglePadding, padding = togglePadding,
), ),
thumb = BaseUITheme.InteractionState( thumb = BaseUITheme.InteractionState(
@ -928,61 +776,62 @@ fun rememberBaseUITheme(): BaseUITheme {
), ),
), ),
card = BaseUITheme.CardTheme( card = BaseUITheme.CardTheme(
background = Brush.verticalGradient(listOf(containerLighter, containerDarker)), background = glassBrush(glassTop, glassBottom),
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(20.dp),
border = BaseUITheme.Border(border, 0.5.dp), border = BaseUITheme.Border(glassBorder, 1.dp),
padding = PaddingValues(0.dp), padding = PaddingValues(0.dp),
shadow = BaseUITheme.Shadow( shadow = BaseUITheme.Shadow(
6.dp, 12.dp,
true, true,
shadowColor.copy(alpha = 0.15f), shadowColor.copy(alpha = 0.16f),
shadowColor.copy(alpha = 0.18f) shadowColor.copy(alpha = 0.2f)
), ),
), ),
listRowTile = BaseUITheme.ListRowTheme( listRowTile = BaseUITheme.ListRowTheme(
background = BaseUITheme.AdvancedInteractionState( background = BaseUITheme.AdvancedInteractionState(
normal = Brush.verticalGradient(listOf(Color.Transparent, Color.Transparent)), normal = glassBrush(Color.Transparent, Color.Transparent),
hovered = Brush.verticalGradient(listOf(containerLighter, containerDarker)), hovered = glassBrush(glassTop, glassBottom),
pressed = Brush.verticalGradient(listOf(containerPressed, containerPressed)), pressed = glassBrush(glassPressed, glassPressed),
focused = Brush.verticalGradient(listOf(containerLighter, containerDarker)), focused = glassBrush(glassTop, glassBottom),
selected = Brush.verticalGradient( selected = glassBrush(
listOf( lerp(scheme.primaryContainer, Color.White, 0.4f),
scheme.primaryContainer.copy(alpha = 0.3f), scheme.primaryContainer,
scheme.primaryContainer.copy(alpha = 0.3f)
)
), ),
disabled = Brush.verticalGradient( disabled = glassBrush(
listOf( glassTop.copy(alpha = 0.5f),
containerLighter.copy(alpha = 0.5f), glassBottom.copy(alpha = 0.5f),
containerDarker.copy(alpha = 0.5f)
)
), ),
), ),
shape = BaseUITheme.AdvancedInteractionState( shape = BaseUITheme.AdvancedInteractionState(
normal = RoundedCornerShape(8.dp), normal = RoundedCornerShape(10.dp),
hovered = RoundedCornerShape(8.dp), hovered = RoundedCornerShape(10.dp),
pressed = RoundedCornerShape(8.dp), pressed = RoundedCornerShape(10.dp),
focused = RoundedCornerShape(8.dp), focused = RoundedCornerShape(10.dp),
selected = RoundedCornerShape(8.dp), selected = RoundedCornerShape(10.dp),
disabled = RoundedCornerShape(8.dp), disabled = RoundedCornerShape(10.dp),
), ),
border = BaseUITheme.AdvancedInteractionState( border = BaseUITheme.AdvancedInteractionState(
normal = BaseUITheme.Border(Color.Transparent, 0.dp), normal = BaseUITheme.Border(Color.Transparent, 0.dp),
hovered = BaseUITheme.Border(Color.Transparent, 0.dp), hovered = BaseUITheme.Border(glassBorder, 1.dp),
pressed = BaseUITheme.Border(Color.Transparent, 0.dp), pressed = BaseUITheme.Border(Color.Transparent, 0.dp),
focused = BaseUITheme.Border(Color.Transparent, 0.dp), focused = BaseUITheme.Border(glassBorder, 1.dp),
selected = BaseUITheme.Border(Color.Transparent, 0.dp), selected = BaseUITheme.Border(Color.Transparent, 0.dp),
disabled = BaseUITheme.Border(Color.Transparent, 0.dp), disabled = BaseUITheme.Border(Color.Transparent, 0.dp),
), ),
shadow = BaseUITheme.AdvancedInteractionState( shadow = BaseUITheme.AdvancedInteractionState(
normal = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent), normal = noShadow,
hovered = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent), hovered = BaseUITheme.Shadow(
pressed = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent), 6.dp,
focused = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent), true,
selected = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent), shadowColor.copy(alpha = 0.12f),
disabled = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent), shadowColor.copy(alpha = 0.16f)
),
pressed = noShadow,
focused = noShadow,
selected = noShadow,
disabled = noShadow,
), ),
padding = PaddingValues(horizontal = 8.dp, vertical = 6.dp), padding = PaddingValues(horizontal = 10.dp, vertical = 8.dp),
textStyle = BaseUITheme.AdvancedInteractionState( textStyle = BaseUITheme.AdvancedInteractionState(
normal = TextStyle.Default, normal = TextStyle.Default,
hovered = TextStyle.Default, hovered = TextStyle.Default,
@ -1001,17 +850,12 @@ fun rememberBaseUITheme(): BaseUITheme {
), ),
), ),
dialog = BaseUITheme.DialogTheme( dialog = BaseUITheme.DialogTheme(
background = Brush.verticalGradient( background = glassBrush(glassTop, glassBottom),
listOf(
scheme.surface,
scheme.surfaceContainerHigh,
)
),
scrim = Color.Black.copy(alpha = 0.42f), scrim = Color.Black.copy(alpha = 0.42f),
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(24.dp),
border = BaseUITheme.Border(border, 0.5.dp), border = BaseUITheme.Border(glassEdge, 1.dp),
shadow = BaseUITheme.Shadow( shadow = BaseUITheme.Shadow(
24.dp, 28.dp,
true, true,
shadowColor.copy(alpha = 0.3f), shadowColor.copy(alpha = 0.3f),
shadowColor.copy(alpha = 0.4f) shadowColor.copy(alpha = 0.4f)
@ -1019,27 +863,20 @@ fun rememberBaseUITheme(): BaseUITheme {
padding = PaddingValues(24.dp), padding = PaddingValues(24.dp),
), ),
dropdownMenu = BaseUITheme.DropdownMenuTheme( dropdownMenu = BaseUITheme.DropdownMenuTheme(
background = Brush.verticalGradient( background = glassBrush(glassTop, glassBottom),
listOf( shape = RoundedCornerShape(16.dp),
scheme.surfaceContainerHigh, border = BaseUITheme.Border(glassBorder, 1.dp),
scheme.surfaceContainer,
)
),
shape = RoundedCornerShape(12.dp),
border = BaseUITheme.Border(border, 0.5.dp),
shadow = BaseUITheme.Shadow( shadow = BaseUITheme.Shadow(
12.dp, 16.dp,
true, true,
shadowColor.copy(alpha = 0.2f), shadowColor.copy(alpha = 0.22f),
shadowColor.copy(alpha = 0.25f) shadowColor.copy(alpha = 0.28f)
),
itemHoverBackground = Brush.verticalGradient(
listOf(containerLighter, containerDarker)
), ),
itemHoverBackground = glassBrush(glassTop, glassBottom),
itemForeground = scheme.onSurface, itemForeground = scheme.onSurface,
itemHighlight = highlight, itemHighlight = gloss,
padding = PaddingValues(4.dp), padding = PaddingValues(6.dp),
itemPadding = PaddingValues(horizontal = 14.dp, vertical = 10.dp), itemPadding = PaddingValues(horizontal = 14.dp, vertical = 11.dp),
), ),
) )
} }

View File

@ -24,15 +24,27 @@ import androidx.compose.ui.unit.dp
fun Modifier.highlight(color: Color): Modifier = fun Modifier.highlight(color: Color): Modifier =
drawWithCache { drawWithCache {
val highlightBrush = Brush.verticalGradient( // Frutiger-Aero glass: a bright specular sheen across the top half plus a
colors = listOf(color, Color.Transparent), // faint reflection rising from the bottom edge.
val topGloss = Brush.verticalGradient(
colors = listOf(color, color.copy(alpha = color.alpha * 0.25f), Color.Transparent),
startY = 0f, startY = 0f,
endY = size.height * 0.5f, endY = size.height * 0.55f,
)
val bottomReflection = Brush.verticalGradient(
colors = listOf(Color.Transparent, color.copy(alpha = color.alpha * 0.3f)),
startY = size.height * 0.72f,
endY = size.height,
) )
onDrawWithContent { onDrawWithContent {
drawContent() drawContent()
drawRect( drawRect(
brush = highlightBrush, brush = topGloss,
topLeft = androidx.compose.ui.geometry.Offset.Zero,
size = size,
)
drawRect(
brush = bottomReflection,
topLeft = androidx.compose.ui.geometry.Offset.Zero, topLeft = androidx.compose.ui.geometry.Offset.Zero,
size = size, size = size,
) )

View File

@ -50,9 +50,6 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.blur.HazeColorEffect
import dev.chrisbanes.haze.blur.blurEffect
import dev.chrisbanes.haze.hazeEffect
import dev.krtirtho.spotube.NavigationItem import dev.krtirtho.spotube.NavigationItem
import dev.krtirtho.spotube.core.navigation.NavigationState import dev.krtirtho.spotube.core.navigation.NavigationState
import dev.krtirtho.spotube.core.navigation.Navigator import dev.krtirtho.spotube.core.navigation.Navigator
@ -85,21 +82,13 @@ fun AppSidebar(
var expanded by rememberSaveable { mutableStateOf(true) } var expanded by rememberSaveable { mutableStateOf(true) }
val width by animateDpAsState(targetValue = if (expanded) 236.dp else 86.dp) val width by animateDpAsState(targetValue = if (expanded) 236.dp else 86.dp)
val currentLibraryTab by libraryState.currentTab.collectAsState() val currentLibraryTab by libraryState.currentTab.collectAsState()
val surfaceTint = MaterialTheme.colorScheme.surface.copy(alpha = 0.85f)
val listState = rememberLazyListState() val listState = rememberLazyListState()
Column( Column(
modifier = modifier modifier = modifier
.fillMaxHeight() .fillMaxHeight()
.width(width) .width(width),
.hazeEffect(hazeState) {
blurEffect {
blurRadius = 20.dp
colorEffects = listOf(
HazeColorEffect.tint(surfaceTint)
)
}
},
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {