From ac0b0b65aac3da13d6a2f76fbf9fc1270b1376ee Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Mon, 17 Aug 2026 07:49:54 -0400 Subject: [PATCH] feat(ui): optional frosted liquid-glass on app-bar action chrome MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CircularIconButton gains an optional hazeState — when supplied it frosts whatever content scrolls beneath the bar (clip precedes hazeEffect so the blur is bounded to the circle); flat translucent fill when null. AppBarDefaults.UpNavigation/Close/Share and AppBarWithTitle thread the optional hazeState. Fully backward-compatible (default null = current look). --- .../ui/components/CircularIconButton.kt | 31 +++++++++++++++++-- .../com/getcode/ui/components/TitleBar.kt | 15 ++++++--- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/ui/components/src/main/kotlin/com/getcode/ui/components/CircularIconButton.kt b/ui/components/src/main/kotlin/com/getcode/ui/components/CircularIconButton.kt index f80628d5a5..4109437562 100644 --- a/ui/components/src/main/kotlin/com/getcode/ui/components/CircularIconButton.kt +++ b/ui/components/src/main/kotlin/com/getcode/ui/components/CircularIconButton.kt @@ -1,6 +1,7 @@ package com.getcode.ui.components import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape @@ -9,12 +10,18 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.lerp import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.getcode.ui.components.AppBarDefaults.IconSize import androidx.compose.foundation.clickable import com.getcode.theme.CodeTheme +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.blur.HazeBlurStyle +import dev.chrisbanes.haze.blur.HazeColorEffect +import dev.chrisbanes.haze.blur.blurEffect +import dev.chrisbanes.haze.hazeEffect private val ButtonSize: Dp @Composable get() = CodeTheme.dimens.staticGrid.x8 @@ -26,15 +33,35 @@ fun CircularIconButton( modifier: Modifier = Modifier, imageSize: Dp = IconSize, buttonSize: Dp = ButtonSize, + hazeState: HazeState? = null, onClick: () -> Unit, testTag: String? = null, content: @Composable (Dp) -> Unit, ) { + // When a HazeState is supplied the button frosts whatever content scrolls beneath the app bar + // (iOS "liquid glass"), matching the v2 nav pill; `clip` precedes `hazeEffect` so the blur is + // bounded to the circle. Falls back to the flat translucent fill when no HazeState is supplied. + val fill = if (hazeState != null) { + val glassTint = lerp(CodeTheme.colors.background, Color.White, 0.18f) + val liquidGlass = HazeBlurStyle( + blurRadius = CodeTheme.dimens.grid.x4, + backgroundColor = CodeTheme.colors.background, + colorEffect = HazeColorEffect.tint(glassTint.copy(alpha = 0.72f)), + ) + Modifier + .clip(CircleShape) + .hazeEffect(hazeState) { blurEffect { style = liquidGlass } } + .border(CodeTheme.dimens.border, Color.White.copy(alpha = 0.08f), CircleShape) + } else { + Modifier + .background(ButtonBackground, CircleShape) + .clip(CircleShape) + } + Box( modifier = modifier .size(buttonSize) - .background(ButtonBackground, CircleShape) - .clip(CircleShape) + .then(fill) .clickable { onClick() } .then(if (testTag != null) Modifier.testTag(testTag) else Modifier), contentAlignment = Alignment.Center, diff --git a/ui/components/src/main/kotlin/com/getcode/ui/components/TitleBar.kt b/ui/components/src/main/kotlin/com/getcode/ui/components/TitleBar.kt index 87eaff6969..c12ae2269f 100644 --- a/ui/components/src/main/kotlin/com/getcode/ui/components/TitleBar.kt +++ b/ui/components/src/main/kotlin/com/getcode/ui/components/TitleBar.kt @@ -32,6 +32,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.getcode.navigation.core.LocalCodeNavigator +import dev.chrisbanes.haze.HazeState import com.getcode.navigation.flow.FlowDismissStyle import com.getcode.navigation.flow.LocalFlowDismissStyle import com.getcode.navigation.scenes.LocalSheetNavigator @@ -51,9 +52,10 @@ object AppBarDefaults { internal val IconSize = 20.dp @Composable - fun UpNavigation(modifier: Modifier = Modifier, onClick: () -> Unit) { + fun UpNavigation(modifier: Modifier = Modifier, hazeState: HazeState? = null, onClick: () -> Unit) { CircularIconButton( modifier = modifier, + hazeState = hazeState, onClick = onClick, testTag = "action_back" ) { size -> @@ -67,9 +69,10 @@ object AppBarDefaults { } @Composable - fun Close(modifier: Modifier = Modifier, onClick: () -> Unit) { + fun Close(modifier: Modifier = Modifier, hazeState: HazeState? = null, onClick: () -> Unit) { CircularIconButton( modifier = modifier, + hazeState = hazeState, onClick = onClick, testTag = "action_close" ) { size -> @@ -83,9 +86,10 @@ object AppBarDefaults { } @Composable - fun Share(modifier: Modifier = Modifier, onClick: () -> Unit) { + fun Share(modifier: Modifier = Modifier, hazeState: HazeState? = null, onClick: () -> Unit) { CircularIconButton( modifier = modifier, + hazeState = hazeState, onClick = onClick, testTag = "action_share" ) { size -> @@ -190,6 +194,7 @@ fun AppBarWithTitle( titleAlignment: Alignment.Horizontal = Alignment.Start, contentPadding: PaddingValues = AppBarDefaults.ContentPadding, onBackIconClicked: (() -> Unit)? = null, + hazeState: HazeState? = null, endContent: @Composable RowScope.() -> Unit = { }, ) { val navigator = LocalCodeNavigator.current @@ -215,7 +220,7 @@ fun AppBarWithTitle( contentPadding = contentPadding, leftIcon = { if (showBack && !showClose) { - AppBarDefaults.UpNavigation { onBackIconClicked() } + AppBarDefaults.UpNavigation(hazeState = hazeState) { onBackIconClicked() } } }, titleRegion = { @@ -229,7 +234,7 @@ fun AppBarWithTitle( rightContents = { endContent() if (showClose) { - AppBarDefaults.Close { onBackIconClicked() } + AppBarDefaults.Close(hazeState = hazeState) { onBackIconClicked() } } }, )