diff --git a/app/src/main/java/app/marlboroadvance/mpvex/preferences/LiquidUIPreferences.kt b/app/src/main/java/app/marlboroadvance/mpvex/preferences/LiquidUIPreferences.kt index cdf3ff71d..f075dc372 100644 --- a/app/src/main/java/app/marlboroadvance/mpvex/preferences/LiquidUIPreferences.kt +++ b/app/src/main/java/app/marlboroadvance/mpvex/preferences/LiquidUIPreferences.kt @@ -47,7 +47,9 @@ class LiquidUIPreferences(context: Context) { fun blurRadiusFlow(target: LiquidTarget): Flow = dataStore.data.map { it[floatPreferencesKey("${target.id}_blur")] ?: 0f } fun refractionHeightFlow(target: LiquidTarget): Flow = dataStore.data.map { it[floatPreferencesKey("${target.id}_height")] ?: 40f } fun refractionAmountFlow(target: LiquidTarget): Flow = dataStore.data.map { it[floatPreferencesKey("${target.id}_amount")] ?: 23f } - fun tintAlphaFlow(target: LiquidTarget): Flow = 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 = dataStore.data.map { it[floatPreferencesKey("${target.id}_alpha")] ?: 0.5f } fun chromaticAberrationFlow(target: LiquidTarget): Flow = dataStore.data.map { it[booleanPreferencesKey("${target.id}_chromatic")] ?: false } fun depthEffectFlow(target: LiquidTarget): Flow = dataStore.data.map { it[booleanPreferencesKey("${target.id}_depth")] ?: true } diff --git a/app/src/main/java/app/marlboroadvance/mpvex/ui/components/liquid/LiquidAlertDialog.kt b/app/src/main/java/app/marlboroadvance/mpvex/ui/components/liquid/LiquidAlertDialog.kt index 4c776e250..acc8fac5c 100644 --- a/app/src/main/java/app/marlboroadvance/mpvex/ui/components/liquid/LiquidAlertDialog.kt +++ b/app/src/main/java/app/marlboroadvance/mpvex/ui/components/liquid/LiquidAlertDialog.kt @@ -20,6 +20,12 @@ 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, @@ -27,10 +33,9 @@ fun LiquidAlertDialog( 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, diff --git a/app/src/main/java/app/marlboroadvance/mpvex/ui/components/liquid/LiquidComponents.kt b/app/src/main/java/app/marlboroadvance/mpvex/ui/components/liquid/LiquidComponents.kt index 2de4cbeaf..ac41d3156 100644 --- a/app/src/main/java/app/marlboroadvance/mpvex/ui/components/liquid/LiquidComponents.kt +++ b/app/src/main/java/app/marlboroadvance/mpvex/ui/components/liquid/LiquidComponents.kt @@ -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 { 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 { null } + @OptIn(ExperimentalFoundationApi::class) @Composable fun TransparentLiquidButton( diff --git a/app/src/main/java/app/marlboroadvance/mpvex/ui/components/liquid/LiquidGlassSurface.kt b/app/src/main/java/app/marlboroadvance/mpvex/ui/components/liquid/LiquidGlassSurface.kt index 15a48c3cb..83f8aa170 100644 --- a/app/src/main/java/app/marlboroadvance/mpvex/ui/components/liquid/LiquidGlassSurface.kt +++ b/app/src/main/java/app/marlboroadvance/mpvex/ui/components/liquid/LiquidGlassSurface.kt @@ -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 @@ -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() } }