____ _ _______ __
/ __ \ | /| / / __/ |/ /
/ /_/ / |/ |/ / _// /
\____/|__/|__/___/_||_/
owenclave / app/src/main/java/io/nekohasekai/sagernet/ui/compose/components/ExpressiveDialog.kt
@file:OptIn(androidx.compose.material3.ExperimentalMaterial3ExpressiveApi::class)
package io.nekohasekai.sagernet.ui.compose.components
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties
/**
* A distinctly M3E dialog rendered as a same-window [Popup] so the root content
* behind it can be blurred (via [LocalScrimController]). Shows a dark scrim that
* also covers the system bars, plus a bouncy scale-in surface.
*/
@Composable
fun ExpressiveDialog(
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable ColumnScope.() -> Unit,
) {
val scrim = LocalScrimController.current
// Register so the root content blurs while this dialog is open.
DisposableEffect(Unit) {
scrim.push()
onDispose { scrim.pop() }
}
val appear by animateFloatAsState(
targetValue = 1f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessMedium,
),
label = "dialogAppear",
)
Popup(
alignment = Alignment.Center,
onDismissRequest = onDismissRequest,
properties = PopupProperties(focusable = true),
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
// Dark scrim over everything (including behind the status bar).
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.45f))
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
) { onDismissRequest() },
)
// The dialog surface, centered, scale + fade in.
Surface(
modifier = modifier
.padding(24.dp)
.widthIn(min = 280.dp, max = 400.dp)
.graphicsLayer {
alpha = appear
scaleX = 0.9f + 0.1f * appear
scaleY = 0.9f + 0.1f * appear
},
shape = MaterialTheme.shapes.extraLarge,
color = MaterialTheme.colorScheme.surfaceContainerHigh,
tonalElevation = 6.dp,
shadowElevation = 12.dp,
) {
Column(
modifier = Modifier.padding(24.dp),
content = content,
)
}
}
}
}