mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c435047b9c | ||
|
|
414f04182e |
No files matched your search
@@ -89,6 +89,9 @@ class MainActivity : ComponentActivity() {
|
||||
private var flood by mutableStateOf<PendingFlood?>(null)
|
||||
private var floodProgress by androidx.compose.runtime.mutableFloatStateOf(0f)
|
||||
private class PendingFlood(val from: androidx.compose.ui.geometry.Rect, val intent: Intent)
|
||||
/** A session stopped behind a flood: its blue, drawn back into the button it started from (FloodReturn). */
|
||||
private var returning by mutableStateOf<ReturningFlood?>(null)
|
||||
private class ReturningFlood(val color: Int, val to: androidx.compose.ui.geometry.Rect?)
|
||||
private var ready by mutableStateOf(false)
|
||||
private var available by mutableStateOf<LinuxRuntimeInstaller.Release?>(null)
|
||||
private var busy by mutableStateOf(false)
|
||||
@@ -481,6 +484,13 @@ class MainActivity : ComponentActivity() {
|
||||
)
|
||||
if (showCredits) CreditsDialog { showCredits = false }
|
||||
flood?.let { f -> com.droiddeck.launcher.ui.LaunchFlood(f.from, onProgress = { floodProgress = it }) { launchFlooded(f) } }
|
||||
returning?.let { r ->
|
||||
com.droiddeck.launcher.ui.FloodReturn(androidx.compose.ui.graphics.Color(r.color), r.to, onProgress = { floodProgress = it }) {
|
||||
returning = null
|
||||
floodProgress = 0f
|
||||
com.droiddeck.launcher.ui.LaunchOrigin.flooding = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -557,6 +567,10 @@ class MainActivity : ComponentActivity() {
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
com.droiddeck.launcher.ui.Motion.refresh(this)
|
||||
// Back from a session stopped behind a flood: open on its blue, before the first frame.
|
||||
com.droiddeck.launcher.ui.QuitFlood.take()?.let { c ->
|
||||
returning = ReturningFlood(c, com.droiddeck.launcher.ui.LaunchOrigin.takeReturn())
|
||||
}
|
||||
refreshPhantomStatus()
|
||||
// Swaps queued while a game ran on that Proton go in once nothing uses it (usually the
|
||||
// session has just ended). Cheap when nothing is queued.
|
||||
@@ -592,6 +606,7 @@ class MainActivity : ComponentActivity() {
|
||||
displayManager.unregisterDisplayListener(secondScreenDisplayListener)
|
||||
// The session covers the page by now; coming back finds it as it was.
|
||||
flood = null
|
||||
returning = null
|
||||
floodProgress = 0f
|
||||
com.droiddeck.launcher.ui.LaunchOrigin.flooding = null
|
||||
super.onStop()
|
||||
|
||||
@@ -29,6 +29,8 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.droiddeck.launcher.core.FileUtils
|
||||
import com.droiddeck.launcher.frontend.Library
|
||||
import com.droiddeck.launcher.gpu.FrameGen
|
||||
@@ -144,6 +146,10 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
|
||||
// Compose reads these; the activity writes them.
|
||||
private var drawerOpen by mutableStateOf(false)
|
||||
/** A stop growing out of the dialog's Stop (overlay px); the session ends once it covers the screen. */
|
||||
private var quitFrom by mutableStateOf<androidx.compose.ui.geometry.Rect?>(null)
|
||||
/** Left behind that flood: the front end opens on its blue, so no sink of our own. */
|
||||
private var quitFlooded = false
|
||||
/** The drawer's Components tab: the Protons as last read (ComponentsManager). */
|
||||
private var drawerComponents by mutableStateOf<ComponentsManager.Snapshot?>(null)
|
||||
private var drawerPage by mutableIntStateOf(0)
|
||||
@@ -394,6 +400,15 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
onBackground = { drawerOpen = false; moveTaskToBack(true) },
|
||||
onShareLogs = { drawerOpen = false; shareCurrentSessionLogs() },
|
||||
onStop = { drawerOpen = false; SessionService.stop(this@SessionActivity); finish() },
|
||||
onStopFrom = { onScreen ->
|
||||
drawerOpen = false
|
||||
if (onScreen == null || com.droiddeck.launcher.ui.Motion.scale == 0f || quitFrom != null) {
|
||||
if (quitFrom == null) { SessionService.stop(this@SessionActivity); finish() }
|
||||
} else {
|
||||
val at = IntArray(2).also { sessionOverlay.getLocationOnScreen(it) }
|
||||
quitFrom = onScreen.translate(-at[0].toFloat(), -at[1].toFloat())
|
||||
}
|
||||
},
|
||||
onClose = { drawerOpen = false },
|
||||
components = drawerComponents,
|
||||
onComponentsRefresh = { refreshDrawerComponents() },
|
||||
@@ -403,6 +418,14 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
title = pausedTitle(),
|
||||
onResume = { SessionService.resume(this@SessionActivity) },
|
||||
)
|
||||
quitFrom?.let { from ->
|
||||
val error = androidx.compose.material3.MaterialTheme.colorScheme.error
|
||||
com.droiddeck.launcher.ui.LaunchFlood(
|
||||
from, onProgress = {},
|
||||
fromColors = error to androidx.compose.ui.graphics.lerp(error, androidx.compose.ui.graphics.Color.Black, 0.12f),
|
||||
cornerAtRest = 20.dp,
|
||||
) { finishFlooded() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1554,10 +1577,22 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
/** The screen is all blue: the session stops and the front end opens on the same blue (FloodReturn). */
|
||||
private fun finishFlooded() {
|
||||
if (isFinishing) return
|
||||
val signal = com.droiddeck.launcher.ui.Themes.byId(SessionPrefs.theme(this)).signal
|
||||
com.droiddeck.launcher.ui.QuitFlood.mark(signal.toArgb())
|
||||
window.setBackgroundDrawable(android.graphics.drawable.ColorDrawable(signal.toArgb()))
|
||||
quitFlooded = true
|
||||
SessionService.stop(this)
|
||||
finish()
|
||||
}
|
||||
|
||||
/** Leaving the session sinks its surface back down onto the front end. */
|
||||
override fun finish() {
|
||||
super.finish()
|
||||
overridePendingTransition(R.anim.session_hold, R.anim.session_sink)
|
||||
if (quitFlooded) overridePendingTransition(0, 0)
|
||||
else overridePendingTransition(R.anim.session_hold, R.anim.session_sink)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -36,9 +36,18 @@ import kotlinx.coroutines.coroutineScope
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.layout.LayoutCoordinates
|
||||
import androidx.compose.ui.layout.boundsInRoot
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import java.lang.ref.WeakReference
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.sin
|
||||
|
||||
// Starting a session from a blue button: the button grows until the page is its blue, and the
|
||||
// loading screen opens on that blue and gathers it into the ball of the throbber.
|
||||
// Stopping one from the drawer runs it the other way: Stop grows until the page is blue, and the
|
||||
// front end opens on that blue and draws it back down into the button the session came from.
|
||||
|
||||
/** Carries the flood's colour to the session, which opens on it. */
|
||||
const val EXTRA_FLOOD = "com.droiddeck.launcher.FLOOD"
|
||||
@@ -48,6 +57,8 @@ internal object LaunchOrigin {
|
||||
private var bounds: Rect? = null
|
||||
private var button: Any? = null
|
||||
private var at = 0L
|
||||
/** The last button a session was started from, for a stop to return into. */
|
||||
private var last: WeakReference<Any>? = null
|
||||
|
||||
/** The button a flood is growing out of: it steps aside so only the flood shows. */
|
||||
var flooding by mutableStateOf<Any?>(null)
|
||||
@@ -55,9 +66,22 @@ internal object LaunchOrigin {
|
||||
fun mark(r: Rect, key: Any) {
|
||||
bounds = r
|
||||
button = key
|
||||
last = WeakReference(key)
|
||||
at = SystemClock.uptimeMillis()
|
||||
}
|
||||
|
||||
/**
|
||||
* Where the button the running session was started from sits now, if it is still on screen;
|
||||
* it hides for the return, as for the flood.
|
||||
*/
|
||||
fun takeReturn(): Rect? {
|
||||
val key = last?.get() as? Array<*> ?: return null
|
||||
val c = key.getOrNull(0) as? LayoutCoordinates ?: return null
|
||||
if (!c.isAttached) return null
|
||||
flooding = key
|
||||
return c.boundsInRoot()
|
||||
}
|
||||
|
||||
/** The button behind a launch starting now, if one was pressed just before; it hides for the flood. */
|
||||
fun take(): Rect? {
|
||||
val b = bounds?.takeIf { SystemClock.uptimeMillis() - at < 1_500 }
|
||||
@@ -68,6 +92,23 @@ internal object LaunchOrigin {
|
||||
}
|
||||
}
|
||||
|
||||
/** A session stopped behind a flood: the blue the front end opens on, for a few seconds. */
|
||||
internal object QuitFlood {
|
||||
private var color: Int? = null
|
||||
private var at = 0L
|
||||
|
||||
fun mark(argb: Int) {
|
||||
color = argb
|
||||
at = SystemClock.uptimeMillis()
|
||||
}
|
||||
|
||||
fun take(): Int? {
|
||||
val c = color?.takeIf { SystemClock.uptimeMillis() - at < 3_000 }
|
||||
color = null
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
/** The page a flood grows over, sinking back a touch as it is covered; [progress] is the flood's. */
|
||||
@Composable
|
||||
internal fun FloodBehind(progress: () -> Float, content: @Composable () -> Unit) {
|
||||
@@ -87,9 +128,17 @@ private val Gather = CubicBezierEasing(0.6f, 0f, 0.15f, 1f)
|
||||
* Swallows touches while it runs.
|
||||
*/
|
||||
@Composable
|
||||
internal fun LaunchFlood(from: Rect, onProgress: (Float) -> Unit, onCovered: () -> Unit) {
|
||||
internal fun LaunchFlood(
|
||||
from: Rect,
|
||||
onProgress: (Float) -> Unit,
|
||||
/** The button's own colours, where it is not the page's blue one (a red Stop). */
|
||||
fromColors: Pair<Color, Color>? = null,
|
||||
cornerAtRest: Dp = 12.dp,
|
||||
onCovered: () -> Unit,
|
||||
) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
val start = fromColors ?: (colors.primary to pal.primary2)
|
||||
// Left, top, right, bottom: 0 on the button, 1 on the page's edge. Springs overshoot past the
|
||||
// edge, off the page, so the wobble is felt in the pull and never seen as a shrink.
|
||||
val edges = remember { List(4) { Animatable(0f) } }
|
||||
@@ -117,30 +166,83 @@ internal fun LaunchFlood(from: Rect, onProgress: (Float) -> Unit, onCovered: ()
|
||||
}
|
||||
}
|
||||
Canvas(Modifier.fillMaxSize().onSizeChanged { page = Size(it.width.toFloat(), it.height.toFloat()) }.pointerInput(Unit) { awaitEachGesture { while (true) awaitPointerEvent().changes.forEach { it.consume() } } }) {
|
||||
val (l, t, r, b) = edges.map { it.value }
|
||||
val mean = edges.sumOf { it.value.coerceIn(0f, 1f).toDouble() }.toFloat() / 4f
|
||||
// The button sinks with the page behind (FloodBehind), so the flood leaves from where it is now.
|
||||
val k = sinkScale(mean)
|
||||
val cx = size.width / 2f
|
||||
val cy = size.height / 2f
|
||||
val left = lerp(cx + (from.left - cx) * k, 0f, l)
|
||||
val top = lerp(cy + (from.top - cy) * k, 0f, t)
|
||||
val right = lerp(cx + (from.right - cx) * k, size.width, r)
|
||||
val bottom = lerp(cy + (from.bottom - cy) * k, size.height, b)
|
||||
val w = (right - left).coerceAtLeast(0f)
|
||||
val h = (bottom - top).coerceAtLeast(0f)
|
||||
// A button's corners at rest, a blob in flight, square once it fills the page.
|
||||
val blob = kotlin.math.sin(Math.PI * mean).toFloat().coerceAtLeast(0f)
|
||||
val corner = lerp(12.dp.toPx() * (1f - mean), minOf(w, h) * 0.42f, blob)
|
||||
// The button's gradient gives way to the ball's flat blue in the first half.
|
||||
val mix = (mean * 2f).coerceAtMost(1f)
|
||||
drawRoundRect(
|
||||
Brush.linearGradient(
|
||||
listOf(lerp(colors.primary, pal.signal, mix), lerp(pal.primary2, pal.signal, mix)),
|
||||
start = Offset(left, top), end = Offset(right, bottom),
|
||||
),
|
||||
topLeft = Offset(left, top), size = Size(w, h), cornerRadius = CornerRadius(corner),
|
||||
)
|
||||
drawFlood(from, edges.map { it.value }, start, pal.signal, cornerAtRest.toPx())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The flood from a button at [from] with its edges [v] of the way to the page's (left, top, right,
|
||||
* bottom): the button sinking with the page behind, blobby in flight, its colours [start] giving
|
||||
* way to the flat [signal] in the first half.
|
||||
*/
|
||||
private fun DrawScope.drawFlood(from: Rect, v: List<Float>, start: Pair<Color, Color>, signal: Color, cornerAtRest: Float) {
|
||||
val (l, t, r, b) = v
|
||||
val mean = v.sumOf { it.coerceIn(0f, 1f).toDouble() }.toFloat() / 4f
|
||||
// The button sinks with the page behind (FloodBehind), so the flood leaves from where it is now.
|
||||
val k = sinkScale(mean)
|
||||
val cx = size.width / 2f
|
||||
val cy = size.height / 2f
|
||||
val left = lerp(cx + (from.left - cx) * k, 0f, l)
|
||||
val top = lerp(cy + (from.top - cy) * k, 0f, t)
|
||||
val right = lerp(cx + (from.right - cx) * k, size.width, r)
|
||||
val bottom = lerp(cy + (from.bottom - cy) * k, size.height, b)
|
||||
val w = (right - left).coerceAtLeast(0f)
|
||||
val h = (bottom - top).coerceAtLeast(0f)
|
||||
// A button's corners at rest, a blob in flight, square once it fills the page.
|
||||
val blob = sin(PI * mean).toFloat().coerceAtLeast(0f)
|
||||
val corner = lerp(cornerAtRest * (1f - mean), minOf(w, h) * 0.42f, blob)
|
||||
// The button's gradient gives way to the ball's flat blue in the first half.
|
||||
val mix = (mean * 2f).coerceAtMost(1f)
|
||||
val brush = Brush.linearGradient(
|
||||
listOf(lerp(start.first, signal, mix), lerp(start.second, signal, mix)),
|
||||
start = Offset(left, top), end = Offset(right, bottom),
|
||||
)
|
||||
drawRoundRect(brush, topLeft = Offset(left, top), size = Size(w, h), cornerRadius = CornerRadius(corner))
|
||||
}
|
||||
|
||||
/**
|
||||
* The front end's first frames after a stop: the page in [flood] after a beat, drawn back down
|
||||
* into the button at [to] (root px, measured before the page sank) on critically damped springs,
|
||||
* the far edges first, so it sets down on the button without a bounce. [onProgress] (1 to 0) lets
|
||||
* the page behind rise back. No button to return to: the blue fades. Calls [onLanded] at the end.
|
||||
*/
|
||||
@Composable
|
||||
internal fun FloodReturn(flood: Color, to: Rect?, onProgress: (Float) -> Unit, onLanded: () -> Unit) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
val edges = remember { List(4) { Animatable(1f) } }
|
||||
val fade = remember { Animatable(1f) }
|
||||
val landed by rememberUpdatedState(onLanded)
|
||||
val progress by rememberUpdatedState(onProgress)
|
||||
var page by remember { mutableStateOf(Size.Zero) }
|
||||
LaunchedEffect(page != Size.Zero) {
|
||||
if (page == Size.Zero) return@LaunchedEffect
|
||||
progress(1f)
|
||||
// A beat of plain blue first, so the change of screen does not read as a cut.
|
||||
delay(Motion.ms(220).toLong())
|
||||
if (to == null || Motion.scale == 0f) {
|
||||
fade.animateTo(0f, tween(Motion.ms(240).coerceAtLeast(1)))
|
||||
} else {
|
||||
val travel = listOf(to.left, to.top, page.width - to.right, page.height - to.bottom).map { it.coerceAtLeast(0f) }
|
||||
val far = travel.max().coerceAtLeast(1f)
|
||||
coroutineScope {
|
||||
edges.mapIndexed { i, edge ->
|
||||
val lead = travel[i] / far
|
||||
launch {
|
||||
delay(Motion.ms((180 * (1f - lead)).toInt()).toLong())
|
||||
edge.animateTo(0f, spring(dampingRatio = 1f, stiffness = lerp(110f, 190f, lead))) {
|
||||
progress(edges.sumOf { it.value.coerceIn(0f, 1f).toDouble() }.toFloat() / 4f)
|
||||
}
|
||||
}
|
||||
}.forEach { it.join() }
|
||||
}
|
||||
}
|
||||
progress(0f)
|
||||
landed()
|
||||
}
|
||||
Canvas(Modifier.fillMaxSize().onSizeChanged { page = Size(it.width.toFloat(), it.height.toFloat()) }.pointerInput(Unit) { awaitEachGesture { while (true) awaitPointerEvent().changes.forEach { it.consume() } } }) {
|
||||
if (to == null) drawRect(flood, alpha = fade.value)
|
||||
else drawFlood(to, edges.map { it.value }, colors.primary to pal.primary2, flood, 12.dp.toPx())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -85,6 +85,9 @@ import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.input.InputMode
|
||||
import androidx.compose.ui.platform.LocalInputModeManager
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.layout.boundsInWindow
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@@ -172,6 +175,8 @@ class DrawerActions(
|
||||
val onBackground: () -> Unit,
|
||||
val onShareLogs: () -> Unit,
|
||||
val onStop: () -> Unit,
|
||||
/** Stop with the dialog's button's bounds on screen, for the flood to grow out of; null falls back to [onStop]. */
|
||||
val onStopFrom: ((androidx.compose.ui.geometry.Rect?) -> Unit)? = null,
|
||||
val onClose: () -> Unit,
|
||||
/** Components tab: every Proton with what it uses; null until first read. */
|
||||
val components: ComponentsManager.Snapshot? = null,
|
||||
@@ -453,7 +458,14 @@ fun SessionDrawer(open: Boolean, page: Int, controllerActive: Boolean, onPageCha
|
||||
if (confirmStop) {
|
||||
val cancelFocus = remember { FocusRequester() }
|
||||
val cancel = { confirmStop = false }
|
||||
val stop = { confirmStop = false; a.onStop() }
|
||||
// Where Stop sits on screen: the dialog is a window of its own, so its bounds are taken
|
||||
// against the screen and the session's overlay converts them back.
|
||||
var stopOnScreen by remember { mutableStateOf<androidx.compose.ui.geometry.Rect?>(null) }
|
||||
val stop = {
|
||||
confirmStop = false
|
||||
val from = a.onStopFrom
|
||||
if (from != null) from(stopOnScreen) else a.onStop()
|
||||
}
|
||||
LaunchedEffect(controllerActive) {
|
||||
if (controllerActive) {
|
||||
androidx.compose.runtime.withFrameNanos { }
|
||||
@@ -465,9 +477,13 @@ fun SessionDrawer(open: Boolean, page: Int, controllerActive: Boolean, onPageCha
|
||||
modifier = Modifier.controllerBack(onBack = cancel),
|
||||
title = { Text("Stop session?") },
|
||||
confirmButton = {
|
||||
val dialogView = LocalView.current
|
||||
TextButton(
|
||||
onClick = stop,
|
||||
modifier = Modifier.controllerConfirm(onClick = stop),
|
||||
modifier = Modifier.controllerConfirm(onClick = stop).onGloballyPositioned { c ->
|
||||
val at = IntArray(2).also { dialogView.rootView.getLocationOnScreen(it) }
|
||||
stopOnScreen = c.boundsInWindow().translate(at[0].toFloat(), at[1].toFloat())
|
||||
},
|
||||
colors = ButtonDefaults.textButtonColors(contentColor = colors.error),
|
||||
) { Text("Stop") }
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user