mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 02:51:10 +00:00
Merge pull request #14 from The412Banner/fix/exact-panel-shape
Display: Exactly this panel shape + custom resolution per mode
This commit is contained in:
6 files changed
+125
-11
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)
|
||||
|
||||
@@ -444,13 +444,20 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
// Wider than 16:9 is fine - games and the client cope with a phone's 20:9 - so the
|
||||
// panel's aspect is kept above that, unless the user pinned 16:9 for a foldable, and the
|
||||
// compositor letterboxes onto a squarer panel.
|
||||
val aspect = if (SessionPrefs.shapeMode(this) == SessionPrefs.SHAPE_WIDE) 16f / 9f
|
||||
else maxOf(panelW / panelH, 16f / 9f)
|
||||
// "Exactly this panel" drops that floor, for a 4:3 or 3:2 handheld whose games should
|
||||
// fill it.
|
||||
val aspect = when (SessionPrefs.shapeMode(this)) {
|
||||
SessionPrefs.SHAPE_WIDE -> 16f / 9f
|
||||
SessionPrefs.SHAPE_EXACT -> panelW / panelH
|
||||
else -> maxOf(panelW / panelH, 16f / 9f)
|
||||
}
|
||||
// 720 tall at most by default, client and desktop alike: the client's CEF is the heaviest
|
||||
// thing in the session, and pixels above that cost frames for nothing anyone can see on a
|
||||
// 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")
|
||||
|
||||
@@ -35,10 +35,18 @@ object SessionPrefs {
|
||||
|
||||
const val SHAPE_AUTO = "auto"
|
||||
const val SHAPE_WIDE = "16:9"
|
||||
const val SHAPE_EXACT = "exact"
|
||||
|
||||
/** The choices the settings offer, in order. */
|
||||
val shapeChoices = listOf(
|
||||
SHAPE_AUTO to "The panel's shape (16:9 or wider)",
|
||||
SHAPE_EXACT to "Exactly this panel (4:3, 3:2…)",
|
||||
SHAPE_WIDE to "16:9 with bars",
|
||||
)
|
||||
|
||||
/**
|
||||
* The shape of the display the session presents: the panel's own (never narrower than 16:9)
|
||||
* or a fixed 16:9. A foldable defaults to 16:9, which sits with modest bars on either of its
|
||||
* The shape of the display the session presents: the panel's own (never narrower than 16:9),
|
||||
* exactly the panel's (a 4:3 or 3:2 handheld, drawn edge to edge), or a fixed 16:9. A foldable defaults to 16:9, which sits with modest bars on either of its
|
||||
* panels; the panel's own shape would fit one and leave a strip on the other, and gamescope's
|
||||
* display cannot change size once the session is up.
|
||||
*/
|
||||
@@ -300,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", "Adds bars to preserve 16:9.",
|
||||
listOf("auto" to "The panel's shape", "16:9" to "16:9 with bars"), 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") } },
|
||||
)
|
||||
}
|
||||
@@ -261,7 +261,7 @@ fun SessionDrawer(open: Boolean, a: DrawerActions) {
|
||||
SettingsGroup("Next session") {
|
||||
ChoiceRow(
|
||||
host, "shape", "Display shape", null,
|
||||
listOf(SessionPrefs.SHAPE_AUTO to "The panel's shape", SessionPrefs.SHAPE_WIDE to "16:9 with bars"), a.shapeMode, onPick = a.onShape,
|
||||
SessionPrefs.shapeChoices, a.shapeMode, onPick = a.onShape,
|
||||
)
|
||||
if (a.steam) ChoiceRow(
|
||||
host, "fex", "FEX preset", null,
|
||||
|
||||
Reference in new issue
Block a user