diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/Surface.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/Surface.kt new file mode 100644 index 00000000..b3ad145f --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/Surface.kt @@ -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 . + */ + +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)) + } + } + } + } + } +} diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/Theme.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/Theme.kt index e195ffc2..9e27fc39 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/Theme.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/Theme.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.luminance +import androidx.compose.ui.graphics.lerp import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -219,19 +220,51 @@ fun rememberBaseUITheme(): BaseUITheme { val scheme = MaterialTheme.colorScheme 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 - 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) + // ------------------------------------------------------------------ + // Frutiger-Aero glass palette. + // + // 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 border = scheme.outlineVariant - val secondaryContainer = scheme.secondaryContainer - val secondaryContainerPressed = secondaryContainer.copy(alpha = if (isLight) 0.85f else 0.92f) - val secondaryHighlight = - if (isLight) Color.White.copy(alpha = 0.5f) else Color.White.copy(alpha = 0.08f) + fun glassBrush(top: Color, bottom: Color): Brush = + Brush.verticalGradient(listOf(top, bottom)) - 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 = BaseUITheme.InteractionState(defaultShape, defaultShape, defaultShape, defaultShape) 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 noShadowState = BaseUITheme.InteractionState(noShadow, noShadow, noShadow, noShadow) val defaultButtonPadding = BaseUITheme.InteractionState( - PaddingValues(horizontal = 20.dp, vertical = 10.dp), - PaddingValues(horizontal = 20.dp, vertical = 10.dp), - PaddingValues(horizontal = 20.dp, vertical = 10.dp), - PaddingValues(horizontal = 20.dp, vertical = 10.dp), + PaddingValues(horizontal = 22.dp, vertical = 10.dp), + PaddingValues(horizontal = 22.dp, vertical = 10.dp), + PaddingValues(horizontal = 22.dp, vertical = 10.dp), + PaddingValues(horizontal = 22.dp, vertical = 10.dp), ) val iconButtonPadding = BaseUITheme.InteractionState( - PaddingValues(8.dp), - PaddingValues(8.dp), - PaddingValues(8.dp), - PaddingValues(8.dp), + PaddingValues(9.dp), + PaddingValues(9.dp), + PaddingValues(9.dp), + PaddingValues(9.dp), ) + // --- Glass (outline) ------------------------------------------------- val outlineColors = BaseUITheme.InteractionState( normal = BaseUITheme.ButtonColors( - background = Brush.verticalGradient(listOf(containerLighter, containerDarker)), + background = glassBrush(glassTop, glassBottom), foreground = scheme.onSurface, - highlight = highlight, + highlight = gloss, ), hovered = BaseUITheme.ButtonColors( - background = Brush.verticalGradient(listOf(containerLighter, containerDarker)), + background = glassBrush(glassTop, glassBottom), foreground = scheme.onSurface, - highlight = highlight, + highlight = Color.White.copy(alpha = if (isLight) 0.9f else 0.26f), ), pressed = BaseUITheme.ButtonColors( - background = Brush.verticalGradient(listOf(containerPressed, containerPressed)), + background = glassBrush(glassPressed, glassPressed), foreground = scheme.onSurface, - highlight = highlight, + highlight = glossSoft, ), focused = BaseUITheme.ButtonColors( - background = Brush.verticalGradient(listOf(containerLighter, containerDarker)), + background = glassBrush(glassTop, glassBottom), foreground = scheme.onSurface, - highlight = highlight, + highlight = gloss, ), ) - val outlineShadow = BaseUITheme.InteractionState( + val glassShadow = BaseUITheme.InteractionState( normal = BaseUITheme.Shadow( - 6.dp, + 8.dp, true, - shadowColor.copy(alpha = 0.15f), - shadowColor.copy(alpha = 0.18f) + shadowColor.copy(alpha = 0.16f), + shadowColor.copy(alpha = 0.2f), ), hovered = BaseUITheme.Shadow( - 9.dp, + 14.dp, true, shadowColor.copy(alpha = 0.22f), - shadowColor.copy(alpha = 0.26f) + shadowColor.copy(alpha = 0.26f), ), pressed = BaseUITheme.Shadow( - 1.dp, + 2.dp, true, - shadowColor.copy(alpha = 0.08f), - shadowColor.copy(alpha = 0.1f) + shadowColor.copy(alpha = 0.1f), + shadowColor.copy(alpha = 0.12f), ), focused = BaseUITheme.Shadow( - 6.dp, + 8.dp, true, - shadowColor.copy(alpha = 0.15f), - shadowColor.copy(alpha = 0.18f) + shadowColor.copy(alpha = 0.16f), + shadowColor.copy(alpha = 0.2f), ), ) - val outlineBorder = 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), + val glassBorderState = BaseUITheme.InteractionState( + normal = BaseUITheme.Border(glassBorder, 1.dp), + hovered = BaseUITheme.Border(glassEdge, 1.dp), + pressed = BaseUITheme.Border(glassBorder.copy(alpha = 0.7f), 1.dp), + focused = BaseUITheme.Border(glassEdge, 1.dp), ) + // --- Aqua accent (primary) ------------------------------------------- + val primaryEdge = lerp(scheme.primary, Color.White, 0.5f) val primaryColors = BaseUITheme.InteractionState( - normal = BaseUITheme.ButtonColors( - background = Brush.verticalGradient(listOf(scheme.primary, scheme.primary)), - foreground = scheme.onPrimary, - highlight = Color.White.copy(alpha = 0.25f), - ), + normal = BaseUITheme.ButtonColors(aqua(scheme.primary), scheme.onPrimary, gloss), hovered = BaseUITheme.ButtonColors( - background = Brush.verticalGradient(listOf(scheme.primary, scheme.primary)), - foreground = scheme.onPrimary, - highlight = Color.White.copy(alpha = 0.25f), + aqua(scheme.primary), + scheme.onPrimary, + Color.White.copy(alpha = if (isLight) 0.9f else 0.3f), ), pressed = BaseUITheme.ButtonColors( - background = Brush.verticalGradient( - listOf( - scheme.primary.copy(alpha = 0.85f), - scheme.primary - ) + glassBrush( + lerp(scheme.primary, Color.Black, 0.16f), + lerp(scheme.primary, Color.Black, 0.24f), ), - 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), + scheme.onPrimary, + glossSoft, ), + focused = BaseUITheme.ButtonColors(aqua(scheme.primary), scheme.onPrimary, gloss), ) val primaryShadow = BaseUITheme.InteractionState( normal = BaseUITheme.Shadow( - 6.dp, + 10.dp, true, scheme.primary.copy(alpha = 0.35f), - scheme.primary.copy(alpha = 0.4f) + scheme.primary.copy(alpha = 0.42f), ), hovered = BaseUITheme.Shadow( - 9.dp, + 16.dp, true, scheme.primary.copy(alpha = 0.45f), - scheme.primary.copy(alpha = 0.5f) + scheme.primary.copy(alpha = 0.5f), ), pressed = BaseUITheme.Shadow( - 1.dp, + 3.dp, true, - scheme.primary.copy(alpha = 0.2f), - scheme.primary.copy(alpha = 0.25f) + scheme.primary.copy(alpha = 0.22f), + scheme.primary.copy(alpha = 0.28f), ), focused = BaseUITheme.Shadow( - 6.dp, + 10.dp, true, scheme.primary.copy(alpha = 0.35f), - scheme.primary.copy(alpha = 0.4f) + scheme.primary.copy(alpha = 0.42f), ), ) val primaryBorder = 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), + normal = BaseUITheme.Border(primaryEdge, 1.dp), + hovered = BaseUITheme.Border(primaryEdge, 1.dp), + pressed = BaseUITheme.Border(primaryEdge.copy(alpha = 0.8f), 1.dp), + focused = BaseUITheme.Border(primaryEdge, 1.dp), ) + // --- Secondary (soft aqua glass) ------------------------------------- + val secondaryBase = scheme.secondaryContainer val secondaryColors = BaseUITheme.InteractionState( normal = BaseUITheme.ButtonColors( - background = Brush.verticalGradient( - listOf( - secondaryContainer, - secondaryContainer.copy(alpha = if (secondaryContainer.luminance() > 0.5f) 0.92f else 1f) - ) - ), - foreground = scheme.onSecondaryContainer, - highlight = secondaryHighlight, + glassBrush(lerp(secondaryBase, Color.White, 0.5f), secondaryBase), + scheme.onSecondaryContainer, + gloss, ), hovered = BaseUITheme.ButtonColors( - background = Brush.verticalGradient( - listOf( - secondaryContainer, - secondaryContainer.copy(alpha = if (secondaryContainer.luminance() > 0.5f) 0.92f else 1f) - ) - ), - foreground = scheme.onSecondaryContainer, - highlight = secondaryHighlight, + glassBrush(lerp(secondaryBase, Color.White, 0.55f), secondaryBase), + scheme.onSecondaryContainer, + Color.White.copy(alpha = if (isLight) 0.85f else 0.24f), ), pressed = BaseUITheme.ButtonColors( - background = Brush.verticalGradient( - listOf( - secondaryContainerPressed, - secondaryContainerPressed - ) + glassBrush( + lerp(secondaryBase, Color.Black, 0.08f), + lerp(secondaryBase, Color.Black, 0.14f), ), - foreground = scheme.onSecondaryContainer, - highlight = secondaryHighlight, + scheme.onSecondaryContainer, + glossSoft, ), focused = BaseUITheme.ButtonColors( - background = Brush.verticalGradient( - listOf( - secondaryContainer, - secondaryContainer.copy(alpha = if (secondaryContainer.luminance() > 0.5f) 0.92f else 1f) - ) - ), - foreground = scheme.onSecondaryContainer, - highlight = secondaryHighlight, + glassBrush(lerp(secondaryBase, Color.White, 0.5f), secondaryBase), + scheme.onSecondaryContainer, + gloss, ), ) val secondaryShadow = BaseUITheme.InteractionState( normal = BaseUITheme.Shadow( - 6.dp, + 8.dp, true, - shadowColor.copy(alpha = 0.15f), - shadowColor.copy(alpha = 0.18f) + secondaryBase.copy(alpha = 0.4f), + secondaryBase.copy(alpha = 0.5f), ), hovered = BaseUITheme.Shadow( - 9.dp, + 14.dp, true, - shadowColor.copy(alpha = 0.22f), - shadowColor.copy(alpha = 0.26f) + secondaryBase.copy(alpha = 0.5f), + secondaryBase.copy(alpha = 0.6f), ), pressed = BaseUITheme.Shadow( - 1.dp, + 2.dp, true, - shadowColor.copy(alpha = 0.08f), - shadowColor.copy(alpha = 0.1f) + secondaryBase.copy(alpha = 0.3f), + secondaryBase.copy(alpha = 0.35f), ), focused = BaseUITheme.Shadow( - 6.dp, + 8.dp, true, - shadowColor.copy(alpha = 0.15f), - shadowColor.copy(alpha = 0.18f) + secondaryBase.copy(alpha = 0.4f), + secondaryBase.copy(alpha = 0.5f), ), ) val secondaryBorder = BaseUITheme.InteractionState( - normal = BaseUITheme.Border(secondaryContainer.copy(alpha = 0.5f), 0.5.dp), - hovered = BaseUITheme.Border(secondaryContainer.copy(alpha = 0.5f), 0.5.dp), - pressed = BaseUITheme.Border(secondaryContainer.copy(alpha = 0.5f), 0.5.dp), - focused = BaseUITheme.Border(secondaryContainer.copy(alpha = 0.5f), 0.5.dp), + normal = BaseUITheme.Border(lerp(secondaryBase, Color.White, 0.5f), 1.dp), + hovered = BaseUITheme.Border(lerp(secondaryBase, Color.White, 0.6f), 1.dp), + pressed = BaseUITheme.Border(lerp(secondaryBase, Color.White, 0.4f), 1.dp), + focused = BaseUITheme.Border(lerp(secondaryBase, Color.White, 0.5f), 1.dp), ) + // --- Ghost ----------------------------------------------------------- val ghostColors = BaseUITheme.InteractionState( normal = BaseUITheme.ButtonColors( - background = Brush.verticalGradient(listOf(Color.Transparent, Color.Transparent)), + background = glassBrush(Color.Transparent, Color.Transparent), foreground = scheme.onSurface, highlight = Color.Transparent, ), hovered = BaseUITheme.ButtonColors( - background = Brush.verticalGradient( - listOf( - containerPressed.copy(alpha = 0.5f), - containerPressed.copy(alpha = 0.5f) - ) + background = glassBrush( + lerp(scheme.surfaceContainerHigh, Color.White, 0.1f), + scheme.surfaceContainer, ), foreground = scheme.onSurface, - highlight = Color.Transparent, + highlight = glossSoft, ), pressed = BaseUITheme.ButtonColors( - background = Brush.verticalGradient(listOf(containerPressed, containerPressed)), + background = glassBrush(glassPressed, glassPressed), foreground = scheme.onSurface, highlight = Color.Transparent, ), focused = BaseUITheme.ButtonColors( - background = Brush.verticalGradient(listOf(Color.Transparent, Color.Transparent)), + background = glassBrush(Color.Transparent, Color.Transparent), foreground = scheme.onSurface, highlight = Color.Transparent, ), @@ -472,8 +482,8 @@ fun rememberBaseUITheme(): BaseUITheme { val outlineStyle = BaseUITheme.ButtonStyle( outlineColors, defaultShapeState, - outlineShadow, - outlineBorder, + glassShadow, + glassBorderState, defaultButtonPadding ) val primaryStyle = BaseUITheme.ButtonStyle( @@ -501,8 +511,8 @@ fun rememberBaseUITheme(): BaseUITheme { val outlineIconStyle = BaseUITheme.ButtonStyle( outlineColors, defaultShapeState, - outlineShadow, - outlineBorder, + glassShadow, + glassBorderState, iconButtonPadding ) val primaryIconStyle = BaseUITheme.ButtonStyle( @@ -534,20 +544,20 @@ fun rememberBaseUITheme(): BaseUITheme { focused = scheme.onSurface, ) - val checkBoxShape = RoundedCornerShape(6.dp) + val checkBoxShape = RoundedCornerShape(7.dp) val checkBoxShapeState = BaseUITheme.InteractionState(checkBoxShape, checkBoxShape, checkBoxShape, checkBoxShape) val noPadding = 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 = BaseUITheme.InteractionState(chipTabShape, chipTabShape, chipTabShape, chipTabShape) val chipTabPadding = BaseUITheme.InteractionState( - PaddingValues(horizontal = 14.dp, vertical = 4.dp), - PaddingValues(horizontal = 14.dp, vertical = 4.dp), - PaddingValues(horizontal = 14.dp, vertical = 4.dp), - PaddingValues(horizontal = 14.dp, vertical = 4.dp), + PaddingValues(horizontal = 16.dp, vertical = 6.dp), + PaddingValues(horizontal = 16.dp, vertical = 6.dp), + PaddingValues(horizontal = 16.dp, vertical = 6.dp), + PaddingValues(horizontal = 16.dp, vertical = 6.dp), ) val toggleShape = RoundedCornerShape(50) @@ -575,16 +585,16 @@ fun rememberBaseUITheme(): BaseUITheme { ), textField = TextFieldTheme( background = BaseUITheme.InteractionState( - normal = Brush.verticalGradient(listOf(containerLighter, containerDarker)), - hovered = Brush.verticalGradient(listOf(containerLighter, containerDarker)), - pressed = Brush.verticalGradient(listOf(containerLighter, containerDarker)), - focused = Brush.verticalGradient(listOf(containerLighter, containerDarker)), + normal = glassBrush(glassTop, glassBottom), + hovered = glassBrush(glassTop, glassBottom), + pressed = glassBrush(glassPressed, glassPressed), + focused = glassBrush(glassTop, glassBottom), ), highlight = BaseUITheme.InteractionState( - normal = highlight, - hovered = highlight, - pressed = highlight, - focused = highlight, + normal = gloss, + hovered = gloss, + pressed = glossSoft, + focused = Color.White.copy(alpha = if (isLight) 0.85f else 0.22f), ), shape = BaseUITheme.InteractionState( normal = defaultShape, @@ -593,53 +603,48 @@ fun rememberBaseUITheme(): BaseUITheme { focused = defaultShape, ), border = BaseUITheme.InteractionState( - normal = BaseUITheme.Border(border.copy(alpha = 0.85f), 0.5.dp), - hovered = BaseUITheme.Border(border.copy(alpha = 0.85f), 0.5.dp), - pressed = BaseUITheme.Border(border, 0.5.dp), - focused = BaseUITheme.Border(scheme.primary, 0.5.dp), + normal = BaseUITheme.Border(glassBorder, 1.dp), + hovered = BaseUITheme.Border(glassEdge, 1.dp), + pressed = BaseUITheme.Border(glassBorder.copy(alpha = 0.7f), 1.dp), + focused = BaseUITheme.Border(primaryEdge, 1.5.dp), ), shadow = BaseUITheme.InteractionState( normal = BaseUITheme.Shadow( - 9.dp, + 8.dp, true, - shadowColor.copy(alpha = 0.22f), - 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.14f), shadowColor.copy(alpha = 0.18f) ), - focused = BaseUITheme.Shadow( - 9.dp, + hovered = BaseUITheme.Shadow( + 10.dp, true, - scheme.primary.copy(alpha = 0.2f), - scheme.primary.copy(alpha = 0.25f) + shadowColor.copy(alpha = 0.18f), + 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, cursor = SolidColor(scheme.primary), foreground = textFieldForeground, ), autoCompleteMenuTheme = AutoCompleteMenuTheme( - background = Brush.verticalGradient( - listOf( - scheme.surfaceContainerHigh, - scheme.surfaceContainer - ) - ), - shape = RoundedCornerShape(12.dp), - border = BaseUITheme.Border(border, 0.5.dp), + background = glassBrush(glassTop, glassBottom), + shape = RoundedCornerShape(16.dp), + border = BaseUITheme.Border(glassBorder, 1.dp), padding = PaddingValues(0.dp), - shadowElevation = 8.dp, + shadowElevation = 16.dp, ), slider = SliderTheme( trackActiveColor = BaseUITheme.InteractionState( @@ -649,10 +654,10 @@ fun rememberBaseUITheme(): BaseUITheme { focused = scheme.primary, ), trackInactiveColor = BaseUITheme.InteractionState( - normal = border.copy(alpha = 0.6f), - hovered = border.copy(alpha = 0.6f), - pressed = border.copy(alpha = 0.6f), - focused = border.copy(alpha = 0.6f), + normal = glassBorder, + hovered = glassBorder, + pressed = glassBorder, + focused = glassBorder, ), thumbColor = BaseUITheme.InteractionState( normal = scheme.primary, @@ -664,94 +669,44 @@ fun rememberBaseUITheme(): BaseUITheme { normal = BaseUITheme.Shadow( 5.dp, 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( 8.dp, true, - scheme.primary.copy(alpha = 0.4f), + scheme.primary.copy(alpha = 0.45f), scheme.primary.copy(alpha = 0.5f) ), pressed = BaseUITheme.Shadow( 2.dp, 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( 5.dp, 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), ), checkBox = CheckBoxTheme( selected = BaseUITheme.ButtonStyle( colors = primaryColors, shape = checkBoxShapeState, - shadow = BaseUITheme.InteractionState( - 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) - ), - ), + shadow = primaryShadow, border = primaryBorder, padding = noPadding, ), unselected = BaseUITheme.ButtonStyle( colors = outlineColors, shape = checkBoxShapeState, - shadow = BaseUITheme.InteractionState( - normal = BaseUITheme.Shadow( - 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, + shadow = glassShadow, + border = glassBorderState, padding = noPadding, ), checkmarkColor = scheme.onPrimary, @@ -767,131 +722,24 @@ fun rememberBaseUITheme(): BaseUITheme { unselected = BaseUITheme.ButtonStyle( colors = outlineColors, shape = chipTabShapeState, - shadow = outlineShadow, - border = outlineBorder, + shadow = glassShadow, + border = glassBorderState, padding = chipTabPadding, ), ), toggle = BaseUITheme.ToggleTheme( checked = BaseUITheme.ButtonStyle( - colors = BaseUITheme.InteractionState( - 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), - ), - ), + colors = primaryColors, shape = toggleShapeState, - shadow = BaseUITheme.InteractionState( - normal = BaseUITheme.Shadow( - 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), - ), + shadow = primaryShadow, + border = primaryBorder, padding = togglePadding, ), unchecked = BaseUITheme.ButtonStyle( - colors = BaseUITheme.InteractionState( - 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, - ), - ), + colors = outlineColors, shape = toggleShapeState, - shadow = BaseUITheme.InteractionState( - normal = BaseUITheme.Shadow( - 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), - ), + shadow = glassShadow, + border = glassBorderState, padding = togglePadding, ), thumb = BaseUITheme.InteractionState( @@ -928,61 +776,62 @@ fun rememberBaseUITheme(): BaseUITheme { ), ), card = BaseUITheme.CardTheme( - background = Brush.verticalGradient(listOf(containerLighter, containerDarker)), - shape = RoundedCornerShape(16.dp), - border = BaseUITheme.Border(border, 0.5.dp), + background = glassBrush(glassTop, glassBottom), + shape = RoundedCornerShape(20.dp), + border = BaseUITheme.Border(glassBorder, 1.dp), padding = PaddingValues(0.dp), shadow = BaseUITheme.Shadow( - 6.dp, + 12.dp, true, - shadowColor.copy(alpha = 0.15f), - shadowColor.copy(alpha = 0.18f) + shadowColor.copy(alpha = 0.16f), + shadowColor.copy(alpha = 0.2f) ), ), listRowTile = BaseUITheme.ListRowTheme( background = BaseUITheme.AdvancedInteractionState( - normal = Brush.verticalGradient(listOf(Color.Transparent, Color.Transparent)), - hovered = Brush.verticalGradient(listOf(containerLighter, containerDarker)), - pressed = Brush.verticalGradient(listOf(containerPressed, containerPressed)), - focused = Brush.verticalGradient(listOf(containerLighter, containerDarker)), - selected = Brush.verticalGradient( - listOf( - scheme.primaryContainer.copy(alpha = 0.3f), - scheme.primaryContainer.copy(alpha = 0.3f) - ) + normal = glassBrush(Color.Transparent, Color.Transparent), + hovered = glassBrush(glassTop, glassBottom), + pressed = glassBrush(glassPressed, glassPressed), + focused = glassBrush(glassTop, glassBottom), + selected = glassBrush( + lerp(scheme.primaryContainer, Color.White, 0.4f), + scheme.primaryContainer, ), - disabled = Brush.verticalGradient( - listOf( - containerLighter.copy(alpha = 0.5f), - containerDarker.copy(alpha = 0.5f) - ) + disabled = glassBrush( + glassTop.copy(alpha = 0.5f), + glassBottom.copy(alpha = 0.5f), ), ), shape = BaseUITheme.AdvancedInteractionState( - normal = RoundedCornerShape(8.dp), - hovered = RoundedCornerShape(8.dp), - pressed = RoundedCornerShape(8.dp), - focused = RoundedCornerShape(8.dp), - selected = RoundedCornerShape(8.dp), - disabled = RoundedCornerShape(8.dp), + normal = RoundedCornerShape(10.dp), + hovered = RoundedCornerShape(10.dp), + pressed = RoundedCornerShape(10.dp), + focused = RoundedCornerShape(10.dp), + selected = RoundedCornerShape(10.dp), + disabled = RoundedCornerShape(10.dp), ), border = BaseUITheme.AdvancedInteractionState( 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), - focused = BaseUITheme.Border(Color.Transparent, 0.dp), + focused = BaseUITheme.Border(glassBorder, 1.dp), selected = BaseUITheme.Border(Color.Transparent, 0.dp), disabled = BaseUITheme.Border(Color.Transparent, 0.dp), ), shadow = BaseUITheme.AdvancedInteractionState( - normal = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent), - hovered = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent), - pressed = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent), - focused = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent), - selected = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent), - disabled = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent), + normal = noShadow, + hovered = BaseUITheme.Shadow( + 6.dp, + true, + shadowColor.copy(alpha = 0.12f), + 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( normal = TextStyle.Default, hovered = TextStyle.Default, @@ -1001,17 +850,12 @@ fun rememberBaseUITheme(): BaseUITheme { ), ), dialog = BaseUITheme.DialogTheme( - background = Brush.verticalGradient( - listOf( - scheme.surface, - scheme.surfaceContainerHigh, - ) - ), + background = glassBrush(glassTop, glassBottom), scrim = Color.Black.copy(alpha = 0.42f), - shape = RoundedCornerShape(16.dp), - border = BaseUITheme.Border(border, 0.5.dp), + shape = RoundedCornerShape(24.dp), + border = BaseUITheme.Border(glassEdge, 1.dp), shadow = BaseUITheme.Shadow( - 24.dp, + 28.dp, true, shadowColor.copy(alpha = 0.3f), shadowColor.copy(alpha = 0.4f) @@ -1019,27 +863,20 @@ fun rememberBaseUITheme(): BaseUITheme { padding = PaddingValues(24.dp), ), dropdownMenu = BaseUITheme.DropdownMenuTheme( - background = Brush.verticalGradient( - listOf( - scheme.surfaceContainerHigh, - scheme.surfaceContainer, - ) - ), - shape = RoundedCornerShape(12.dp), - border = BaseUITheme.Border(border, 0.5.dp), + background = glassBrush(glassTop, glassBottom), + shape = RoundedCornerShape(16.dp), + border = BaseUITheme.Border(glassBorder, 1.dp), shadow = BaseUITheme.Shadow( - 12.dp, + 16.dp, true, - shadowColor.copy(alpha = 0.2f), - shadowColor.copy(alpha = 0.25f) - ), - itemHoverBackground = Brush.verticalGradient( - listOf(containerLighter, containerDarker) + shadowColor.copy(alpha = 0.22f), + shadowColor.copy(alpha = 0.28f) ), + itemHoverBackground = glassBrush(glassTop, glassBottom), itemForeground = scheme.onSurface, - itemHighlight = highlight, - padding = PaddingValues(4.dp), - itemPadding = PaddingValues(horizontal = 14.dp, vertical = 10.dp), + itemHighlight = gloss, + padding = PaddingValues(6.dp), + itemPadding = PaddingValues(horizontal = 14.dp, vertical = 11.dp), ), ) } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/Utils.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/Utils.kt index 19e986eb..96d44fd5 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/Utils.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/Utils.kt @@ -24,15 +24,27 @@ import androidx.compose.ui.unit.dp fun Modifier.highlight(color: Color): Modifier = drawWithCache { - val highlightBrush = Brush.verticalGradient( - colors = listOf(color, Color.Transparent), + // Frutiger-Aero glass: a bright specular sheen across the top half plus a + // 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, - 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 { drawContent() drawRect( - brush = highlightBrush, + brush = topGloss, + topLeft = androidx.compose.ui.geometry.Offset.Zero, + size = size, + ) + drawRect( + brush = bottomReflection, topLeft = androidx.compose.ui.geometry.Offset.Zero, size = size, ) diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppSidebar.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppSidebar.kt index ce632b19..dd61c618 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppSidebar.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppSidebar.kt @@ -50,9 +50,6 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp 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.core.navigation.NavigationState import dev.krtirtho.spotube.core.navigation.Navigator @@ -85,21 +82,13 @@ fun AppSidebar( var expanded by rememberSaveable { mutableStateOf(true) } val width by animateDpAsState(targetValue = if (expanded) 236.dp else 86.dp) val currentLibraryTab by libraryState.currentTab.collectAsState() - val surfaceTint = MaterialTheme.colorScheme.surface.copy(alpha = 0.85f) val listState = rememberLazyListState() + Column( modifier = modifier .fillMaxHeight() - .width(width) - .hazeEffect(hazeState) { - blurEffect { - blurRadius = 20.dp - colorEffects = listOf( - HazeColorEffect.tint(surfaceTint) - ) - } - }, + .width(width), horizontalAlignment = Alignment.CenterHorizontally ) { Column(modifier = Modifier.weight(1f)) {