mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-12 19:41:10 +00:00
A drawer on Back: HUD, frame generation, on-screen controls, background, stop
Back used to background the app outright. It now opens a slim panel on the right with the switches a player reaches for mid-game — the performance HUD, the frame-generation picker (shared with the main screen), the on-screen controls' mode (auto / always / never, replacing the file-only override) — and the two things that leave the session: send it to the background, or stop it. Back again, or a tap outside, closes it.
This commit is contained in:
1 parent
3582186951
commit
51303fa342
9 files changed
+260
-42
No files matched your search
@@ -70,9 +70,10 @@ Pick an engine and 2×/3×/4× on the main screen; it applies immediately, mid-g
|
||||
A session belongs to a foreground service, not to the activity, so leaving Big Picture does not
|
||||
end it: the process stays at perceptible priority, a partial wake lock keeps the CPU from dropping
|
||||
the guest's threads, and a high-performance WiFi lock keeps a backgrounded download from being
|
||||
throttled to nothing. Back puts the app away and leaves Steam running; the (silent) notification
|
||||
brings it back. A session ends only when you say so — the notification's **Stop session** action —
|
||||
or when the app is swiped out of recents.
|
||||
throttled to nothing. **Back opens the drawer**: the performance HUD switch, frame generation, on-screen controls
|
||||
(auto / always / never), *Send to background* and *Stop session*. Backgrounded, Steam keeps
|
||||
running and the (silent) notification brings it back. A session ends only when you say so — the
|
||||
drawer or the notification's **Stop session** — or when the app is swiped out of recents.
|
||||
|
||||
## Device switches
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ class MainActivity : Activity() {
|
||||
playButton = findViewById(R.id.play)
|
||||
runtimeButton = findViewById(R.id.runtime)
|
||||
frameGenButton = findViewById(R.id.frame_gen)
|
||||
frameGenButton.setOnClickListener { pickFrameGen() }
|
||||
frameGenButton.setOnClickListener { FrameGen.showPicker(this) { render() } }
|
||||
|
||||
playButton.setOnClickListener { startActivity(Intent(this, SessionActivity::class.java)) }
|
||||
runtimeButton.setOnClickListener { onRuntimeButton() }
|
||||
@@ -123,34 +123,6 @@ class MainActivity : Activity() {
|
||||
ui.post { render() }
|
||||
}
|
||||
|
||||
/** Off, or an engine at 2x/3x/4x. Saved, and pushed to a running session on the next resume. */
|
||||
private fun pickFrameGen() {
|
||||
val choices = arrayOf("Off",
|
||||
"Win-FG 2×", "Win-FG 3×", "Win-FG 4×",
|
||||
"LSFG 2× (needs Lossless Scaling)", "LSFG 3× (needs Lossless Scaling)", "LSFG 4× (needs Lossless Scaling)")
|
||||
val engine = FrameGen.engine(this)
|
||||
val current = when (engine) {
|
||||
FrameGen.ENGINE_WINFG -> FrameGen.multiplier(this) - 1
|
||||
FrameGen.ENGINE_LSFG -> FrameGen.multiplier(this) + 2
|
||||
else -> 0
|
||||
}
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.frame_gen_title)
|
||||
.setSingleChoiceItems(choices, current) { dialog, which ->
|
||||
when (which) {
|
||||
0 -> FrameGen.set(this, FrameGen.ENGINE_OFF, 2)
|
||||
in 1..3 -> FrameGen.set(this, FrameGen.ENGINE_WINFG, which + 1)
|
||||
else -> FrameGen.set(this, FrameGen.ENGINE_LSFG, which - 2)
|
||||
}
|
||||
dialog.dismiss()
|
||||
render()
|
||||
}
|
||||
.setNeutralButton(android.R.string.ok, null)
|
||||
.setMessage(null)
|
||||
.show()
|
||||
Log.i(TAG, "frame generation picker: " + FrameGen.label(this))
|
||||
}
|
||||
|
||||
private fun render() {
|
||||
frameGenButton.text = getString(R.string.frame_gen, FrameGen.label(this))
|
||||
val installed = LinuxRuntimeInstaller.installedVersion(this)
|
||||
|
||||
@@ -24,6 +24,8 @@ import com.steamdeck.launcher.input.PadBridge
|
||||
import com.steamdeck.launcher.runtime.LinuxRuntime
|
||||
import com.steamdeck.launcher.session.LoadingPanel
|
||||
import com.steamdeck.launcher.session.PerfHud
|
||||
import com.steamdeck.launcher.session.SessionDrawer
|
||||
import com.steamdeck.launcher.session.SessionPrefs
|
||||
import com.steamdeck.launcher.session.SessionService
|
||||
import com.steamdeck.launcher.session.SessionState
|
||||
import com.steamdeck.launcher.wayland.CompositorHost
|
||||
@@ -44,6 +46,7 @@ class SessionActivity : Activity(), SurfaceHolder.Callback {
|
||||
private lateinit var surfaceView: SurfaceView
|
||||
private lateinit var loading: LoadingPanel
|
||||
private lateinit var hud: PerfHud
|
||||
private lateinit var drawer: SessionDrawer
|
||||
private var padBridge: PadBridge? = null
|
||||
private var onScreenControls: OnScreenControls? = null
|
||||
private var watching = true
|
||||
@@ -83,6 +86,13 @@ class SessionActivity : Activity(), SurfaceHolder.Callback {
|
||||
// client's first frame, since before that there is nothing to count.
|
||||
hud = PerfHud(this)
|
||||
root.addView(hud.view, root.indexOfChild(panel))
|
||||
// Back opens this; it sits above everything, the loading panel included.
|
||||
drawer = SessionDrawer(this, root,
|
||||
onHudChanged = { hud.refresh() },
|
||||
onOscChanged = { updateOnScreenControls() },
|
||||
onFrameGenChanged = { applyFrameGen() },
|
||||
onBackground = { moveTaskToBack(true) },
|
||||
onStop = { SessionService.stop(this); finish() })
|
||||
// A session that has already drawn is past its milestones; do not cover its picture.
|
||||
if (SessionState.running && SessionState.firstFrameSeen) {
|
||||
loading.hide()
|
||||
@@ -259,26 +269,27 @@ class SessionActivity : Activity(), SurfaceHolder.Callback {
|
||||
// ── Lifecycle ───────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Back leaves the session running and puts the app behind whatever the user wants next; the
|
||||
* notification brings it back. Ending a session is done deliberately — from the
|
||||
* notification's Stop action, or by swiping the app out of recents.
|
||||
* Back opens the drawer (and closes it again). Leaving the session running in the background
|
||||
* and ending it are both actions in there, so neither can happen by accident from a button
|
||||
* a game might also be reading.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onBackPressed() {
|
||||
moveTaskToBack(true)
|
||||
drawer.toggle()
|
||||
}
|
||||
|
||||
/**
|
||||
* `steamdeck-osc` in Downloads forces the matter either way ("always" / "never"); with no such
|
||||
* file the controls follow what is attached.
|
||||
* The drawer's mode ("always" / "never") decides outright; on "auto" the controls follow
|
||||
* what is attached. `steamdeck-osc` in Downloads still overrides, for a device we cannot reach.
|
||||
*/
|
||||
private fun updateOnScreenControls() {
|
||||
val forced = File(Environment.getExternalStorageDirectory(), "Download/steamdeck-osc")
|
||||
.takeIf { it.isFile }
|
||||
?.let { FileUtils.readString(it)?.trim()?.lowercase() }
|
||||
val show = when {
|
||||
forced == "always" -> true
|
||||
forced == "never" -> false
|
||||
?: SessionPrefs.oscMode(this)
|
||||
val show = when (forced) {
|
||||
SessionPrefs.OSC_ALWAYS -> true
|
||||
SessionPrefs.OSC_NEVER -> false
|
||||
else -> !PadBridge.anyControllerConnected()
|
||||
}
|
||||
val controls = onScreenControls ?: return
|
||||
|
||||
@@ -82,5 +82,30 @@ object FrameGen {
|
||||
return problem
|
||||
}
|
||||
|
||||
/** Off, or an engine at 2x/3x/4x; saved on pick, then [onChanged]. */
|
||||
fun showPicker(activity: android.app.Activity, onChanged: () -> Unit) {
|
||||
val choices = arrayOf("Off",
|
||||
"Win-FG 2×", "Win-FG 3×", "Win-FG 4×",
|
||||
"LSFG 2× (needs Lossless Scaling)", "LSFG 3× (needs Lossless Scaling)", "LSFG 4× (needs Lossless Scaling)")
|
||||
val current = when (engine(activity)) {
|
||||
ENGINE_WINFG -> multiplier(activity) - 1
|
||||
ENGINE_LSFG -> multiplier(activity) + 2
|
||||
else -> 0
|
||||
}
|
||||
android.app.AlertDialog.Builder(activity)
|
||||
.setTitle(com.steamdeck.launcher.R.string.frame_gen_title)
|
||||
.setSingleChoiceItems(choices, current) { dialog, which ->
|
||||
when (which) {
|
||||
0 -> set(activity, ENGINE_OFF, 2)
|
||||
in 1..3 -> set(activity, ENGINE_WINFG, which + 1)
|
||||
else -> set(activity, ENGINE_LSFG, which - 2)
|
||||
}
|
||||
dialog.dismiss()
|
||||
onChanged()
|
||||
}
|
||||
.setNeutralButton(android.R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private const val TAG = "FrameGen"
|
||||
}
|
||||
@@ -49,7 +49,10 @@ class PerfHud(context: Context) {
|
||||
private var lastTick = 0L
|
||||
private var running = false
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private val enabled = !File(Environment.getExternalStorageDirectory(), "Download/steamdeck-no-hud").exists()
|
||||
/** The session's own switch (drawer), with the Downloads file as a device-side override. */
|
||||
private val enabled: Boolean
|
||||
get() = SessionPrefs.hudEnabled(context)
|
||||
&& !File(Environment.getExternalStorageDirectory(), "Download/steamdeck-no-hud").exists()
|
||||
|
||||
private val listener = object : WaylandCompositor.GameListener {
|
||||
override fun onGameSurface(window: String?, gpuName: String?) {}
|
||||
@@ -68,6 +71,11 @@ class PerfHud(context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Re-reads the switch: starts or stops to match it. */
|
||||
fun refresh() {
|
||||
if (enabled) start() else stop()
|
||||
}
|
||||
|
||||
fun start() {
|
||||
if (!enabled || running) return
|
||||
running = true
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.steamdeck.launcher.session
|
||||
|
||||
import android.app.Activity
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.Switch
|
||||
import com.steamdeck.launcher.R
|
||||
import com.steamdeck.launcher.gpu.FrameGen
|
||||
|
||||
/**
|
||||
* The drawer Back opens over a running session. It owns nothing: every control reads and writes
|
||||
* a preference or calls back into the activity, so closing it changes nothing by itself.
|
||||
*/
|
||||
class SessionDrawer(
|
||||
private val activity: Activity,
|
||||
parent: ViewGroup,
|
||||
private val onHudChanged: (Boolean) -> Unit,
|
||||
private val onOscChanged: () -> Unit,
|
||||
private val onFrameGenChanged: () -> Unit,
|
||||
private val onBackground: () -> Unit,
|
||||
private val onStop: () -> Unit,
|
||||
) {
|
||||
private val root: View = activity.layoutInflater.inflate(R.layout.session_drawer, parent, false)
|
||||
private val hud = root.findViewById<Switch>(R.id.drawer_hud)
|
||||
private val frameGen = root.findViewById<Button>(R.id.drawer_frame_gen)
|
||||
private val osc = root.findViewById<Button>(R.id.drawer_osc)
|
||||
|
||||
init {
|
||||
root.visibility = View.GONE
|
||||
parent.addView(root)
|
||||
// The dimmed area closes; the panel itself swallows its touches.
|
||||
root.setOnClickListener { close() }
|
||||
root.findViewById<View>(R.id.drawer_panel).setOnClickListener { }
|
||||
hud.setOnCheckedChangeListener { _, on ->
|
||||
SessionPrefs.setHudEnabled(activity, on)
|
||||
onHudChanged(on)
|
||||
}
|
||||
frameGen.setOnClickListener {
|
||||
FrameGen.showPicker(activity) {
|
||||
refresh()
|
||||
onFrameGenChanged()
|
||||
}
|
||||
}
|
||||
osc.setOnClickListener {
|
||||
val next = when (SessionPrefs.oscMode(activity)) {
|
||||
SessionPrefs.OSC_AUTO -> SessionPrefs.OSC_ALWAYS
|
||||
SessionPrefs.OSC_ALWAYS -> SessionPrefs.OSC_NEVER
|
||||
else -> SessionPrefs.OSC_AUTO
|
||||
}
|
||||
SessionPrefs.setOscMode(activity, next)
|
||||
refresh()
|
||||
onOscChanged()
|
||||
}
|
||||
root.findViewById<View>(R.id.drawer_background).setOnClickListener {
|
||||
close()
|
||||
onBackground()
|
||||
}
|
||||
root.findViewById<View>(R.id.drawer_stop).setOnClickListener {
|
||||
close()
|
||||
onStop()
|
||||
}
|
||||
}
|
||||
|
||||
val isOpen: Boolean get() = root.visibility == View.VISIBLE
|
||||
|
||||
fun open() {
|
||||
refresh()
|
||||
root.visibility = View.VISIBLE
|
||||
root.bringToFront()
|
||||
}
|
||||
|
||||
fun close() {
|
||||
root.visibility = View.GONE
|
||||
}
|
||||
|
||||
fun toggle() = if (isOpen) close() else open()
|
||||
|
||||
private fun refresh() {
|
||||
hud.isChecked = SessionPrefs.hudEnabled(activity)
|
||||
frameGen.text = activity.getString(R.string.frame_gen, FrameGen.label(activity))
|
||||
val mode = when (SessionPrefs.oscMode(activity)) {
|
||||
SessionPrefs.OSC_ALWAYS -> activity.getString(R.string.osc_always)
|
||||
SessionPrefs.OSC_NEVER -> activity.getString(R.string.osc_never)
|
||||
else -> activity.getString(R.string.osc_auto)
|
||||
}
|
||||
osc.text = activity.getString(R.string.drawer_osc, mode)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.steamdeck.launcher.session
|
||||
|
||||
import android.content.Context
|
||||
|
||||
/** The in-session switches: the HUD and how the on-screen controls decide to appear. */
|
||||
object SessionPrefs {
|
||||
const val OSC_AUTO = "auto"
|
||||
const val OSC_ALWAYS = "always"
|
||||
const val OSC_NEVER = "never"
|
||||
|
||||
private fun prefs(context: Context) = context.getSharedPreferences("session", Context.MODE_PRIVATE)
|
||||
|
||||
fun hudEnabled(context: Context): Boolean = prefs(context).getBoolean("hud", true)
|
||||
|
||||
fun setHudEnabled(context: Context, on: Boolean) {
|
||||
prefs(context).edit().putBoolean("hud", on).apply()
|
||||
}
|
||||
|
||||
fun oscMode(context: Context): String = prefs(context).getString("osc", OSC_AUTO) ?: OSC_AUTO
|
||||
|
||||
fun setOscMode(context: Context, mode: String) {
|
||||
prefs(context).edit().putString("osc", mode).apply()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
The in-session drawer, opened with Back. A tap on the dimmed area or Back again closes it.
|
||||
Small on purpose: the switches a player reaches for mid-game, nothing that needs the main
|
||||
screen.
|
||||
-->
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/drawer_scrim"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#66000000"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/drawer_panel"
|
||||
android:layout_width="264dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="end"
|
||||
android:orientation="vertical"
|
||||
android:background="#F2151A22"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/app_name"
|
||||
android:textColor="#FFFFFFFF"
|
||||
android:textSize="16sp"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<Switch
|
||||
android:id="@+id/drawer_hud"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="40dp"
|
||||
android:text="@string/drawer_hud"
|
||||
android:textColor="#FFDDDDDD"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/drawer_frame_gen"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:minHeight="40dp"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/drawer_osc"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:minHeight="40dp"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/drawer_background"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="40dp"
|
||||
android:textSize="13sp"
|
||||
android:text="@string/drawer_background" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/drawer_stop"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:minHeight="40dp"
|
||||
android:textSize="13sp"
|
||||
android:textColor="#FFFF8A80"
|
||||
android:text="@string/stop_session" />
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
@@ -36,6 +36,12 @@
|
||||
<string name="frame_gen">Frame generation: %1$s</string>
|
||||
<string name="frame_gen_title">Frame generation</string>
|
||||
<string name="frame_gen_help">Extra frames are generated between the real ones on the way to the screen, so a game running at 30 fps looks like 60. Win-FG is built in. LSFG uses the shaders from your own copy of Lossless Scaling — install it from the Steam client in this app first. Takes effect immediately, even mid-game.</string>
|
||||
<string name="drawer_hud">Performance HUD</string>
|
||||
<string name="drawer_osc">On-screen controls: %1$s</string>
|
||||
<string name="drawer_background">Send to background</string>
|
||||
<string name="osc_auto">auto</string>
|
||||
<string name="osc_always">always</string>
|
||||
<string name="osc_never">never</string>
|
||||
<string name="credits_line">by The412Banner and maxjivi05 · credits</string>
|
||||
<string name="credits_title">Credits</string>
|
||||
<string name="credits_body">SteamDeck\n\nThe412Banner — the app, the Wayland compositor, the runtime and the Steam session.\n\nmaxjivi05 (Max) — the gamescope runtime this is built on: the proot session, the session shim, the fake-evdev interposer and the controller work, from WinNative.\n\nGPL-3.0. Valve, Steam, Steam Deck and Proton are trademarks of Valve Corporation; this project is not affiliated with Valve. gamescope, Mesa, Turnip, Xwayland, PulseAudio, proot and Arch Linux ARM are their authors\' own.</string>
|
||||
|
||||
Reference in new issue
Block a user