mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 11:01:07 +00:00
Custom resolution per mode, as in Bannerlator
Resolution gains Custom…: a width × height box (320×240 to 3840×2160, evened) with one-tap presets for 4:3 (960×720, 1024×768, 1280×960), 16:10 (1280×800) and 16:9. A custom size replaces the cap and the shape for that mode - the Shape row greys out and says so - and the compositor fits it to the panel with bars where the shapes differ. Choosing any cap clears it. The device report lists it for both modes.
This commit is contained in:
1 parent
e3fe4ed94e
commit
92df14bb6e
5 files changed
+107
-6
No files matched your search
@@ -149,6 +149,7 @@ class MainActivity : ComponentActivity() {
|
||||
// The mode whose settings dialog is open, with what it shows; refreshed by openModeSettings().
|
||||
private var settingsMode by mutableStateOf<String?>(null)
|
||||
private var resolutionCap by mutableStateOf(1080)
|
||||
private var customResolution by mutableStateOf<Pair<Int, Int>?>(null)
|
||||
private var fexPreset by mutableStateOf("")
|
||||
private var steamChannel by mutableStateOf("publicbeta")
|
||||
private var theme by mutableStateOf("paper")
|
||||
@@ -414,7 +415,7 @@ class MainActivity : ComponentActivity() {
|
||||
private fun ModeSettingsHost(mode: String) {
|
||||
ModeSettingsPage(
|
||||
ModeSettings(
|
||||
mode = mode, resolutionCap = resolutionCap, shapeMode = shapeMode,
|
||||
mode = mode, resolutionCap = resolutionCap, customResolution = customResolution, shapeMode = shapeMode,
|
||||
hdr = hdrOn, hdrReason = hdrReason,
|
||||
linuxRows = linuxRows,
|
||||
linuxSelected = if (mode == SessionService.MODE_STEAM) linuxSteam else linuxDesktop,
|
||||
@@ -436,6 +437,7 @@ class MainActivity : ComponentActivity() {
|
||||
),
|
||||
ModeSettingsActions(
|
||||
onResolution = { cap -> SessionPrefs.setResolutionCap(this, mode, cap); resolutionCap = cap },
|
||||
onCustomResolution = { size -> SessionPrefs.setCustomResolution(this, mode, size); customResolution = size },
|
||||
onShape = { shape -> SessionPrefs.setShapeMode(this, shape); shapeMode = shape },
|
||||
onHdr = { on -> SessionPrefs.setHdr(this, mode, on); hdrOn = on },
|
||||
onSelectLinux = { id -> SessionPrefs.setLinuxDriver(this, mode, id); refreshDrivers() },
|
||||
@@ -530,6 +532,7 @@ class MainActivity : ComponentActivity() {
|
||||
showApps = false
|
||||
refreshDrivers()
|
||||
resolutionCap = SessionPrefs.resolutionCap(this, mode)
|
||||
customResolution = SessionPrefs.customResolution(this, mode)
|
||||
fexPreset = SessionPrefs.fexPreset(this)
|
||||
steamChannel = SessionPrefs.steamChannel(this)
|
||||
addedGamesDirs = SessionPrefs.addedGamesDirs(this)
|
||||
|
||||
@@ -456,6 +456,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
// handheld panel. The mode's settings (the cog beside Play / Desktop) can change
|
||||
// the cap or lift it to the panel.
|
||||
val mode = SessionPrefs.prefMode(intent.getStringExtra(SessionService.EXTRA_MODE) ?: SessionService.MODE_STEAM)
|
||||
// A custom resolution is taken as given; the compositor fits it to the panel.
|
||||
SessionPrefs.customResolution(this, mode)?.let { return it }
|
||||
val cap = SessionPrefs.resolutionCap(this, mode)
|
||||
val height = (if (cap <= 0) panelH else minOf(panelH, cap.toFloat())).toInt()
|
||||
val width = (height * aspect).toInt()
|
||||
|
||||
@@ -109,6 +109,8 @@ object DeviceReport {
|
||||
k("Session output", SessionState.outputSize?.let { "${it.first}x${it.second}" })
|
||||
k("Session refresh", String.format(Locale.US, "%.2f Hz", SessionState.refreshHz))
|
||||
k("Shape setting", SessionPrefs.shapeMode(context))
|
||||
k("Custom resolution (Steam)", SessionPrefs.customResolution(context, SessionService.MODE_STEAM)?.let { "${it.first}x${it.second}" } ?: "off")
|
||||
k("Custom resolution (Desktop)", SessionPrefs.customResolution(context, SessionService.MODE_DESKTOP)?.let { "${it.first}x${it.second}" } ?: "off")
|
||||
k("Foldable", context.packageManager.hasSystemFeature("android.hardware.sensor.hinge_angle"))
|
||||
|
||||
h("Drivers")
|
||||
|
||||
@@ -308,6 +308,27 @@ object SessionPrefs {
|
||||
prefs(context).edit().putInt("resolutionCap.$mode", cap).apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* A fixed size for the session's display, per mode, or null. When set it replaces both the
|
||||
* cap and the shape: the compositor fits it to the panel with bars where the shapes differ.
|
||||
*/
|
||||
fun customResolution(context: Context, mode: String): Pair<Int, Int>? =
|
||||
parseResolution(prefs(context).getString("customRes.$mode", null))
|
||||
|
||||
fun setCustomResolution(context: Context, mode: String, size: Pair<Int, Int>?) {
|
||||
prefs(context).edit().putString("customRes.$mode", size?.let { "${it.first}x${it.second}" }).apply()
|
||||
}
|
||||
|
||||
/** "1024x768" (or ×, or *) to an even size inside 320x240..3840x2160; anything else is null. */
|
||||
fun parseResolution(text: String?): Pair<Int, Int>? {
|
||||
val parts = text?.trim()?.split('x', 'X', '×', '*')?.map { it.trim() } ?: return null
|
||||
if (parts.size != 2) return null
|
||||
val w = parts[0].toIntOrNull() ?: return null
|
||||
val h = parts[1].toIntOrNull() ?: return null
|
||||
if (w !in 320..3840 || h !in 240..2160) return null
|
||||
return Pair(w and 1.inv(), h and 1.inv())
|
||||
}
|
||||
|
||||
/**
|
||||
* What the desktop shell composites with: pixman (software, the default - the Adreno stand-in
|
||||
* is not a DRM render node, so labwc's gbm allocator cannot use it), or gles2 / vulkan for a
|
||||
|
||||
@@ -6,6 +6,10 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -18,6 +22,8 @@ class DriverRow(val id: String, val name: String, val detail: String, val remova
|
||||
class ModeSettings(
|
||||
val mode: String,
|
||||
val resolutionCap: Int,
|
||||
/** A fixed session size, or null for the cap and shape. */
|
||||
val customResolution: Pair<Int, Int>? = null,
|
||||
val shapeMode: String,
|
||||
val hdr: Boolean,
|
||||
val hdrReason: String?,
|
||||
@@ -50,6 +56,8 @@ class AddedGameRow(val folderPath: String, val folderName: String, val exePath:
|
||||
|
||||
class ModeSettingsActions(
|
||||
val onResolution: (Int) -> Unit,
|
||||
/** Null clears it. */
|
||||
val onCustomResolution: (Pair<Int, Int>?) -> Unit = {},
|
||||
val onShape: (String) -> Unit,
|
||||
val onHdr: (Boolean) -> Unit,
|
||||
val onSelectLinux: (String) -> Unit,
|
||||
@@ -88,16 +96,25 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
|
||||
) {
|
||||
SettingsGroup("Display") {
|
||||
val default = SessionPrefs.defaultResolutionCap(s.mode)
|
||||
var editCustom by remember { mutableStateOf(false) }
|
||||
val custom = s.customResolution
|
||||
ChoiceRow(
|
||||
host, "res", "Resolution", "Applies next session.",
|
||||
listOf(720 to "Up to 720p", 900 to "Up to 900p", 1080 to "Up to 1080p", 0 to "The panel's own")
|
||||
.map { (cap, label) -> cap to (if (cap == default) "$label - the default" else label) },
|
||||
s.resolutionCap, note = "720p can improve menu responsiveness.",
|
||||
onPick = a.onResolution,
|
||||
.map { (cap, label) -> cap to (if (cap == default) "$label - the default" else label) } +
|
||||
(CUSTOM to (custom?.let { "Custom · ${it.first}×${it.second}" } ?: "Custom…")),
|
||||
if (custom != null) CUSTOM else s.resolutionCap, note = "720p can improve menu responsiveness.",
|
||||
onPick = { v -> if (v == CUSTOM) editCustom = true else { a.onCustomResolution(null); a.onResolution(v) } },
|
||||
)
|
||||
ChoiceRow(
|
||||
host, "shape", "Shape", "The panel's shape stays at 16:9 or wider; pick Exactly this panel for a 4:3 or 3:2 screen.",
|
||||
com.steamdeck.launcher.session.SessionPrefs.shapeChoices, s.shapeMode, onPick = a.onShape,
|
||||
host, "shape", "Shape",
|
||||
if (custom != null) "Set by the custom resolution." else "The panel's shape stays at 16:9 or wider; pick Exactly this panel for a 4:3 or 3:2 screen.",
|
||||
com.steamdeck.launcher.session.SessionPrefs.shapeChoices, s.shapeMode, enabled = custom == null, onPick = a.onShape,
|
||||
)
|
||||
if (editCustom) CustomResolutionDialog(
|
||||
initial = custom,
|
||||
onSave = { size -> editCustom = false; a.onCustomResolution(size) },
|
||||
onDismiss = { editCustom = false },
|
||||
)
|
||||
}
|
||||
SettingsGroup("HDR") {
|
||||
@@ -267,3 +284,59 @@ private fun DriverRowMenu(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The Resolution menu's "Custom…" entry. */
|
||||
private const val CUSTOM = -1
|
||||
|
||||
/** Width × height for the session, with the common handheld shapes one tap away. */
|
||||
@OptIn(androidx.compose.foundation.layout.ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun CustomResolutionDialog(initial: Pair<Int, Int>?, onSave: (Pair<Int, Int>) -> Unit, onDismiss: () -> Unit) {
|
||||
var w by remember { mutableStateOf(initial?.first?.toString() ?: "") }
|
||||
var h by remember { mutableStateOf(initial?.second?.toString() ?: "") }
|
||||
val parsed = SessionPrefs.parseResolution("${w}x$h")
|
||||
androidx.compose.material3.AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text("Custom resolution") },
|
||||
text = {
|
||||
androidx.compose.foundation.layout.Column {
|
||||
Text(
|
||||
"The session's display size. It replaces the cap and the shape; a size that does not match the panel's shape gets bars.",
|
||||
fontSize = 13.sp, color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
androidx.compose.foundation.layout.Row(
|
||||
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(top = 12.dp),
|
||||
) {
|
||||
val numbers = androidx.compose.foundation.text.KeyboardOptions(keyboardType = androidx.compose.ui.text.input.KeyboardType.Number)
|
||||
androidx.compose.material3.OutlinedTextField(
|
||||
w, { v -> w = v.filter(Char::isDigit).take(4) }, label = { Text("Width") },
|
||||
singleLine = true, keyboardOptions = numbers, modifier = Modifier.weight(1f),
|
||||
)
|
||||
Text("×", fontSize = 18.sp, modifier = Modifier.padding(horizontal = 10.dp))
|
||||
androidx.compose.material3.OutlinedTextField(
|
||||
h, { v -> h = v.filter(Char::isDigit).take(4) }, label = { Text("Height") },
|
||||
singleLine = true, keyboardOptions = numbers, modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
androidx.compose.foundation.layout.FlowRow(
|
||||
horizontalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(6.dp),
|
||||
modifier = Modifier.padding(top = 10.dp),
|
||||
) {
|
||||
for ((pw, ph, tag) in listOf(Triple(960, 720, "4:3"), Triple(1024, 768, "4:3"), Triple(1280, 960, "4:3"), Triple(1280, 800, "16:10"), Triple(1152, 648, "16:9"), Triple(1280, 720, "16:9"))) {
|
||||
androidx.compose.material3.AssistChip(
|
||||
onClick = { w = pw.toString(); h = ph.toString() },
|
||||
label = { Text("$pw×$ph · $tag", fontSize = 12.sp) },
|
||||
)
|
||||
}
|
||||
}
|
||||
if (parsed == null && (w.isNotEmpty() || h.isNotEmpty())) Text(
|
||||
"Between 320×240 and 3840×2160.", fontSize = 12.sp, color = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.padding(top = 6.dp),
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = { androidx.compose.material3.TextButton(enabled = parsed != null, onClick = { parsed?.let(onSave) }) { Text("Use") } },
|
||||
dismissButton = { androidx.compose.material3.TextButton(onClick = onDismiss) { Text("Cancel") } },
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user