Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ class LiquidUIPreferences(context: Context) {
fun blurRadiusFlow(target: LiquidTarget): Flow<Float> = dataStore.data.map { it[floatPreferencesKey("${target.id}_blur")] ?: 0f }
fun refractionHeightFlow(target: LiquidTarget): Flow<Float> = dataStore.data.map { it[floatPreferencesKey("${target.id}_height")] ?: 40f }
fun refractionAmountFlow(target: LiquidTarget): Flow<Float> = dataStore.data.map { it[floatPreferencesKey("${target.id}_amount")] ?: 23f }
fun tintAlphaFlow(target: LiquidTarget): Flow<Float> = dataStore.data.map { it[floatPreferencesKey("${target.id}_alpha")] ?: 0.15f }
// CHANGED: default raised 0.15f → 0.5f. Old value let backdrop text bleed through navigation/dialog
// glass; 0.5f is the Backdrop docs' recommended balance of "glass look" vs. text readability.
fun tintAlphaFlow(target: LiquidTarget): Flow<Float> = dataStore.data.map { it[floatPreferencesKey("${target.id}_alpha")] ?: 0.5f }

fun chromaticAberrationFlow(target: LiquidTarget): Flow<Boolean> = dataStore.data.map { it[booleanPreferencesKey("${target.id}_chromatic")] ?: false }
fun depthEffectFlow(target: LiquidTarget): Flow<Boolean> = dataStore.data.map { it[booleanPreferencesKey("${target.id}_depth")] ?: true }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,22 @@ fun LiquidAlertDialog(
) {
val backdrop = LocalLiquidBackdrop.current

// CHANGED: dialog scrim was previously `Color.White.copy(alpha = 0.15f)`. That flat 15%-opaque white was the
// root cause of the dialog text bleeding into menu/background text — and it didn't adapt to dark theme.
// New scrim: theme color (`surfaceContainerHigh`) at 0.85 alpha. Adapts to light/dark automatically and is
// opaque enough that text behind the dialog can't compete with text inside it.
val scrimColor = MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = 0.85f)

AlertDialog(
onDismissRequest = onDismissRequest,
confirmButton = confirmButton,
dismissButton = dismissButton,
icon = icon,
title = title,
text = text,
// The Magic Sauce: If Liquid is enabled, it strips the grey background and makes it a transparent glass pane!
modifier = if (backdrop != null) {
modifier.background(
color = Color.White.copy(alpha = 0.15f),
color = scrimColor,
shape = MaterialTheme.shapes.extraLarge
)
} else modifier,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,17 @@ import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.dp
import com.kyant.backdrop.Backdrop
import app.marlboroadvance.mpvex.preferences.LiquidTarget
import app.marlboroadvance.mpvex.preferences.LiquidUIPreferences

// Broadcasts the glass camera to any button that wants it!
val LocalLiquidBackdrop = androidx.compose.runtime.staticCompositionLocalOf<com.kyant.backdrop.Backdrop?> { null }

// ADDED: shared LiquidUIPreferences CompositionLocal.
// Why: previously every LiquidGlassSurface built its own DataStore wrapper via remember{ LiquidUIPreferences(context) }.
// With many glass surfaces on screen (nav + buttons + cards) that meant N wrappers all observing the same DataStore.
// MainActivity can now provide one instance for the whole tree; null fallback keeps old behavior working.
val LocalLiquidPreferences = androidx.compose.runtime.staticCompositionLocalOf<LiquidUIPreferences?> { null }

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun TransparentLiquidButton(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp

import com.kyant.backdrop.Backdrop
Expand All @@ -28,49 +29,75 @@ import app.marlboroadvance.mpvex.preferences.LiquidTarget
@Composable
fun LiquidGlassSurface(
backdrop: Backdrop,
target: LiquidTarget = LiquidTarget.NAV, // Targets the Nav bar by default
target: LiquidTarget = LiquidTarget.NAV,
modifier: Modifier = Modifier,
shape: Shape = RoundedCornerShape(24.dp),
defaultTintColor: Color = Color.White,
shape: Shape = RoundedCornerShape(24.dp),
defaultTintColor: Color = Color.White,
content: @Composable () -> Unit
) {
val context = LocalContext.current
val liquidPrefs = remember { LiquidUIPreferences(context) }

// SAFE STATE COLLECTIONS: Only updates when the target actually changes
val blurRadius by remember(target) { liquidPrefs.blurRadiusFlow(target) }.collectAsState(initial = 0f)
val refractionHeight by remember(target) { liquidPrefs.refractionHeightFlow(target) }.collectAsState(initial = 40f)
val refractionAmount by remember(target) { liquidPrefs.refractionAmountFlow(target) }.collectAsState(initial = 23f)
val chromaticAberration by remember(target) { liquidPrefs.chromaticAberrationFlow(target) }.collectAsState(initial = false)
val depthEffect by remember(target) { liquidPrefs.depthEffectFlow(target) }.collectAsState(initial = true)
val vibrancyEnabled by remember(target) { liquidPrefs.vibrancyEnabledFlow(target) }.collectAsState(initial = true)
val tintAlpha by remember(target) { liquidPrefs.tintAlphaFlow(target) }.collectAsState(initial = 0.15f)
// CHANGED: prefer shared LocalLiquidPreferences if MainActivity provided one (perf: one DataStore wrapper for whole tree).
// Fallback to a remembered instance keyed on applicationContext so it survives configuration changes
// and is not rebuilt on every recomposition.
val sharedPrefs = LocalLiquidPreferences.current
val liquidPrefs = sharedPrefs ?: remember(context.applicationContext) {
LiquidUIPreferences(context.applicationContext)
}

val blurRadius by remember(liquidPrefs, target) { liquidPrefs.blurRadiusFlow(target) }.collectAsState(initial = 0f)
val refractionHeight by remember(liquidPrefs, target) { liquidPrefs.refractionHeightFlow(target) }.collectAsState(initial = 40f)
val refractionAmount by remember(liquidPrefs, target) { liquidPrefs.refractionAmountFlow(target) }.collectAsState(initial = 23f)
val chromaticAberration by remember(liquidPrefs, target) { liquidPrefs.chromaticAberrationFlow(target) }.collectAsState(initial = false)
val depthEffect by remember(liquidPrefs, target) { liquidPrefs.depthEffectFlow(target) }.collectAsState(initial = true)
val vibrancyEnabled by remember(liquidPrefs, target) { liquidPrefs.vibrancyEnabledFlow(target) }.collectAsState(initial = true)
// CHANGED: initial value 0.15f → 0.5f to match the new DataStore default; prevents a flash of see-through glass
// (where backdrop text would bleed through) on first frame before the flow emits.
val tintAlpha by remember(liquidPrefs, target) { liquidPrefs.tintAlphaFlow(target) }.collectAsState(initial = 0.5f)

val density = LocalDensity.current
// ADDED (perf): hoist dp→px conversions out of the per-frame `effects` draw lambda.
// Previously `refractionHeight.dp.toPx()` etc. ran on every frame inside drawBackdrop's effects block.
// Now they only recompute when density or the underlying pref value actually changes.
val blurPx = remember(density, blurRadius) { with(density) { blurRadius.dp.toPx() } }
val refractionHeightPx = remember(density, refractionHeight) { with(density) { refractionHeight.dp.toPx() } }
val refractionAmountPx = remember(density, refractionAmount) { with(density) { refractionAmount.dp.toPx() } }
// ADDED: clamp tint alpha to [0,1] defensively; a stray out-of-range pref value would otherwise crash drawRect.
val safeTintAlpha = tintAlpha.coerceIn(0f, 1f)

if (Build.VERSION.SDK_INT >= 33) {
if (Build.VERSION.SDK_INT >= 33) {
Box(
modifier = modifier
.drawBackdrop(
backdrop = backdrop,
shape = { shape },
effects = {
// CHANGED: enforce Backdrop docs' required effect order — color filter (vibrancy) → blur → lens.
// ADDED (perf): skip lens() entirely when refraction params are 0 — the lens shader is
// the most expensive effect in this pipeline, and running it with zero amount is wasted GPU work.
if (vibrancyEnabled) vibrancy()
if (blurRadius > 0f) blur(blurRadius.dp.toPx())

lens(
refractionHeight = refractionHeight.dp.toPx(),
refractionAmount = refractionAmount.dp.toPx(),
depthEffect = depthEffect,
chromaticAberration = chromaticAberration
)
if (blurPx > 0f) blur(blurPx)
if (refractionHeightPx > 0f && refractionAmountPx > 0f) {
lens(
refractionHeight = refractionHeightPx,
refractionAmount = refractionAmountPx,
depthEffect = depthEffect,
chromaticAberration = chromaticAberration
)
}
},
onDrawSurface = { drawRect(defaultTintColor.copy(alpha = tintAlpha)) }
onDrawSurface = { drawRect(defaultTintColor.copy(alpha = safeTintAlpha)) }
)
) { content() }
} else {
} else {
// CHANGED: pre-API-33 fallback alpha is now coerced to ≥ 0.7f.
// Reason: this branch can't run blur/lens shaders (RuntimeShader is API 33+), so the tint is the ONLY
// thing separating foreground text from backdrop content. 0.5f looked transparent here even though it
// works fine on API 33+ where blur/lens further obscure the backdrop.
val fallbackAlpha = safeTintAlpha.coerceAtLeast(0.7f)
Box(
modifier = modifier
.background(defaultTintColor.copy(alpha = tintAlpha), shape)
.border(1.dp, Color.White.copy(alpha = 0.2f), shape)
.background(defaultTintColor.copy(alpha = fallbackAlpha), shape)
.border(1.dp, Color.White.copy(alpha = 0.2f), shape)
.clip(shape)
) { content() }
}
Expand Down
Loading