From aff627c0cdcd88d1651a4b7428493f279c7289fb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <99822064+revonateB0T@users.noreply.github.com> Date: Tue, 26 Nov 2024 17:28:27 +0800 Subject: [PATCH] Implement standby behaviour (#1980) * Implement standby behaviour * Refine --------- Co-authored-by: Dude so hot --- .../hippo/ehviewer/ui/tools/PausableDialog.kt | 67 ++++++++++++++++++- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/PausableDialog.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/PausableDialog.kt index 6381551d6..0ef8156b9 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/PausableDialog.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/PausableDialog.kt @@ -1,32 +1,46 @@ package com.hippo.ehviewer.ui.tools +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.VectorConverter import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.MutatorMutex import androidx.compose.foundation.gestures.draggable2D import androidx.compose.foundation.gestures.rememberDraggable2DState import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.safeContent import androidx.compose.material3.AlertDialog import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.Icon +import androidx.compose.material3.IconButtonDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.round +import androidx.compose.ui.unit.toSize import androidx.compose.ui.util.lerp +import kotlinx.coroutines.launch import kotlinx.serialization.KSerializer import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder +import moe.tarsin.kt.unreachable fun lerp(start: IntOffset, stop: IntOffset, fraction: Float) = IntOffset( lerp(start.x, stop.x, fraction), @@ -47,6 +61,8 @@ fun BoxScope.PausableAlertDialog( text: @Composable () -> Unit, idleIcon: ImageVector, ) { + val scope = rememberCoroutineScope() + val mutex = remember { MutatorMutex() } var showDialog by remember { mutableStateOf(true) } if (showDialog) { AlertDialog( @@ -58,16 +74,61 @@ fun BoxScope.PausableAlertDialog( ) } val fraction by animateFloatAsState(if (showDialog) 0f else 1f) - val info = LocalWindowInfo.current.containerSize + val (viewportW, viewportH) = LocalWindowInfo.current.containerSize.toSize() + val density = LocalDensity.current + val (buttonW, buttonH) = with(density) { IconButtonDefaults.smallContainerSize().toSize() } + val safeContent = WindowInsets.safeContent + val layoutDirection = LocalLayoutDirection.current + val safe = Rect( + left = (buttonW - viewportW) / 2 + safeContent.getLeft(density, layoutDirection), + top = (buttonH - viewportH) / 2 + safeContent.getTop(density), + right = (viewportW - buttonW) / 2 - safeContent.getRight(density, layoutDirection), + bottom = (viewportH - buttonH) / 2 - safeContent.getBottom(density), + ) // Disappear at screen center - val disappear = IntOffset(info.width / 2, 0) + val disappear = safe.center.round() var idle by rememberMutableStateInDataStore("DialogBubble", OffsetSerializer) { Offset.Zero } + LaunchedEffect(Unit) { + idle = idle.safeOffset(safe) + } val state = rememberDraggable2DState { delta -> idle += delta } FilledTonalIconButton( onClick = { showDialog = true }, - modifier = Modifier.offset { lerp(disappear, idle.round(), fraction) }.graphicsLayer { alpha = fraction }.align(Alignment.CenterStart).draggable2D(state = state), + modifier = Modifier.offset { + lerp(disappear, idle.round(), fraction) + }.graphicsLayer { + alpha = fraction + }.align(Alignment.Center).draggable2D( + state = state, + onDragStopped = { v -> + val target = idle.safeOffset(safe) + scope.launch { + mutex.mutate { + Animatable(idle, Offset.VectorConverter).animateTo(target, initialVelocity = Offset(v.x, v.y)) { + idle = value + } + } + } + }, + ), ) { Icon(imageVector = idleIcon, contentDescription = null) } } + +fun Offset.safeOffset(edge: Rect): Offset { + val (x, y) = this + val (l, t, r, b) = edge + val dl = x - l + val dr = r - x + val dt = y - t + val db = b - y + return when (minOf(dl, dr, dt, db)) { + dl -> Offset(l, y) + dr -> Offset(r, y) + dt -> Offset(x, t) + db -> Offset(x, b) + else -> unreachable() + } +}