front end: Setup rows join the rail's tracked column (the pill can reach Performance); the session drawer keeps its frame-generation dialog

This commit is contained in:
The412Banner committed 2026-09-22 18:31:13 -04:00
1 parent 8dd36b794e
commit af3f377dfd
2 files changed
+120 -53

No files matched your search

@@ -0,0 +1,67 @@
package com.steamdeck.launcher.ui
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.unit.dp
import com.steamdeck.launcher.gpu.FrameGen
/**
* Off, or an engine at 2x/3x/4x. The LSFG rows are dimmed and unpickable until Lossless Scaling
* is installed in the Steam client — the engine cannot run without its DLL, so offering it would
* only produce a setting that quietly does nothing.
*/
@Composable
fun FrameGenDialog(
engine: String,
multiplier: Int,
lsfgReady: Boolean,
onPick: (engine: String, multiplier: Int) -> Unit,
onDismiss: () -> Unit,
) {
data class Row(val label: String, val engine: String, val multiplier: Int, val enabled: Boolean)
val note = if (lsfgReady) "" else " — install Lossless Scaling in Steam"
val rows = listOf(Row("Off", FrameGen.ENGINE_OFF, 2, true)) +
(2..4).map { Row("Win-FG ${it}×", FrameGen.ENGINE_WINFG, it, true) } +
(2..4).map { Row("LSFG ${it}×$note", FrameGen.ENGINE_LSFG, it, lsfgReady) }
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Frame generation") },
text = {
Column {
Text(
"Extra frames are generated between the real ones on the way to the screen, so a game running at 30 fps looks like 60. Takes effect at once, mid-game included.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(bottom = 8.dp),
)
for (row in rows) {
val selected = row.engine == engine && (row.engine == FrameGen.ENGINE_OFF || row.multiplier == multiplier)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.alpha(if (row.enabled) 1f else 0.38f)
.clickable(enabled = row.enabled) { onPick(row.engine, row.multiplier) }
.padding(vertical = 2.dp),
) {
RadioButton(selected = selected, onClick = null, enabled = row.enabled)
Text(row.label, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(start = 4.dp))
}
}
}
},
confirmButton = { TextButton(onClick = onDismiss) { Text("OK") } },
)
}
@@ -366,62 +366,62 @@ private fun Rail(
if (s.emulators.none { it.installed }) NavItem("install emulators under Desktop & apps", "x", false, small = true, muted = true, register = register, unregister = unregister) { a.onApps() }
NavItem("Settings", "settings:lxqt", selected == "settings:lxqt", small = true, tiny = true, muted = true, i = s.emulators.count { it.installed }, register = register, unregister = unregister) { a.onDesktopSettings() }
}
Spacer(Modifier.height(6.dp))
// Setup: the same fold as Steam and Desktop. A row that holds a value opens a small menu
// in place; a row that opens a page lights up while the page is shown.
val menus = remember { MenuHost() }
val item: @Composable (String, String?, Int, String?, () -> Unit) -> Unit = { label, value, i, key, act ->
NavItem(label, key ?: "x", key != null && selected == key, small = true, tiny = true, muted = true, value = value, i = i, register = register, unregister = unregister) { act() }
}
NavItem("Setup", "x", false, caret = openSetup, count = 9, onClick = onToggleSetup)
Sub(openSetup) {
item("Files", null, 0, null, a.onFiles)
item("Desktop & apps", null, 1, null, a.onApps)
item("Compatibility tools", null, 2, null, a.onProtons)
Box {
item("Frame generation", s.frameGenLabel, 3, null) { menus.open = "fg" }
AnchoredMenu(
menus.open == "fg", onDismiss = { if (menus.open == "fg") menus.open = null }, title = "Frame generation",
note = "Extra frames between the real ones on the way to the screen, so 30 fps looks like 60. Takes effect at once, mid-game included.",
) {
val need = if (s.lsfgReady) null else "install Lossless Scaling in Steam"
MenuItem("Off", checked = s.frameGenEngine == FrameGen.ENGINE_OFF) { a.onFrameGenPick(FrameGen.ENGINE_OFF, 2); menus.open = null }
for (m in 2..4) MenuItem("Win-FG ${m}×", checked = s.frameGenEngine == FrameGen.ENGINE_WINFG && s.frameGenMultiplier == m) { a.onFrameGenPick(FrameGen.ENGINE_WINFG, m); menus.open = null }
for (m in 2..4) MenuItem("LSFG ${m}×", checked = s.frameGenEngine == FrameGen.ENGINE_LSFG && s.frameGenMultiplier == m, enabled = s.lsfgReady, detail = need) { a.onFrameGenPick(FrameGen.ENGINE_LSFG, m); menus.open = null }
}
}
item("Performance", null, 4, "performance", a.onPerformance)
item("ROMs folder", s.romsDir?.substringAfterLast('/')?.ifEmpty { s.romsDir } ?: "choose", 5, null, a.onRoms)
Box {
item("Session logs", if (s.logsEnabled) "on" else "off", 6, null) { menus.open = "logs" }
AnchoredMenu(
menus.open == "logs", onDismiss = { if (menus.open == "logs") menus.open = null }, title = "Session logs",
note = "Each session leaves a folder in Downloads: the guest log, the device and network reports, the app's own log.",
) {
MenuItem("On", checked = s.logsEnabled) { if (!s.logsEnabled) a.onLogs(); menus.open = null }
MenuItem("Off", checked = !s.logsEnabled) { if (s.logsEnabled) a.onLogs(); menus.open = null }
}
}
Box {
item("Start offline", when { s.offlineAccount == null -> "sign in first"; s.offline -> "on"; else -> "off" }, 7, null) { if (s.offlineAccount != null) menus.open = "offline" }
AnchoredMenu(
menus.open == "offline", onDismiss = { if (menus.open == "offline") menus.open = null }, title = "Start offline",
note = "The client starts as ${s.offlineAccount ?: "the saved account"} without the network: the library and installed games, no store.",
) {
MenuItem("On", checked = s.offline) { if (!s.offline) a.onOffline(); menus.open = null }
MenuItem("Off", checked = !s.offline) { if (s.offline) a.onOffline(); menus.open = null }
}
}
item("Linux runtime", when {
s.busy -> "working…"
!s.ready -> "install"
s.available != null && s.available != s.installed -> "update"
else -> "remove"
}, 8, null, a.onRuntime)
}
}
}
Spacer(Modifier.height(6.dp))
// Setup: the same fold as Steam and Desktop. A row that holds a value opens a small menu
// in place; a row that opens a page lights up while the page is shown.
val menus = remember { MenuHost() }
val item: @Composable (String, String?, Int, String?, () -> Unit) -> Unit = { label, value, i, key, act ->
NavItem(label, key ?: "x", key != null && selected == key, small = true, tiny = true, muted = true, value = value, i = i, register = register, unregister = unregister) { act() }
}
NavItem("Setup", "x", false, caret = openSetup, count = 9, onClick = onToggleSetup)
Sub(openSetup) {
item("Files", null, 0, null, a.onFiles)
item("Desktop & apps", null, 1, null, a.onApps)
item("Compatibility tools", null, 2, null, a.onProtons)
Box {
item("Frame generation", s.frameGenLabel, 3, null) { menus.open = "fg" }
AnchoredMenu(
menus.open == "fg", onDismiss = { if (menus.open == "fg") menus.open = null }, title = "Frame generation",
note = "Extra frames between the real ones on the way to the screen, so 30 fps looks like 60. Takes effect at once, mid-game included.",
) {
val need = if (s.lsfgReady) null else "install Lossless Scaling in Steam"
MenuItem("Off", checked = s.frameGenEngine == FrameGen.ENGINE_OFF) { a.onFrameGenPick(FrameGen.ENGINE_OFF, 2); menus.open = null }
for (m in 2..4) MenuItem("Win-FG ${m}×", checked = s.frameGenEngine == FrameGen.ENGINE_WINFG && s.frameGenMultiplier == m) { a.onFrameGenPick(FrameGen.ENGINE_WINFG, m); menus.open = null }
for (m in 2..4) MenuItem("LSFG ${m}×", checked = s.frameGenEngine == FrameGen.ENGINE_LSFG && s.frameGenMultiplier == m, enabled = s.lsfgReady, detail = need) { a.onFrameGenPick(FrameGen.ENGINE_LSFG, m); menus.open = null }
}
}
item("Performance", null, 4, "performance", a.onPerformance)
item("ROMs folder", s.romsDir?.substringAfterLast('/')?.ifEmpty { s.romsDir } ?: "choose", 5, null, a.onRoms)
Box {
item("Session logs", if (s.logsEnabled) "on" else "off", 6, null) { menus.open = "logs" }
AnchoredMenu(
menus.open == "logs", onDismiss = { if (menus.open == "logs") menus.open = null }, title = "Session logs",
note = "Each session leaves a folder in Downloads: the guest log, the device and network reports, the app's own log.",
) {
MenuItem("On", checked = s.logsEnabled) { if (!s.logsEnabled) a.onLogs(); menus.open = null }
MenuItem("Off", checked = !s.logsEnabled) { if (s.logsEnabled) a.onLogs(); menus.open = null }
}
}
Box {
item("Start offline", when { s.offlineAccount == null -> "sign in first"; s.offline -> "on"; else -> "off" }, 7, null) { if (s.offlineAccount != null) menus.open = "offline" }
AnchoredMenu(
menus.open == "offline", onDismiss = { if (menus.open == "offline") menus.open = null }, title = "Start offline",
note = "The client starts as ${s.offlineAccount ?: "the saved account"} without the network: the library and installed games, no store.",
) {
MenuItem("On", checked = s.offline) { if (!s.offline) a.onOffline(); menus.open = null }
MenuItem("Off", checked = !s.offline) { if (s.offline) a.onOffline(); menus.open = null }
}
}
item("Linux runtime", when {
s.busy -> "working…"
!s.ready -> "install"
s.available != null && s.available != s.installed -> "update"
else -> "remove"
}, 8, null, a.onRuntime)
}
Text(
"credits", fontSize = 11.sp, color = colors.onSurfaceVariant,
modifier = Modifier.clip(Shape10).clickable(onClick = a.onCredits).padding(horizontal = 10.dp, vertical = 8.dp),