mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 11:01:07 +00:00
Merge main into feat/language-selection
Brings in the display settings and in-session controls rework (#247). Its layout and behaviour are kept; the choices it shows (scaling filters, frame limits, texture options, effect presets, Back order, the auto touch mode) use the translated labels, and its new strings are translated.
This commit is contained in:
commit
ca5f3c97fc
22 files changed
+995
-445
No files matched your search
@@ -314,13 +314,11 @@ 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 resolution by mutableStateOf(com.droiddeck.launcher.session.SessionDisplay.DEFAULT_RESOLUTION)
|
||||
private var fexPreset by mutableStateOf("")
|
||||
private var steamChannel by mutableStateOf("steamdeck_publicbeta")
|
||||
private var runSteamAtStartup by mutableStateOf(false)
|
||||
private var theme by mutableStateOf("graphite")
|
||||
private var shapeMode by mutableStateOf(SessionPrefs.SHAPE_AUTO)
|
||||
private var hdrOn by mutableStateOf(false)
|
||||
private var fpsLimit by mutableStateOf(0)
|
||||
private var upscaler by mutableStateOf(0)
|
||||
@@ -1140,7 +1138,8 @@ class MainActivity : ComponentActivity() {
|
||||
private fun ModeSettingsHost(mode: String) {
|
||||
ModeSettingsPage(
|
||||
ModeSettings(
|
||||
mode = mode, resolutionCap = resolutionCap, customResolution = customResolution, shapeMode = shapeMode,
|
||||
mode = mode, resolution = resolution,
|
||||
panelSize = com.droiddeck.launcher.session.SessionDisplay.panelSize(this),
|
||||
hdr = hdrOn, hdrReason = hdrReason, fpsLimit = fpsLimit,
|
||||
upscaler = upscaler, upscaleSharpness = upscaleSharpness,
|
||||
gpuDrivers = drivers.summary(),
|
||||
@@ -1197,9 +1196,7 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
},
|
||||
onWifiDiscoverySettings = { openWifiDiscoverySettings() },
|
||||
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 },
|
||||
onResolution = { value -> SessionPrefs.setResolutionChoice(this, mode, value); resolution = value },
|
||||
onHdr = { on -> SessionPrefs.setHdr(this, mode, on); hdrOn = on },
|
||||
onGpuDrivers = { openComponents(focusContent = true, tab = com.droiddeck.launcher.ui.GPU_TAB) },
|
||||
onFpsLimit = { fps -> SessionPrefs.setFpsLimit(this, mode, fps); fpsLimit = fps },
|
||||
@@ -1355,8 +1352,7 @@ class MainActivity : ComponentActivity() {
|
||||
showProtons = false
|
||||
showComponents = false
|
||||
showMapping = false
|
||||
resolutionCap = SessionPrefs.resolutionCap(this, mode)
|
||||
customResolution = SessionPrefs.customResolution(this, mode)
|
||||
resolution = SessionPrefs.resolutionChoice(this, mode, com.droiddeck.launcher.session.SessionDisplay.panelSize(this))
|
||||
fexPreset = SessionPrefs.fexPreset(this)
|
||||
fastSync = SessionPrefs.fastSync(this)
|
||||
fsyncFirst = SessionPrefs.fsyncFirst(this)
|
||||
@@ -1368,7 +1364,6 @@ class MainActivity : ComponentActivity() {
|
||||
steamController = SessionPrefs.steamController(this)
|
||||
runSteamAtStartup = SessionPrefs.runSteamAtStartup(this)
|
||||
addedGamesDirs = SessionPrefs.addedGamesDirs(this)
|
||||
shapeMode = SessionPrefs.shapeMode(this)
|
||||
hdrOn = SessionPrefs.hdr(this, mode)
|
||||
fpsLimit = SessionPrefs.fpsLimit(this, mode)
|
||||
upscaler = SessionPrefs.upscaler(this)
|
||||
|
||||
@@ -83,7 +83,9 @@ import com.droiddeck.launcher.wayland.CompositorHost
|
||||
import com.droiddeck.launcher.wayland.WaylandCompositor
|
||||
import java.io.File
|
||||
import com.droiddeck.launcher.session.ComponentsManager
|
||||
import com.droiddeck.launcher.ui.DRAWER_PAGES
|
||||
import com.droiddeck.launcher.ui.SessionDrawerPage
|
||||
import com.droiddeck.launcher.ui.sessionDrawerPages
|
||||
import com.droiddeck.launcher.ui.step
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
@@ -211,7 +213,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
private var closeAtOnce = 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)
|
||||
private var drawerPage by mutableStateOf(SessionDrawerPage.CONTROLLER)
|
||||
private var drawerControllerActive by mutableStateOf(false)
|
||||
private var backActionsInverted by mutableStateOf(false)
|
||||
/** The on-screen PC keyboard (ui/PcKeyboard): real key presses, Esc and F1 included. */
|
||||
@@ -231,7 +233,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
private var fexPreset by mutableStateOf("")
|
||||
private var suspendPolicy by mutableStateOf(SessionPrefs.SUSPEND_MANUAL)
|
||||
private var oscMode by mutableStateOf(SessionPrefs.OSC_AUTO)
|
||||
private var shapeMode by mutableStateOf(SessionPrefs.SHAPE_AUTO)
|
||||
private var onScreenButtonsVisible by mutableStateOf(false)
|
||||
private var secondScreenMode by mutableStateOf(SessionState.secondScreenMode)
|
||||
private var secondScreenDisplays by mutableStateOf<List<SecondScreenDisplay>>(emptyList())
|
||||
private var selectedSecondScreenDisplay by mutableStateOf(SessionState.secondScreenDisplay)
|
||||
@@ -481,9 +483,9 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
effects = effects, textureAnisotropy = textureAnisotropy, textureLodBias = textureLodBias,
|
||||
frameGen = frameGen,
|
||||
lossless = lossless,
|
||||
oscMode = oscMode, suspendPolicy = suspendPolicy, touchMode = touchMode,
|
||||
oscMode = oscMode, onScreenButtonsVisible = onScreenButtonsVisible, suspendPolicy = suspendPolicy, touchMode = touchMode,
|
||||
touchAuto = getString(if (usingTouchpad()) R.string.session_touch_auto_touchpad else R.string.session_touch_auto_direct),
|
||||
shapeMode = shapeMode, fexPreset = fexPreset,
|
||||
fexPreset = fexPreset,
|
||||
secondScreenMode = secondScreenMode,
|
||||
secondScreenDisplays = secondScreenDisplays,
|
||||
selectedSecondScreenDisplay = selectedSecondScreenDisplay,
|
||||
@@ -535,7 +537,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
SessionService.suspendPolicyChanged(this@SessionActivity)
|
||||
},
|
||||
onTouch = { v -> SessionPrefs.setTouchMode(this@SessionActivity, v); readPrefs() },
|
||||
onShape = { v -> SessionPrefs.setShapeMode(this@SessionActivity, v); readPrefs() },
|
||||
onFexPreset = { v -> SessionPrefs.setFexPreset(this@SessionActivity, v); readPrefs() },
|
||||
onSecondScreenMode = ::selectSecondScreenMode,
|
||||
onSecondScreenDisplay = ::selectSecondScreenDisplay,
|
||||
@@ -769,7 +770,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
fexPreset = SessionPrefs.fexPreset(this)
|
||||
suspendPolicy = SessionPrefs.suspendPolicy(this, SessionState.mode)
|
||||
oscMode = SessionPrefs.oscMode(this)
|
||||
shapeMode = SessionPrefs.shapeMode(this)
|
||||
backActionsInverted = SessionPrefs.backActionsInverted(this)
|
||||
}
|
||||
|
||||
@@ -1043,38 +1043,15 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
|
||||
private fun outputSize(): Pair<Int, Int> {
|
||||
val bounds = panelBounds()
|
||||
val panelW = maxOf(bounds.width(), bounds.height()).toFloat()
|
||||
val panelH = minOf(bounds.width(), bounds.height()).toFloat()
|
||||
// Never narrower than 16:9. A foldable's inner panel is nearly square, and a game handed a
|
||||
// square display draws for the frame it was made for and cuts the sides off itself.
|
||||
// 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.
|
||||
// "Match screen" 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 }
|
||||
// An emulator whose frames cost next to nothing (Library.drawsAtPanel) gets the panel,
|
||||
// unless the user chose a resolution for the mode.
|
||||
val panel = com.droiddeck.launcher.session.SessionDisplay.panelSize(this)
|
||||
val program = intent.getStringExtra(SessionService.EXTRA_PROGRAM)
|
||||
val cap = if (intent.getStringExtra(SessionService.EXTRA_MODE) == SessionService.MODE_RUN &&
|
||||
Library.drawsAtPanel(program) && !SessionPrefs.resolutionChosen(this, mode)) 0
|
||||
else SessionPrefs.resolutionCap(this, mode)
|
||||
val height = (if (cap <= 0) panelH else minOf(panelH, cap.toFloat())).toInt()
|
||||
val width = (height * aspect).toInt()
|
||||
// Odd sizes upset the scaler; both dimensions even is what every mode here would be.
|
||||
return Pair(width and 1.inv(), height and 1.inv())
|
||||
// Lightweight emulators keep their panel-sized default until a resolution is chosen.
|
||||
val choice = if (intent.getStringExtra(SessionService.EXTRA_MODE) == SessionService.MODE_RUN &&
|
||||
Library.drawsAtPanel(program) && !SessionPrefs.resolutionChosen(this, mode))
|
||||
com.droiddeck.launcher.session.SessionDisplay.MATCH_SCREEN
|
||||
else SessionPrefs.resolutionChoice(this, mode, panel)
|
||||
return com.droiddeck.launcher.session.SessionDisplay.resolveChoice(panel, choice)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1293,7 +1270,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
if (drawerOpen) {
|
||||
if (fromController && (event.keyCode == KeyEvent.KEYCODE_BUTTON_L1 || event.keyCode == KeyEvent.KEYCODE_BUTTON_R1)) {
|
||||
if (event.action == KeyEvent.ACTION_DOWN && event.repeatCount == 0) {
|
||||
drawerPage = (drawerPage + if (event.keyCode == KeyEvent.KEYCODE_BUTTON_R1) 1 else DRAWER_PAGES - 1) % DRAWER_PAGES
|
||||
drawerPage = sessionDrawerPages(SessionState.mode == SessionService.MODE_STEAM && secondScreenDisplays.isNotEmpty())
|
||||
.step(drawerPage, if (event.keyCode == KeyEvent.KEYCODE_BUTTON_R1) 1 else -1)
|
||||
releaseDrawerDirection()
|
||||
}
|
||||
return true
|
||||
@@ -1746,6 +1724,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
?: SessionPrefs.oscMode(this)
|
||||
val controls = onScreenControls ?: return
|
||||
if (pipUi) {
|
||||
onScreenButtonsVisible = false
|
||||
controls.releaseAll()
|
||||
controls.visibility = View.GONE
|
||||
return
|
||||
@@ -1761,6 +1740,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
// started from the rail, or Steam, gets it; the drawer turns it on anywhere.
|
||||
else -> !PadBridge.anyControllerConnected() && SessionState.mode != SessionService.MODE_DESKTOP
|
||||
}
|
||||
onScreenButtonsVisible = show
|
||||
if (show == (controls.visibility == View.VISIBLE)) return
|
||||
if (!show) controls.releaseAll()
|
||||
controls.visibility = if (show) View.VISIBLE else View.GONE
|
||||
|
||||
@@ -108,9 +108,9 @@ object DeviceReport {
|
||||
h("Display")
|
||||
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")
|
||||
val panelSize = com.droiddeck.launcher.session.SessionDisplay.panelSize(context)
|
||||
k("Resolution (Steam)", SessionPrefs.resolutionChoice(context, SessionService.MODE_STEAM, panelSize))
|
||||
k("Resolution (Desktop)", SessionPrefs.resolutionChoice(context, SessionService.MODE_DESKTOP, panelSize))
|
||||
k("Foldable", context.packageManager.hasSystemFeature("android.hardware.sensor.hinge_angle"))
|
||||
|
||||
h("Drivers")
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.droiddeck.launcher.session
|
||||
|
||||
import android.content.Context
|
||||
import android.view.WindowManager
|
||||
|
||||
/** The virtual display advertised to the guest, before it is fitted onto the Android panel. */
|
||||
object SessionDisplay {
|
||||
const val MATCH_SCREEN = "screen"
|
||||
const val DEFAULT_RESOLUTION = "1280x720"
|
||||
|
||||
fun screenSize(panel: Pair<Int, Int>): Pair<Int, Int> =
|
||||
(maxOf(panel.first, panel.second) and 1.inv()) to (minOf(panel.first, panel.second) and 1.inv())
|
||||
|
||||
fun resolveChoice(panel: Pair<Int, Int>, choice: String): Pair<Int, Int> =
|
||||
if (choice == MATCH_SCREEN) screenSize(panel) else {
|
||||
// Legacy displays may exceed the custom dialog's size limit. Keep their exact size.
|
||||
val parts = choice.split('x').map { it.toIntOrNull() }
|
||||
if (parts.size == 2 && parts.all { it != null && it > 0 }) parts[0]!! to parts[1]!!
|
||||
else 1280 to 720
|
||||
}
|
||||
|
||||
/** Fixed dimensions and the current panel, with no duplicate panel-size preset. */
|
||||
fun resolutionOptions(panel: Pair<Int, Int>): List<String> =
|
||||
listOf(DEFAULT_RESOLUTION, "1600x900", "1920x1080")
|
||||
.filter { resolveChoice(panel, it) != screenSize(panel) } + MATCH_SCREEN
|
||||
|
||||
fun panelSize(context: Context): Pair<Int, Int> {
|
||||
val manager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
||||
@Suppress("DEPRECATION")
|
||||
val mode = manager.defaultDisplay.mode
|
||||
return maxOf(mode.physicalWidth, mode.physicalHeight) to minOf(mode.physicalWidth, mode.physicalHeight)
|
||||
}
|
||||
|
||||
/** Interpret an older cap/aspect combination without altering the saved picture size. */
|
||||
fun resolve(panel: Pair<Int, Int>, cap: Int, shape: String, custom: Pair<Int, Int>? = null): Pair<Int, Int> {
|
||||
custom?.let { return it }
|
||||
val panelW = maxOf(panel.first, panel.second).toFloat()
|
||||
val panelH = minOf(panel.first, panel.second).toFloat().coerceAtLeast(1f)
|
||||
// Auto retains wide panels but floors squarer ones at 16:9 for game compatibility.
|
||||
// Match screen removes that floor; fixed 16:9 also keeps foldable sessions stable.
|
||||
val aspect = when (shape) {
|
||||
SessionPrefs.SHAPE_WIDE -> 16f / 9f
|
||||
SessionPrefs.SHAPE_EXACT -> panelW / panelH
|
||||
else -> maxOf(panelW / panelH, 16f / 9f)
|
||||
}
|
||||
val height = (if (cap <= 0) panelH else minOf(panelH, cap.toFloat())).toInt()
|
||||
return ((height * aspect).toInt() and 1.inv()) to (height and 1.inv())
|
||||
}
|
||||
|
||||
/** Mirrors gamescope's even-width 16:9 calculation, including near-16:9 rounding. */
|
||||
fun canStretch16x9(size: Pair<Int, Int>): Boolean =
|
||||
(size.second * 16 + 4) / 9 / 2 * 2 > size.first
|
||||
}
|
||||
@@ -92,37 +92,16 @@ object SessionPrefs {
|
||||
const val SHAPE_WIDE = "16:9"
|
||||
const val SHAPE_EXACT = "exact"
|
||||
|
||||
/** The choices the settings offer, in order. */
|
||||
val shapeChoices = listOf(
|
||||
SHAPE_AUTO to "Auto (16:9+)",
|
||||
SHAPE_EXACT to "Match screen",
|
||||
SHAPE_WIDE to "Always 16:9",
|
||||
)
|
||||
|
||||
/** [shapeChoices] labelled in the app's language. */
|
||||
fun shapeChoices(context: Context): List<Pair<String, String>> = shapeChoices.map { (mode, label) ->
|
||||
mode to when (mode) {
|
||||
SHAPE_AUTO -> context.getString(R.string.sprefs_shape_auto)
|
||||
SHAPE_EXACT -> context.getString(R.string.sprefs_shape_exact)
|
||||
SHAPE_WIDE -> context.getString(R.string.sprefs_shape_wide)
|
||||
else -> label
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
fun shapeMode(context: Context): String =
|
||||
private fun shapeMode(context: Context): String =
|
||||
prefs(context).getString("shape", null)
|
||||
?: if (context.packageManager.hasSystemFeature("android.hardware.sensor.hinge_angle")) SHAPE_WIDE else SHAPE_AUTO
|
||||
|
||||
fun setShapeMode(context: Context, mode: String) {
|
||||
prefs(context).edit().putString("shape", mode).apply()
|
||||
}
|
||||
|
||||
fun oscMode(context: Context): String = prefs(context).getString("osc", OSC_AUTO) ?: OSC_AUTO
|
||||
|
||||
fun setOscMode(context: Context, mode: String) {
|
||||
@@ -526,11 +505,33 @@ object SessionPrefs {
|
||||
* and the emulators under it get the same GPU headroom. Read once, when the session's display
|
||||
* is sized; a cap the user chose wins over the default.
|
||||
*/
|
||||
fun resolutionCap(context: Context, mode: String): Int = prefs(context).getInt("resolutionCap.$mode", defaultResolutionCap(mode))
|
||||
private fun resolutionCap(context: Context, mode: String): Int = prefs(context).getInt("resolutionCap.$mode", 720)
|
||||
|
||||
/** Whether the user chose the mode's resolution (a cap or a custom size) rather than the default. */
|
||||
fun resolutionChosen(context: Context, mode: String): Boolean =
|
||||
prefs(context).contains("resolutionCap.$mode") || customResolution(context, mode) != null
|
||||
prefs(context).contains("displayResolution.$mode") ||
|
||||
prefs(context).contains("resolutionCap.$mode") || customResolution(context, mode) != null
|
||||
|
||||
/** One per-mode choice now owns both dimensions; old caps/shapes are read only for migration. */
|
||||
fun resolutionChoice(context: Context, mode: String, panel: Pair<Int, Int>): String {
|
||||
val saved = prefs(context)
|
||||
saved.getString("displayResolution.$mode", null)?.let { value ->
|
||||
if (value == SessionDisplay.MATCH_SCREEN) return value
|
||||
parseResolution(value)?.let { return "${it.first}x${it.second}" }
|
||||
}
|
||||
if (!resolutionChosen(context, mode) && !saved.contains("shape")) return SessionDisplay.DEFAULT_RESOLUTION
|
||||
val legacy = SessionDisplay.resolve(panel, resolutionCap(context, mode), shapeMode(context), customResolution(context, mode))
|
||||
return if (legacy == SessionDisplay.screenSize(panel)) SessionDisplay.MATCH_SCREEN
|
||||
else "${legacy.first}x${legacy.second}"
|
||||
}
|
||||
|
||||
fun setResolutionChoice(context: Context, mode: String, choice: String) {
|
||||
val value = if (choice == SessionDisplay.MATCH_SCREEN) choice else {
|
||||
val size = requireNotNull(parseResolution(choice)) { "Invalid resolution" }
|
||||
"${size.first}x${size.second}"
|
||||
}
|
||||
prefs(context).edit().putString("displayResolution.$mode", value).apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* The FEXCore preset for the games the client launches (core/FexPreset ids); "" = FEX's defaults.
|
||||
@@ -610,25 +611,13 @@ object SessionPrefs {
|
||||
prefs(context).edit().putString("theme", id).apply()
|
||||
}
|
||||
|
||||
/** What a mode gets when nothing was chosen. */
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
fun defaultResolutionCap(mode: String): Int = 720
|
||||
|
||||
fun setResolutionCap(context: Context, mode: String, cap: Int) {
|
||||
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>? =
|
||||
private 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
|
||||
@@ -687,30 +676,37 @@ object SessionPrefs {
|
||||
|
||||
/**
|
||||
* How the compositor resizes the session onto the panel (WaylandCompositor.nativeSetUpscaler's
|
||||
* modes): Off and Linear both filter bilinearly, Nearest keeps pixels square for 2D and old
|
||||
* titles, the rest sharpen where the picture is enlarged; FSR (fit) rounds the picture to
|
||||
* FSR's preferred size first. Sharpen only works at any size.
|
||||
* modes). Linear is the default; Nearest preserves hard pixel edges. Spatial filters work
|
||||
* when enlarged; Sharpen only works at any size. The old Off/Linear and FSR/FSR Fit pairs
|
||||
* are equivalent on Wayland, so saved aliases resolve to one choice.
|
||||
*/
|
||||
val upscalerChoices = listOf(
|
||||
0 to "Off", 1 to "Linear", 2 to "Nearest", 4 to "AMD FSR 1", 5 to "AMD FSR 1 (fit)", 3 to "Snapdragon GSR",
|
||||
0 to "Linear", 2 to "Nearest", 4 to "AMD FSR 1", 3 to "Snapdragon GSR",
|
||||
8 to "Snapdragon GSR (quality)", 7 to "NVIDIA NIS", 6 to "Sharpen only",
|
||||
)
|
||||
|
||||
/** The [upscalerChoices] labels that are words rather than product names. */
|
||||
private val upscalerLabels = mapOf(
|
||||
0 to R.string.frame_gen_off, 1 to R.string.sprefs_upscaler_linear, 2 to R.string.sprefs_upscaler_nearest,
|
||||
5 to R.string.sprefs_upscaler_fsr_fit, 8 to R.string.sprefs_upscaler_gsr_quality, 6 to R.string.sprefs_upscaler_sharpen,
|
||||
0 to R.string.sprefs_upscaler_linear, 2 to R.string.sprefs_upscaler_nearest,
|
||||
8 to R.string.sprefs_upscaler_gsr_quality, 6 to R.string.sprefs_upscaler_sharpen,
|
||||
)
|
||||
|
||||
/** [upscalerChoices] labelled in the app's language; the English list stays for the device report. */
|
||||
fun upscalerChoices(context: Context): List<Pair<Int, String>> =
|
||||
upscalerChoices.map { (mode, label) -> mode to (upscalerLabels[mode]?.let(context::getString) ?: label) }
|
||||
|
||||
fun upscaler(context: Context): Int =
|
||||
prefs(context).getInt("upscaler", 0).takeIf { m -> upscalerChoices.any { it.first == m } } ?: 0
|
||||
fun canonicalUpscaler(mode: Int): Int = when (mode) {
|
||||
1 -> 0
|
||||
5 -> 4
|
||||
else -> mode.takeIf { m -> upscalerChoices.any { it.first == m } } ?: 0
|
||||
}
|
||||
|
||||
fun upscalerHasSharpness(mode: Int): Boolean = canonicalUpscaler(mode) in 3..8
|
||||
|
||||
fun upscaler(context: Context): Int = canonicalUpscaler(prefs(context).getInt("upscaler", 0))
|
||||
|
||||
fun setUpscaler(context: Context, mode: Int) {
|
||||
prefs(context).edit().putInt("upscaler", mode).apply()
|
||||
prefs(context).edit().putInt("upscaler", canonicalUpscaler(mode)).apply()
|
||||
}
|
||||
|
||||
fun upscaleSharpness(context: Context): Int = prefs(context).getInt("upscaleSharpness", 75).coerceIn(0, 100)
|
||||
|
||||
@@ -52,13 +52,13 @@ import kotlinx.coroutines.withContext
|
||||
// dialog, with each variable's actions in a menu off its value as everywhere else.
|
||||
|
||||
@Composable
|
||||
fun GameEnvironmentRow(modifier: Modifier = Modifier) {
|
||||
fun GameEnvironmentRow(modifier: Modifier = Modifier, showHint: Boolean = true) {
|
||||
var open by remember { mutableStateOf(false) }
|
||||
// Opened with a pad, the editor starts with focus in it; the dialog is a window of its own and
|
||||
// would otherwise give the pad nothing to move from.
|
||||
var byPad by remember { mutableStateOf(false) }
|
||||
val inputMode = LocalInputModeManager.current
|
||||
SettingsRow(stringResource(R.string.game_env_title), stringResource(R.string.game_env_hint)) {
|
||||
SettingsRow(stringResource(R.string.game_env_title), if (showHint) stringResource(R.string.game_env_hint) else null) {
|
||||
SecondaryButton(stringResource(R.string.game_env_edit), modifier = modifier) {
|
||||
byPad = inputMode.inputMode == InputMode.Keyboard
|
||||
open = true
|
||||
|
||||
@@ -54,10 +54,8 @@ class DownloadRow(val key: String, val label: String, val detail: String, val pr
|
||||
|
||||
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 resolution: String,
|
||||
val panelSize: Pair<Int, Int> = 1280 to 720,
|
||||
val hdr: Boolean,
|
||||
val hdrReason: String?,
|
||||
/** The GPU drivers in use, as the row that opens them on the Components page says it. */
|
||||
@@ -86,7 +84,7 @@ class ModeSettings(
|
||||
val fexPreset: String? = null,
|
||||
/** Steam only: SessionPrefs.SYNC_* chosen for Proton games; null outside Steam. */
|
||||
val syncBackend: String? = null,
|
||||
/** Steam only: games are stretched to fill the screen (null = not a Steam page). */
|
||||
/** Steam only: forces game windows fullscreen (null = not a Steam page). */
|
||||
val forceFullscreen: Boolean? = null,
|
||||
val stretch16x9: Boolean? = null,
|
||||
/** Steam only: the client branch forced on the command line. */
|
||||
@@ -127,10 +125,7 @@ class ModeSettings(
|
||||
class AddedGameRow(val folderPath: String, val folderName: String, val exePath: String, val exeName: String, val candidates: List<Pair<String, String>>)
|
||||
|
||||
class ModeSettingsActions(
|
||||
val onResolution: (Int) -> Unit,
|
||||
/** Null clears it. */
|
||||
val onCustomResolution: (Pair<Int, Int>?) -> Unit = {},
|
||||
val onShape: (String) -> Unit,
|
||||
val onResolution: (String) -> Unit,
|
||||
val onHdr: (Boolean) -> Unit,
|
||||
/** Opens the GPU drivers on the Components page: they are shared by every session. */
|
||||
val onGpuDrivers: () -> Unit = {},
|
||||
@@ -174,139 +169,156 @@ class ModeSettingsActions(
|
||||
val onDismiss: () -> Unit,
|
||||
)
|
||||
|
||||
private enum class SteamSettingsTab(val label: Int) {
|
||||
DISPLAY(R.string.drawer_page_display),
|
||||
CONTROLS(R.string.drawer_page_controls),
|
||||
GAMES(R.string.game_settings_title),
|
||||
AUDIO(R.string.mode_audio),
|
||||
SESSION(R.string.mode_session),
|
||||
STEAM(R.string.mode_tab_steam),
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
|
||||
val steam = s.mode == SessionService.MODE_STEAM
|
||||
val host = rememberMenuHost()
|
||||
var confirmDeckyRemoval by remember { mutableStateOf(false) }
|
||||
var explainWifiDiscovery by remember { mutableStateOf(false) }
|
||||
val pageScroll = androidx.compose.foundation.rememberScrollState()
|
||||
var tabIndex by androidx.compose.runtime.saveable.rememberSaveable(s.mode) { mutableStateOf(0) }
|
||||
val tabs = SteamSettingsTab.entries
|
||||
val tab = tabs[tabIndex]
|
||||
val scrolls = tabs.map { androidx.compose.foundation.rememberScrollState() }
|
||||
val tabFocus = remember { tabs.map { androidx.compose.ui.focus.FocusRequester() } }
|
||||
val firstChip = remember { androidx.compose.ui.focus.FocusRequester() }
|
||||
androidx.compose.runtime.LaunchedEffect(Unit) {
|
||||
// One frame first: the box has to be laid out before it can take focus. Opened from the
|
||||
// cog, focus starts on the first control (Resolution) so the d-pad works at once.
|
||||
fun selectTab(index: Int) { host.open = null; tabIndex = index }
|
||||
androidx.compose.runtime.LaunchedEffect(steam, tabIndex) {
|
||||
androidx.compose.runtime.withFrameNanos { }
|
||||
runCatching { firstChip.requestFocus() }
|
||||
runCatching {
|
||||
if (!steam || tab == SteamSettingsTab.DISPLAY) firstChip.requestFocus()
|
||||
else tabFocus[tabIndex].requestFocus()
|
||||
}
|
||||
}
|
||||
SettingsPage(
|
||||
host,
|
||||
title = if (steam) stringResource(R.string.mode_steam_title) else stringResource(R.string.mode_desktop_title),
|
||||
onBack = a.onDismiss,
|
||||
scroll = pageScroll,
|
||||
scroll = scrolls[tabIndex],
|
||||
modifier = if (steam) Modifier.bumpers(
|
||||
onPrevious = { selectTab((tabIndex + tabs.size - 1) % tabs.size) },
|
||||
onNext = { selectTab((tabIndex + 1) % tabs.size) },
|
||||
) else Modifier,
|
||||
header = if (steam) ({
|
||||
TabStrip(tabs.map { stringResource(it.label) }, tabIndex, ::selectTab,
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 12.dp), focusRequesters = tabFocus)
|
||||
}) else null,
|
||||
) {
|
||||
SettingsGroup(stringResource(R.string.mode_display)) {
|
||||
val default = SessionPrefs.defaultResolutionCap(s.mode)
|
||||
var editCustom by remember { mutableStateOf(false) }
|
||||
val custom = s.customResolution
|
||||
ChoiceRow(
|
||||
host, "res", stringResource(R.string.mode_resolution), stringResource(R.string.common_applies_next_session),
|
||||
listOf(720 to stringResource(R.string.mode_res_720), 900 to stringResource(R.string.mode_res_900), 1080 to stringResource(R.string.mode_res_1080), 0 to stringResource(R.string.mode_res_native))
|
||||
.map { (cap, label) -> cap to (if (cap == default) stringResource(R.string.mode_res_default, label) else label) } +
|
||||
(CUSTOM to (custom?.let { stringResource(R.string.mode_res_custom_value, it.first, it.second) } ?: stringResource(R.string.mode_res_custom))),
|
||||
if (custom != null) CUSTOM else s.resolutionCap, note = stringResource(R.string.mode_res_note),
|
||||
chipModifier = androidx.compose.ui.Modifier.focusRequester(firstChip),
|
||||
onPick = { v -> if (v == CUSTOM) editCustom = true else { a.onCustomResolution(null); a.onResolution(v) } },
|
||||
)
|
||||
ChoiceRow(
|
||||
host, "shape", stringResource(R.string.mode_ratio),
|
||||
if (custom != null) stringResource(R.string.mode_ratio_custom) else stringResource(R.string.mode_ratio_auto),
|
||||
com.droiddeck.launcher.session.SessionPrefs.shapeChoices(LocalContext.current), s.shapeMode, enabled = custom == null, onPick = a.onShape,
|
||||
)
|
||||
ChoiceRow(
|
||||
host, "fps", stringResource(R.string.mode_fps), stringResource(R.string.common_applies_next_session),
|
||||
com.droiddeck.launcher.session.SessionPrefs.fpsLimitChoices(LocalContext.current), s.fpsLimit,
|
||||
note = stringResource(R.string.mode_fps_note),
|
||||
onPick = a.onFpsLimit,
|
||||
)
|
||||
ChoiceRow(
|
||||
host, "upscaler", stringResource(R.string.drawer_scaling), stringResource(R.string.mode_upscaler_hint),
|
||||
com.droiddeck.launcher.session.SessionPrefs.upscalerChoices(LocalContext.current), s.upscaler,
|
||||
note = stringResource(R.string.mode_upscaler_note),
|
||||
onPick = a.onUpscaler,
|
||||
)
|
||||
SliderRow(
|
||||
stringResource(R.string.drawer_scaling_sharpness), null, s.upscaleSharpness, 0..100, step = 5,
|
||||
enabled = s.upscaler != 0, format = { "$it%" }, onChange = a.onUpscaleSharpness,
|
||||
)
|
||||
if (editCustom) CustomResolutionDialog(
|
||||
initial = custom,
|
||||
onSave = { size -> editCustom = false; a.onCustomResolution(size) },
|
||||
onDismiss = { editCustom = false },
|
||||
)
|
||||
}
|
||||
SettingsGroup(stringResource(R.string.mode_hdr)) {
|
||||
ToggleRow(
|
||||
host, "hdr", stringResource(R.string.mode_hdr10),
|
||||
s.hdrReason?.let { stringResource(R.string.mode_hdr_unavailable, it) }
|
||||
?: stringResource(R.string.mode_hdr_restart),
|
||||
checked = s.hdr && s.hdrReason == null, enabled = s.hdrReason == null, onChange = a.onHdr,
|
||||
)
|
||||
}
|
||||
SettingsGroup(stringResource(R.string.mode_drivers)) {
|
||||
SettingsRow(stringResource(R.string.mode_gpu_drivers), stringResource(R.string.mode_gpu_drivers_hint)) {
|
||||
ValueChip(s.gpuDrivers ?: stringResource(R.string.common_auto), open = false) { a.onGpuDrivers() }
|
||||
if (!steam || tab == SteamSettingsTab.DISPLAY) {
|
||||
val displaySize = com.droiddeck.launcher.session.SessionDisplay.resolveChoice(s.panelSize, s.resolution)
|
||||
SettingsGroup(stringResource(R.string.display_session)) {
|
||||
ResolutionRow(host, s.resolution, s.panelSize, a.onResolution,
|
||||
chipModifier = Modifier.focusRequester(firstChip))
|
||||
if (s.stretch16x9 != null && (com.droiddeck.launcher.session.SessionDisplay.canStretch16x9(s.panelSize) ||
|
||||
com.droiddeck.launcher.session.SessionDisplay.canStretch16x9(displaySize))) {
|
||||
val canStretch = com.droiddeck.launcher.session.SessionDisplay.canStretch16x9(displaySize)
|
||||
ChoiceRow(host, "game-fit", stringResource(R.string.display_game_fit),
|
||||
stringResource(if (canStretch) R.string.display_game_fit_hint else R.string.display_game_fit_unavailable),
|
||||
listOf(false to stringResource(R.string.display_preserve), true to stringResource(R.string.display_stretch)),
|
||||
s.stretch16x9 && canStretch, enabled = canStretch, onPick = a.onStretch16x9)
|
||||
}
|
||||
}
|
||||
SettingsGroup(stringResource(R.string.display_image_scaling)) {
|
||||
ChoiceRow(
|
||||
host, "upscaler", stringResource(R.string.display_filter), null,
|
||||
SessionPrefs.upscalerChoices(LocalContext.current), s.upscaler,
|
||||
note = stringResource(R.string.display_filter_note), onPick = a.onUpscaler,
|
||||
)
|
||||
if (SessionPrefs.upscalerHasSharpness(s.upscaler)) SliderRow(
|
||||
stringResource(R.string.display_sharpness), null, s.upscaleSharpness, 0..100, step = 5,
|
||||
format = { "$it%" }, onChange = a.onUpscaleSharpness,
|
||||
)
|
||||
}
|
||||
SettingsGroup(stringResource(R.string.display_frame_rate)) {
|
||||
ChoiceRow(host, "fps", stringResource(R.string.mode_fps), stringResource(R.string.common_applies_next_session),
|
||||
SessionPrefs.fpsLimitChoices(LocalContext.current), s.fpsLimit, note = stringResource(R.string.mode_fps_note), onPick = a.onFpsLimit)
|
||||
}
|
||||
SettingsGroup(stringResource(R.string.mode_hdr)) {
|
||||
ToggleRow(
|
||||
host, "hdr", stringResource(R.string.mode_hdr10),
|
||||
s.hdrReason?.let { stringResource(R.string.mode_hdr_unavailable, it) }
|
||||
?: stringResource(R.string.mode_hdr_restart),
|
||||
checked = s.hdr && s.hdrReason == null, enabled = s.hdrReason == null, onChange = a.onHdr,
|
||||
)
|
||||
}
|
||||
SettingsGroup(stringResource(R.string.mode_drivers)) {
|
||||
SettingsRow(stringResource(R.string.mode_gpu_drivers), stringResource(R.string.mode_gpu_drivers_hint)) {
|
||||
ValueChip(s.gpuDrivers ?: stringResource(R.string.common_auto), open = false) { a.onGpuDrivers() }
|
||||
}
|
||||
}
|
||||
}
|
||||
SettingsGroup(if (steam) stringResource(R.string.mode_touch_controls) else stringResource(R.string.mode_touch)) {
|
||||
ChoiceRow(
|
||||
host, "touch", stringResource(R.string.mode_touch), null,
|
||||
listOf(SessionPrefs.TOUCH_AUTO to stringResource(R.string.common_auto), SessionPrefs.TOUCH_PAD to stringResource(R.string.mode_touch_touchpad), SessionPrefs.TOUCH_DIRECT to stringResource(R.string.mode_touch_direct), SessionPrefs.TOUCH_OFF to stringResource(R.string.widgets_off)), s.touchMode,
|
||||
note = stringResource(R.string.mode_touch_note),
|
||||
onPick = a.onTouch,
|
||||
)
|
||||
if (steam && s.oscMode != null) ChoiceRow(
|
||||
host, "osc", stringResource(R.string.mode_osc), null,
|
||||
listOf(
|
||||
SessionPrefs.OSC_AUTO to stringResource(R.string.common_auto),
|
||||
SessionPrefs.OSC_ALWAYS to stringResource(R.string.common_always),
|
||||
SessionPrefs.OSC_STEAM_QAM to stringResource(R.string.mode_osc_qam),
|
||||
SessionPrefs.OSC_NEVER to stringResource(R.string.common_never),
|
||||
), s.oscMode,
|
||||
note = stringResource(R.string.mode_osc_note), onPick = a.onOsc,
|
||||
)
|
||||
if (steam && s.steamController != null) ChoiceRow(
|
||||
host, "controller", stringResource(R.string.mode_controller), stringResource(R.string.mode_controller_hint),
|
||||
listOf(
|
||||
SessionPrefs.CONTROLLER_DECK to stringResource(R.string.mode_controller_deck),
|
||||
SessionPrefs.CONTROLLER_XBOX360 to stringResource(R.string.mode_controller_x360),
|
||||
), s.steamController,
|
||||
note = stringResource(R.string.mode_controller_note),
|
||||
onPick = a.onSteamController,
|
||||
)
|
||||
if (steam) ChoiceRow(
|
||||
host, "back-actions", stringResource(R.string.mode_back), stringResource(SessionPrefs.backActionsOrder(s.backActionsInverted)),
|
||||
listOf(
|
||||
false to stringResource(SessionPrefs.BACK_MENU_THEN_QAM),
|
||||
true to stringResource(SessionPrefs.BACK_QAM_THEN_MENU),
|
||||
), s.backActionsInverted, onPick = a.onBackActionsInverted,
|
||||
)
|
||||
if (!steam || tab == SteamSettingsTab.CONTROLS) {
|
||||
SettingsGroup(if (steam) stringResource(R.string.mode_touch_controls) else stringResource(R.string.mode_touch)) {
|
||||
ChoiceRow(
|
||||
host, "touch", stringResource(R.string.mode_touch), null,
|
||||
listOf(SessionPrefs.TOUCH_AUTO to stringResource(R.string.common_auto), SessionPrefs.TOUCH_PAD to stringResource(R.string.mode_touch_touchpad), SessionPrefs.TOUCH_DIRECT to stringResource(R.string.mode_touch_direct), SessionPrefs.TOUCH_OFF to stringResource(R.string.widgets_off)), s.touchMode,
|
||||
note = stringResource(R.string.mode_touch_note),
|
||||
onPick = a.onTouch,
|
||||
)
|
||||
if (steam && s.oscMode != null) ChoiceRow(
|
||||
host, "osc", stringResource(R.string.mode_osc), null,
|
||||
listOf(
|
||||
SessionPrefs.OSC_AUTO to stringResource(R.string.common_auto),
|
||||
SessionPrefs.OSC_ALWAYS to stringResource(R.string.common_always),
|
||||
SessionPrefs.OSC_STEAM_QAM to stringResource(R.string.mode_osc_qam),
|
||||
SessionPrefs.OSC_NEVER to stringResource(R.string.common_never),
|
||||
), s.oscMode,
|
||||
note = stringResource(R.string.mode_osc_note), onPick = a.onOsc,
|
||||
)
|
||||
if (steam && s.steamController != null) ChoiceRow(
|
||||
host, "controller", stringResource(R.string.mode_controller), stringResource(R.string.mode_controller_hint),
|
||||
listOf(
|
||||
SessionPrefs.CONTROLLER_DECK to stringResource(R.string.mode_controller_deck),
|
||||
SessionPrefs.CONTROLLER_XBOX360 to stringResource(R.string.mode_controller_x360),
|
||||
), s.steamController,
|
||||
note = stringResource(R.string.mode_controller_note),
|
||||
onPick = a.onSteamController,
|
||||
)
|
||||
if (steam) ChoiceRow(
|
||||
host, "back-actions", stringResource(R.string.mode_back), stringResource(SessionPrefs.backActionsOrder(s.backActionsInverted)),
|
||||
listOf(
|
||||
false to stringResource(SessionPrefs.BACK_MENU_THEN_QAM),
|
||||
true to stringResource(SessionPrefs.BACK_QAM_THEN_MENU),
|
||||
), s.backActionsInverted, onPick = a.onBackActionsInverted,
|
||||
)
|
||||
}
|
||||
}
|
||||
SettingsGroup(stringResource(R.string.mode_session)) {
|
||||
ChoiceRow(
|
||||
host, "suspend", stringResource(R.string.mode_suspend),
|
||||
stringResource(R.string.mode_suspend_hint),
|
||||
listOf(
|
||||
SessionPrefs.SUSPEND_AUTO to stringResource(R.string.common_auto),
|
||||
SessionPrefs.SUSPEND_MANUAL to stringResource(R.string.mode_suspend_manual),
|
||||
SessionPrefs.SUSPEND_NEVER to stringResource(R.string.common_never),
|
||||
),
|
||||
s.suspendPolicy,
|
||||
note = stringResource(R.string.mode_suspend_note),
|
||||
onPick = a.onSuspendPolicy,
|
||||
)
|
||||
if (!steam || tab == SteamSettingsTab.SESSION) {
|
||||
SettingsGroup(stringResource(R.string.mode_session)) {
|
||||
ChoiceRow(
|
||||
host, "suspend", stringResource(R.string.mode_suspend),
|
||||
stringResource(R.string.mode_suspend_hint),
|
||||
listOf(
|
||||
SessionPrefs.SUSPEND_AUTO to stringResource(R.string.common_auto),
|
||||
SessionPrefs.SUSPEND_MANUAL to stringResource(R.string.mode_suspend_manual),
|
||||
SessionPrefs.SUSPEND_NEVER to stringResource(R.string.common_never),
|
||||
),
|
||||
s.suspendPolicy,
|
||||
note = stringResource(R.string.mode_suspend_note),
|
||||
onPick = a.onSuspendPolicy,
|
||||
)
|
||||
}
|
||||
if (s.pipSupported) SettingsGroup(stringResource(R.string.pip_title)) {
|
||||
ToggleRow(host, "pip-auto", stringResource(R.string.pip_auto), null,
|
||||
s.pipAutoEnter, onChange = a.onPipAutoEnter)
|
||||
}
|
||||
if (steam) SettingsGroup(stringResource(R.string.mode_startup)) {
|
||||
ToggleRow(
|
||||
host, "steam-startup", stringResource(R.string.mode_steam_startup),
|
||||
stringResource(R.string.mode_steam_startup_hint),
|
||||
s.runSteamAtStartup, onChange = a.onRunSteamAtStartup,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (s.pipSupported) SettingsGroup(stringResource(R.string.pip_title)) {
|
||||
ToggleRow(host, "pip-auto", stringResource(R.string.pip_auto), null,
|
||||
s.pipAutoEnter, onChange = a.onPipAutoEnter)
|
||||
}
|
||||
if (steam) SettingsGroup(stringResource(R.string.mode_startup)) {
|
||||
ToggleRow(
|
||||
host, "steam-startup", stringResource(R.string.mode_steam_startup),
|
||||
stringResource(R.string.mode_steam_startup_hint),
|
||||
s.runSteamAtStartup, onChange = a.onRunSteamAtStartup,
|
||||
)
|
||||
}
|
||||
if (steam) SettingsGroup(stringResource(R.string.mode_decky)) {
|
||||
if (steam && tab == SteamSettingsTab.STEAM) SettingsGroup(stringResource(R.string.mode_decky)) {
|
||||
val updateAvailable = s.deckyInstalled != null && s.deckyLatestRelease != null &&
|
||||
s.deckyInstalled != s.deckyLatestRelease.tag
|
||||
val status = when {
|
||||
@@ -365,7 +377,7 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
|
||||
)
|
||||
}
|
||||
}
|
||||
if (steam && s.steamChannel != null) SettingsGroup(stringResource(R.string.mode_client)) {
|
||||
if (steam && tab == SteamSettingsTab.STEAM && s.steamChannel != null) SettingsGroup(stringResource(R.string.mode_client)) {
|
||||
ToggleRow(
|
||||
host, "steamdeck", stringResource(R.string.mode_deck_mode),
|
||||
stringResource(R.string.mode_deck_mode_hint),
|
||||
@@ -391,7 +403,7 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
|
||||
SecondaryButton(stringResource(R.string.mode_steam_repair_button), enabled = !s.steamRepairQueued, onClick = a.onSteamRepair)
|
||||
}
|
||||
}
|
||||
if (steam && s.wifiDiscovery != null) SettingsGroup(stringResource(R.string.mode_network)) {
|
||||
if (steam && tab == SteamSettingsTab.STEAM && s.wifiDiscovery != null) SettingsGroup(stringResource(R.string.mode_network)) {
|
||||
val hint = when {
|
||||
s.wifiDiscovery && !s.wifiDiscoveryLocation -> stringResource(R.string.mode_wifi_location_off)
|
||||
!s.wifiDiscoveryPermission && s.wifiDiscoveryAsked -> stringResource(R.string.mode_wifi_permission_denied)
|
||||
@@ -410,7 +422,11 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (steam && s.addedGamesDirs != null) SettingsGroup(stringResource(R.string.mode_added_games)) {
|
||||
if ((!steam || tab == SteamSettingsTab.GAMES) && s.forceFullscreen != null) SettingsGroup(stringResource(R.string.display_window_compatibility)) {
|
||||
ToggleRow(host, "fill", stringResource(R.string.display_force_fullscreen),
|
||||
stringResource(R.string.display_force_fullscreen_hint), s.forceFullscreen, onChange = a.onForceFullscreen)
|
||||
}
|
||||
if (steam && tab == SteamSettingsTab.GAMES && s.addedGamesDirs != null) SettingsGroup(stringResource(R.string.mode_added_games)) {
|
||||
for (dir in s.addedGamesDirs) {
|
||||
val n = s.addedGames.count { it.folderPath.startsWith("$dir/") }
|
||||
ActionRow(
|
||||
@@ -435,7 +451,7 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
|
||||
onPick = { path -> if (path == "__pick__") a.onPickAddedGameExe(g.folderPath) else a.onAddedGameExe(g.folderPath, path) },
|
||||
)
|
||||
}
|
||||
if (steam && s.fexPreset != null) SettingsGroup(stringResource(R.string.game_settings_title)) {
|
||||
if (steam && tab == SteamSettingsTab.GAMES && s.fexPreset != null) SettingsGroup(stringResource(R.string.game_settings_title)) {
|
||||
if (s.syncBackend != null) SettingsRow(
|
||||
stringResource(R.string.sync_backend_title),
|
||||
stringResource(R.string.sync_backend_hint),
|
||||
@@ -456,18 +472,9 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
|
||||
note = stringResource(FexPreset.byId(s.fexPreset).detail), onPick = a.onFexPreset,
|
||||
)
|
||||
GameEnvironmentRow()
|
||||
if (s.forceFullscreen != null) ToggleRow(
|
||||
host, "fill", stringResource(R.string.mode_fill),
|
||||
stringResource(R.string.mode_fill_hint),
|
||||
s.forceFullscreen, onChange = a.onForceFullscreen,
|
||||
)
|
||||
if (s.stretch16x9 != null) ToggleRow(
|
||||
host, "stretch169", stringResource(R.string.mode_stretch169),
|
||||
stringResource(R.string.mode_stretch169_hint),
|
||||
s.stretch16x9, onChange = a.onStretch16x9,
|
||||
)
|
||||
|
||||
}
|
||||
if (steam && s.directAudio != null && s.mic != null) SettingsGroup(stringResource(R.string.mode_audio)) {
|
||||
if (steam && tab == SteamSettingsTab.AUDIO && s.directAudio != null && s.mic != null) SettingsGroup(stringResource(R.string.mode_audio)) {
|
||||
ToggleRow(host, "da", stringResource(R.string.mode_directaudio), stringResource(R.string.mode_directaudio_hint), s.directAudio, onChange = a.onDirectAudio)
|
||||
ChoiceRow(
|
||||
host, "clientAudio", stringResource(R.string.mode_client_audio), stringResource(R.string.mode_client_audio_hint),
|
||||
@@ -476,7 +483,7 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
|
||||
)
|
||||
ToggleRow(host, "mic", stringResource(R.string.mode_mic), stringResource(R.string.mode_mic_hint), s.mic, onChange = a.onMic)
|
||||
}
|
||||
if (steam && s.gameStorage != null) SettingsGroup(stringResource(R.string.mode_storage)) {
|
||||
if (steam && tab == SteamSettingsTab.GAMES && s.gameStorage != null) SettingsGroup(stringResource(R.string.mode_storage)) {
|
||||
val custom = s.gameStorage.isNotEmpty() && s.gameStorage != "off" && s.storageOptions.none { it.path == s.gameStorage }
|
||||
// Each choice: its path, its menu line, the chip's shorter line, and the label it is saved with.
|
||||
class StorageChoice(val path: String, val label: String, val chip: String, val saved: String)
|
||||
@@ -551,13 +558,10 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
|
||||
)
|
||||
}
|
||||
|
||||
/** 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) {
|
||||
internal 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")
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.droiddeck.launcher.ui
|
||||
|
||||
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.res.stringResource
|
||||
import com.droiddeck.launcher.R
|
||||
import com.droiddeck.launcher.session.SessionDisplay
|
||||
|
||||
/** Pre-session resolution choice, applied when the session starts. */
|
||||
@Composable
|
||||
fun ResolutionRow(
|
||||
host: MenuHost, choice: String, panel: Pair<Int, Int>, onPick: (String) -> Unit,
|
||||
chipModifier: Modifier = Modifier,
|
||||
) {
|
||||
val custom = "custom"
|
||||
val size = SessionDisplay.resolveChoice(panel, choice)
|
||||
val screen = SessionDisplay.screenSize(panel)
|
||||
val choices = SessionDisplay.resolutionOptions(panel)
|
||||
val selected = if (size == screen) SessionDisplay.MATCH_SCREEN else choice
|
||||
var editCustom by remember { mutableStateOf(false) }
|
||||
val options = choices.map { value ->
|
||||
val dimensions = SessionDisplay.resolveChoice(panel, value)
|
||||
value to if (value == SessionDisplay.MATCH_SCREEN) stringResource(R.string.display_match_screen, screen.first, screen.second)
|
||||
else if (value == SessionDisplay.DEFAULT_RESOLUTION) stringResource(R.string.mode_res_default, "${dimensions.first}×${dimensions.second}")
|
||||
else "${dimensions.first}×${dimensions.second}"
|
||||
} + (custom to if (selected in choices) stringResource(R.string.mode_res_custom)
|
||||
else stringResource(R.string.mode_res_custom_value, size.first, size.second))
|
||||
ChoiceRow(host, "resolution", stringResource(R.string.display_resolution),
|
||||
stringResource(R.string.common_applies_next_session),
|
||||
options, if (selected in choices) selected else custom, chipModifier = chipModifier,
|
||||
onPick = { value -> if (value == custom) editCustom = true else onPick(value) })
|
||||
if (editCustom) CustomResolutionDialog(size,
|
||||
onSave = { value -> editCustom = false; onPick("${value.first}x${value.second}") },
|
||||
onDismiss = { editCustom = false })
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.droiddeck.launcher.ui
|
||||
|
||||
/** Stable page identities keep focus and scroll positions when a secondary display comes and goes. */
|
||||
internal enum class SessionDrawerPage(val firstControl: String) {
|
||||
CONTROLLER("touch"),
|
||||
SECOND_SCREEN("second-screen:none"),
|
||||
DISPLAY("hud"),
|
||||
EFFECTS("look"),
|
||||
GAMES("anisotropy"),
|
||||
SESSION("suspend"),
|
||||
}
|
||||
|
||||
internal fun sessionDrawerPages(hasSecondScreen: Boolean): List<SessionDrawerPage> =
|
||||
SessionDrawerPage.entries.filter { hasSecondScreen || it != SessionDrawerPage.SECOND_SCREEN }
|
||||
|
||||
internal fun List<SessionDrawerPage>.step(current: SessionDrawerPage, direction: Int): SessionDrawerPage {
|
||||
val index = indexOf(current).coerceAtLeast(0)
|
||||
return this[Math.floorMod(index + direction, size)]
|
||||
}
|
||||
@@ -71,6 +71,9 @@ import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.PlatformTextStyle
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.LineHeightStyle
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
|
||||
// The session's loading screen, its paused screen and the HUD text style.
|
||||
@@ -267,7 +270,11 @@ private fun CancelHint(onCancel: () -> Unit, modifier: Modifier = Modifier) {
|
||||
.padding(horizontal = 12.dp),
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center, modifier = Modifier.size(20.dp).clip(CircleShape).background(colors.onBackground)) {
|
||||
Text("B", fontSize = 12.sp, fontWeight = FontWeight.Bold, color = colors.background)
|
||||
Text("B", color = colors.background, style = TextStyle(
|
||||
fontSize = 12.sp, lineHeight = 12.sp, fontWeight = FontWeight.Bold,
|
||||
platformStyle = PlatformTextStyle(includeFontPadding = false),
|
||||
lineHeightStyle = LineHeightStyle(LineHeightStyle.Alignment.Center, LineHeightStyle.Trim.Both),
|
||||
))
|
||||
}
|
||||
Text(stringResource(R.string.common_cancel), fontSize = 13.sp, color = colors.onSurfaceVariant)
|
||||
}
|
||||
|
||||
@@ -28,15 +28,24 @@ import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.material.icons.outlined.AutoFixHigh
|
||||
import androidx.compose.material.icons.outlined.ExpandMore
|
||||
import androidx.compose.material.icons.outlined.Home
|
||||
import androidx.compose.material.icons.outlined.MoreHoriz
|
||||
import androidx.compose.material.icons.outlined.PowerSettingsNew
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.material.icons.outlined.DesktopWindows
|
||||
import androidx.compose.material.icons.outlined.Layers
|
||||
import androidx.compose.material.icons.outlined.ScreenShare
|
||||
import androidx.compose.material.icons.outlined.PictureInPictureAlt
|
||||
import androidx.compose.material.icons.automirrored.outlined.ExitToApp
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material.icons.outlined.SportsEsports
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.RadioButtonDefaults
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.selection.selectableGroup
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import android.view.Display
|
||||
import androidx.activity.compose.BackHandler
|
||||
@@ -79,6 +88,8 @@ import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.semantics.stateDescription
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
@@ -93,7 +104,6 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -129,22 +139,33 @@ import com.droiddeck.launcher.input.SecondScreenMode
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import java.util.Locale
|
||||
|
||||
private val drawerPageTitles = listOf(R.string.drawer_page_display, R.string.drawer_page_effects, R.string.drawer_page_controls, R.string.drawer_page_components, R.string.drawer_page_settings)
|
||||
private val SessionDrawerPage.title: Int get() = when (this) {
|
||||
SessionDrawerPage.CONTROLLER -> R.string.drawer_page_controller
|
||||
SessionDrawerPage.SECOND_SCREEN -> R.string.drawer_second_screen
|
||||
SessionDrawerPage.DISPLAY -> R.string.drawer_page_display
|
||||
SessionDrawerPage.EFFECTS -> R.string.drawer_page_effects
|
||||
SessionDrawerPage.GAMES -> R.string.game_settings_title
|
||||
SessionDrawerPage.SESSION -> R.string.mode_session
|
||||
}
|
||||
|
||||
/** One icon per drawer page, in page order (QAM-style tabs). */
|
||||
private val drawerPageIcons = listOf(Icons.Outlined.DesktopWindows, Icons.Outlined.AutoFixHigh, Icons.Outlined.SportsEsports, Icons.Outlined.Layers, Icons.Outlined.Settings)
|
||||
|
||||
private val drawerPageEntries = listOf("hud", "look", "touch", "cmp-proton", "suspend")
|
||||
private val SessionDrawerPage.icon get() = when (this) {
|
||||
SessionDrawerPage.CONTROLLER -> Icons.Outlined.SportsEsports
|
||||
SessionDrawerPage.SECOND_SCREEN -> Icons.Outlined.ScreenShare
|
||||
SessionDrawerPage.DISPLAY -> Icons.Outlined.DesktopWindows
|
||||
SessionDrawerPage.EFFECTS -> Icons.Outlined.AutoFixHigh
|
||||
SessionDrawerPage.GAMES -> Icons.Outlined.Layers
|
||||
SessionDrawerPage.SESSION -> Icons.Outlined.Settings
|
||||
}
|
||||
|
||||
private class DrawerFocus {
|
||||
private val requesters = HashMap<String, FocusRequester>()
|
||||
private val last = arrayOfNulls<String>(DRAWER_PAGES)
|
||||
private val last = mutableMapOf<SessionDrawerPage, String>()
|
||||
var focused by mutableStateOf<String?>(null)
|
||||
private set
|
||||
|
||||
private fun requester(key: String) = requesters.getOrPut(key) { FocusRequester() }
|
||||
|
||||
fun track(page: Int, key: String): Modifier = Modifier.focusRequester(requester(key)).onFocusChanged {
|
||||
fun track(page: SessionDrawerPage, key: String): Modifier = Modifier.focusRequester(requester(key)).onFocusChanged {
|
||||
if (it.isFocused) {
|
||||
last[page] = key
|
||||
focused = key
|
||||
@@ -152,9 +173,9 @@ private class DrawerFocus {
|
||||
}
|
||||
|
||||
/** Where focus was last on [page], if anywhere. */
|
||||
fun target(page: Int) = last[page]
|
||||
fun target(page: SessionDrawerPage) = last[page]
|
||||
fun request(key: String) = runCatching { requester(key).requestFocus() }
|
||||
fun forget(page: Int) { last[page] = null }
|
||||
fun forget(page: SessionDrawerPage) { last.remove(page) }
|
||||
}
|
||||
|
||||
/** Everything the drawer shows and does. */
|
||||
@@ -168,13 +189,13 @@ class DrawerActions(
|
||||
val frameGen: FrameGen.Mode,
|
||||
val lossless: Lossless.State,
|
||||
val oscMode: String,
|
||||
val onScreenButtonsVisible: Boolean,
|
||||
val suspendPolicy: String,
|
||||
val backActionsInverted: Boolean,
|
||||
val touchMode: String,
|
||||
val touchAuto: String,
|
||||
val shapeMode: String,
|
||||
val fexPreset: String,
|
||||
/** Steam only: games stretched to the screen's size, changed live (null = not Steam). */
|
||||
/** Steam only: forces game windows fullscreen, changed live (null = not Steam). */
|
||||
val fillScreen: Boolean? = null,
|
||||
val upscaler: Int = 0,
|
||||
val upscaleSharpness: Int = 75,
|
||||
@@ -204,7 +225,6 @@ class DrawerActions(
|
||||
val onSuspendPolicy: (String) -> Unit,
|
||||
val onBackActionsInverted: (Boolean) -> Unit,
|
||||
val onTouch: (String) -> Unit,
|
||||
val onShape: (String) -> Unit,
|
||||
val onFexPreset: (String) -> Unit,
|
||||
val onFillScreen: (Boolean) -> Unit = {},
|
||||
val onUpscaler: (Int) -> Unit = {},
|
||||
@@ -225,7 +245,7 @@ class DrawerActions(
|
||||
/** 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. */
|
||||
/** Games tab: every Proton with what it uses; null until first read. */
|
||||
val components: ComponentsManager.Snapshot? = null,
|
||||
/** Re-reads the Protons (and runs swaps that waited for a game to close). */
|
||||
val onComponentsRefresh: () -> Unit = {},
|
||||
@@ -235,16 +255,20 @@ class DrawerActions(
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun SessionDrawer(open: Boolean, page: Int, controllerActive: Boolean, onPageChange: (Int) -> Unit, a: DrawerActions) {
|
||||
internal fun SessionDrawer(open: Boolean, requestedPage: SessionDrawerPage, controllerActive: Boolean, onPageChange: (SessionDrawerPage) -> Unit, a: DrawerActions) {
|
||||
val pages = sessionDrawerPages(a.steam && a.secondScreenDisplays.isNotEmpty())
|
||||
val page = requestedPage.takeIf { it in pages } ?: SessionDrawerPage.CONTROLLER
|
||||
LaunchedEffect(requestedPage, pages) { if (page != requestedPage) onPageChange(page) }
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
val host = rememberMenuHost()
|
||||
var androidAppsExpanded by rememberSaveable { mutableStateOf(false) }
|
||||
var advancedEffectsExpanded by rememberSaveable { mutableStateOf(false) }
|
||||
var appToChooseDisplay by remember { mutableStateOf<HomeApp.LaunchableApp?>(null) }
|
||||
var confirmStop by remember { mutableStateOf(false) }
|
||||
var sheetCoords by remember { mutableStateOf<LayoutCoordinates?>(null) }
|
||||
var stopCoords by remember { mutableStateOf<LayoutCoordinates?>(null) }
|
||||
val pageScroll = remember { List(DRAWER_PAGES) { ScrollState(0) } }
|
||||
val pageScroll = remember { SessionDrawerPage.entries.associateWith { ScrollState(0) } }
|
||||
val veil by animateFloatAsState(if (open) 1f else 0f, Motion.tw(260), label = "veil")
|
||||
val focus = remember { DrawerFocus() }
|
||||
val inputModeManager = LocalInputModeManager.current
|
||||
@@ -254,13 +278,13 @@ fun SessionDrawer(open: Boolean, page: Int, controllerActive: Boolean, onPageCha
|
||||
}
|
||||
BackHandler(enabled = open && confirmStop) { confirmStop = false }
|
||||
LaunchedEffect(page) { host.open = null; appToChooseDisplay = null }
|
||||
LaunchedEffect(open, page) { if (open && page == DRAWER_PAGE_COMPONENTS) a.onComponentsRefresh() }
|
||||
LaunchedEffect(open, page) { if (open && page == SessionDrawerPage.GAMES) a.onComponentsRefresh() }
|
||||
LaunchedEffect(open, controllerActive) {
|
||||
if (open && !controllerActive) focusManager.clearFocus(force = true)
|
||||
}
|
||||
// The page focus was last put on: turning to another (LB, RB, a tab) starts it at its first
|
||||
// option; anything else (a menu closing, the drawer opening again) goes back where it was.
|
||||
var focusedPage by remember { mutableIntStateOf(page) }
|
||||
var focusedPage by remember { mutableStateOf(page) }
|
||||
LaunchedEffect(open, page, controllerActive, host.open, appToChooseDisplay, confirmStop) {
|
||||
if (!open) {
|
||||
host.open = null
|
||||
@@ -270,8 +294,8 @@ fun SessionDrawer(open: Boolean, page: Int, controllerActive: Boolean, onPageCha
|
||||
if (host.open != null || appToChooseDisplay != null || confirmStop) return@LaunchedEffect
|
||||
val turned = page != focusedPage
|
||||
focusedPage = page
|
||||
// The page's top control: on Settings that is Android apps, when it is shown.
|
||||
val first = if (page == drawerPageEntries.lastIndex && a.isHomeApp) "apps" else drawerPageEntries[page]
|
||||
// The page's top control: on Session that is Android apps, when it is shown.
|
||||
val first = if (page == SessionDrawerPage.SESSION && a.isHomeApp) "apps" else page.firstControl
|
||||
if (turned) focus.forget(page)
|
||||
val target = focus.target(page) ?: first
|
||||
repeat(24) {
|
||||
@@ -350,31 +374,21 @@ fun SessionDrawer(open: Boolean, page: Int, controllerActive: Boolean, onPageCha
|
||||
modifier = Modifier.fillMaxWidth().padding(top = if (short) 6.dp else 12.dp),
|
||||
) {
|
||||
DrawerBumper("LB", stringResource(R.string.drawer_prev_page), modifier = focus.track(page, "prev")) {
|
||||
host.open = null; onPageChange((page + DRAWER_PAGES - 1) % DRAWER_PAGES)
|
||||
host.open = null; onPageChange(pages.step(page, -1))
|
||||
}
|
||||
DrawerPageTabs(page = page, compact = short, modifier = Modifier.weight(1f)) { index -> host.open = null; onPageChange(index) }
|
||||
DrawerPageTabs(page = page, pages = pages, compact = short, modifier = Modifier.weight(1f)) { index -> host.open = null; onPageChange(index) }
|
||||
DrawerBumper("RB", stringResource(R.string.drawer_next_page), modifier = focus.track(page, "next")) {
|
||||
host.open = null; onPageChange((page + 1) % DRAWER_PAGES)
|
||||
host.open = null; onPageChange(pages.step(page, 1))
|
||||
}
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.weight(1f).fillMaxWidth().verticalScroll(pageScroll[page]),
|
||||
modifier = Modifier.weight(1f).fillMaxWidth().verticalScroll(pageScroll.getValue(page)),
|
||||
) { CompositionLocalProvider(LocalChipMinWidth provides 120.dp) {
|
||||
when (page) {
|
||||
0 -> {
|
||||
SessionDrawerPage.DISPLAY -> {
|
||||
SettingsGroup(stringResource(R.string.drawer_page_display)) {
|
||||
ToggleRow(host, "hud", stringResource(R.string.drawer_hud), null, a.hudOn,
|
||||
chipModifier = focus.track(page, "hud"), onChange = a.onHud)
|
||||
if (a.fillScreen != null) ToggleRow(
|
||||
host, "fill", stringResource(R.string.drawer_fill), null, a.fillScreen,
|
||||
chipModifier = focus.track(page, "fill"), onChange = a.onFillScreen,
|
||||
)
|
||||
ChoiceRow(host, "upscaler", stringResource(R.string.drawer_scaling), null,
|
||||
SessionPrefs.upscalerChoices(LocalContext.current), a.upscaler,
|
||||
chipModifier = focus.track(page, "upscaler"), onPick = a.onUpscaler)
|
||||
SliderRow(stringResource(R.string.drawer_scaling_sharpness), null, a.upscaleSharpness, 0..100, step = 5,
|
||||
enabled = a.upscaler != 0, format = { "$it%" }, modifier = focus.track(page, "upscale-sharpness"),
|
||||
onChange = a.onUpscaleSharpness)
|
||||
val fgOpen = host.open == "fg"
|
||||
val fgLabel = FrameGen.label(LocalContext.current, a.frameGen)
|
||||
SettingsRow(stringResource(R.string.frame_gen_title), null, highlighted = fgOpen) {
|
||||
@@ -384,14 +398,27 @@ fun SessionDrawer(open: Boolean, page: Int, controllerActive: Boolean, onPageCha
|
||||
}
|
||||
}
|
||||
}
|
||||
SettingsGroup(stringResource(R.string.display_image_scaling)) {
|
||||
ChoiceRow(host, "upscaler", stringResource(R.string.display_filter), null,
|
||||
SessionPrefs.upscalerChoices(LocalContext.current), a.upscaler,
|
||||
note = stringResource(R.string.display_filter_note),
|
||||
chipModifier = focus.track(page, "upscaler"), onPick = a.onUpscaler)
|
||||
if (SessionPrefs.upscalerHasSharpness(a.upscaler)) SliderRow(
|
||||
stringResource(R.string.display_sharpness), null, a.upscaleSharpness, 0..100, step = 5,
|
||||
format = { "$it%" }, modifier = focus.track(page, "upscale-sharpness"), onChange = a.onUpscaleSharpness)
|
||||
}
|
||||
if (a.fillScreen != null) SettingsGroup(stringResource(R.string.display_window_compatibility)) {
|
||||
ToggleRow(host, "fill", stringResource(R.string.display_force_fullscreen),
|
||||
stringResource(R.string.display_force_fullscreen_live), a.fillScreen,
|
||||
chipModifier = focus.track(page, "fill"), onChange = a.onFillScreen)
|
||||
}
|
||||
}
|
||||
DRAWER_PAGE_EFFECTS -> {
|
||||
ScreenEffectsGroup(host, a) { key -> focus.track(page, key) }
|
||||
TextureFilteringGroup(host, a) { key -> focus.track(page, key) }
|
||||
SessionDrawerPage.EFFECTS -> {
|
||||
ScreenEffectsGroup(host, a, advancedEffectsExpanded,
|
||||
onAdvancedToggle = { advancedEffectsExpanded = !advancedEffectsExpanded }) { key -> focus.track(page, key) }
|
||||
}
|
||||
DRAWER_PAGE_COMPONENTS -> ComponentsDrawerPage(host, a) { key -> focus.track(page, key) }
|
||||
DRAWER_PAGE_CONTROLS -> {
|
||||
SettingsGroup(stringResource(R.string.drawer_page_controls)) {
|
||||
SessionDrawerPage.CONTROLLER -> {
|
||||
SettingsGroup(stringResource(R.string.drawer_page_controller)) {
|
||||
ChoiceRow(host, "touch", stringResource(R.string.mode_touch), null,
|
||||
listOf(SessionPrefs.TOUCH_AUTO to stringResource(R.string.drawer_touch_auto, a.touchAuto), SessionPrefs.TOUCH_PAD to stringResource(R.string.mode_touch_touchpad), SessionPrefs.TOUCH_DIRECT to stringResource(R.string.mode_touch_direct), SessionPrefs.TOUCH_OFF to stringResource(R.string.widgets_off)),
|
||||
a.touchMode, chipModifier = focus.track(page, "touch"), onPick = a.onTouch)
|
||||
@@ -402,12 +429,16 @@ fun SessionDrawer(open: Boolean, page: Int, controllerActive: Boolean, onPageCha
|
||||
a.controller?.let { c ->
|
||||
ToggleRow(host, "rumble", stringResource(R.string.ctrl_rumble), null, c.rumble,
|
||||
chipModifier = focus.track(page, "rumble"), onChange = a.onRumble)
|
||||
ToggleRow(host, "steam-button", stringResource(R.string.ctrl_steam_button), null, c.steamButton,
|
||||
chipModifier = focus.track(page, "steam-button"), onChange = a.onSteamButton)
|
||||
ToggleRow(host, "qam-button", stringResource(R.string.ctrl_qam_button), null, c.qamButton,
|
||||
chipModifier = focus.track(page, "qam-button"), onChange = a.onQamButton)
|
||||
ToggleRow(host, "keyboard-button", stringResource(R.string.ctrl_keyboard_button), null, c.keyboardButton,
|
||||
chipModifier = focus.track(page, "keyboard-button"), onChange = a.onKeyboardButton)
|
||||
if (a.onScreenButtonsVisible) {
|
||||
if (a.steam) {
|
||||
ToggleRow(host, "steam-button", stringResource(R.string.ctrl_steam_button), null, c.steamButton,
|
||||
chipModifier = focus.track(page, "steam-button"), onChange = a.onSteamButton)
|
||||
ToggleRow(host, "qam-button", stringResource(R.string.ctrl_qam_button), null, c.qamButton,
|
||||
chipModifier = focus.track(page, "qam-button"), onChange = a.onQamButton)
|
||||
}
|
||||
ToggleRow(host, "keyboard-button", stringResource(R.string.ctrl_keyboard_button), null, c.keyboardButton,
|
||||
chipModifier = focus.track(page, "keyboard-button"), onChange = a.onKeyboardButton)
|
||||
}
|
||||
}
|
||||
if (a.steam) ChoiceRow(host, "back-actions", stringResource(R.string.mode_back), null,
|
||||
listOf(false to stringResource(SessionPrefs.BACK_MENU_THEN_QAM), true to stringResource(SessionPrefs.BACK_QAM_THEN_MENU)),
|
||||
@@ -423,18 +454,23 @@ fun SessionDrawer(open: Boolean, page: Int, controllerActive: Boolean, onPageCha
|
||||
}
|
||||
}
|
||||
}
|
||||
if (a.steam && a.secondScreenDisplays.isNotEmpty()) SettingsGroup(stringResource(R.string.drawer_second_screen)) {
|
||||
ChoiceRow(host, "second-screen-mode", stringResource(R.string.drawer_second_screen_shows), null,
|
||||
(listOf(SecondScreenMode.NONE, SecondScreenMode.KEYBOARD_TRACKPAD, SecondScreenMode.TERMINAL) +
|
||||
(if (com.droiddeck.launcher.session.SessionState.deckPad) listOf(SecondScreenMode.DECK_CONTROLS) else emptyList()))
|
||||
.map { it to stringResource(it.label) },
|
||||
a.secondScreenMode, chipModifier = focus.track(page, "second-screen-mode"), onPick = a.onSecondScreenMode)
|
||||
if (a.secondScreenDisplays.size > 1) ChoiceRow(host, "second-screen-display", stringResource(R.string.drawer_page_display), null,
|
||||
a.secondScreenDisplays.map { it.id to it.label }, a.selectedSecondScreenDisplay,
|
||||
chipModifier = focus.track(page, "second-screen-display"), onPick = a.onSecondScreenDisplay)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
SessionDrawerPage.SECOND_SCREEN -> {
|
||||
SecondScreenGroup(host, a) { key -> focus.track(page, key) }
|
||||
}
|
||||
SessionDrawerPage.GAMES -> {
|
||||
Text(stringResource(R.string.fex_next_launch), fontSize = 12.sp,
|
||||
color = colors.onSurfaceVariant, modifier = Modifier.padding(horizontal = 14.dp, vertical = 8.dp))
|
||||
TextureFilteringGroup(host, a) { key -> focus.track(page, key) }
|
||||
if (a.steam) SettingsGroup(stringResource(R.string.game_settings_title)) {
|
||||
ChoiceRow(host, "fex", stringResource(R.string.fex_preset_title), null,
|
||||
FexPreset.all.map { it.id to stringResource(it.label) }, a.fexPreset,
|
||||
chipModifier = focus.track(page, "fex"), onPick = a.onFexPreset)
|
||||
GameEnvironmentRow(modifier = focus.track(page, "game-env"), showHint = false)
|
||||
}
|
||||
ComponentsDrawerPage(host, a) { key -> focus.track(page, key) }
|
||||
}
|
||||
SessionDrawerPage.SESSION -> {
|
||||
if (a.isHomeApp) SettingsGroup(stringResource(R.string.drawer_android_apps)) {
|
||||
SettingsRow(stringResource(R.string.drawer_launch_app), null) {
|
||||
DrawerOutlineButton(if (androidAppsExpanded) stringResource(R.string.common_hide) else stringResource(R.string.common_show), modifier = focus.track(page, "apps")) {
|
||||
@@ -466,7 +502,7 @@ fun SessionDrawer(open: Boolean, page: Int, controllerActive: Boolean, onPageCha
|
||||
SettingsGroup(stringResource(R.string.drawer_session_behavior)) {
|
||||
ChoiceRow(
|
||||
host, "suspend", stringResource(R.string.mode_suspend),
|
||||
stringResource(R.string.drawer_suspend_hint),
|
||||
null,
|
||||
listOf(
|
||||
SessionPrefs.SUSPEND_AUTO to stringResource(R.string.common_auto),
|
||||
SessionPrefs.SUSPEND_MANUAL to stringResource(R.string.mode_suspend_manual),
|
||||
@@ -478,17 +514,6 @@ fun SessionDrawer(open: Boolean, page: Int, controllerActive: Boolean, onPageCha
|
||||
onPick = a.onSuspendPolicy,
|
||||
)
|
||||
}
|
||||
SettingsGroup(stringResource(R.string.drawer_next_session)) {
|
||||
ChoiceRow(host, "shape", stringResource(R.string.mode_ratio), null,
|
||||
SessionPrefs.shapeChoices(LocalContext.current), a.shapeMode,
|
||||
chipModifier = focus.track(page, "shape"), onPick = a.onShape)
|
||||
}
|
||||
if (a.steam) SettingsGroup(stringResource(R.string.game_settings_title)) {
|
||||
ChoiceRow(host, "fex", stringResource(R.string.fex_preset_title), stringResource(R.string.fex_next_launch),
|
||||
FexPreset.all.map { it.id to stringResource(it.label) }, a.fexPreset,
|
||||
chipModifier = focus.track(page, "fex"), onPick = a.onFexPreset)
|
||||
GameEnvironmentRow(modifier = focus.track(page, "game-env"))
|
||||
}
|
||||
SettingsGroup(stringResource(R.string.drawer_support)) {
|
||||
SettingsRow(stringResource(R.string.drawer_logs), stringResource(R.string.drawer_logs_hint)) {
|
||||
DrawerOutlineButton(stringResource(R.string.drawer_share_logs), modifier = focus.track(page, "share-logs")) {
|
||||
@@ -501,12 +526,11 @@ fun SessionDrawer(open: Boolean, page: Int, controllerActive: Boolean, onPageCha
|
||||
ToggleRow(host, "pip-auto", stringResource(R.string.pip_auto), null,
|
||||
a.pipAutoEnter, onChange = a.onPipAutoEnter)
|
||||
}
|
||||
Spacer(Modifier.height(18.dp))
|
||||
if (a.pipSupported) DrawerOutlineButton(stringResource(R.string.pip_title), modifier = focus.track(page, "pip")) {
|
||||
host.open = null; a.onPip()
|
||||
}
|
||||
if (!a.isHomeApp) DrawerOutlineButton(stringResource(R.string.drawer_background), modifier = focus.track(page, "background")) {
|
||||
host.open = null; a.onBackground()
|
||||
Column(Modifier.fillMaxWidth().padding(top = 12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
if (a.pipSupported) DrawerActionButton(stringResource(R.string.pip_title), Icons.Outlined.PictureInPictureAlt,
|
||||
modifier = focus.track(page, "pip")) { host.open = null; a.onPip() }
|
||||
if (!a.isHomeApp) DrawerActionButton(stringResource(R.string.drawer_background), Icons.AutoMirrored.Outlined.ExitToApp,
|
||||
modifier = focus.track(page, "background")) { host.open = null; a.onBackground() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -546,6 +570,27 @@ fun SessionDrawer(open: Boolean, page: Int, controllerActive: Boolean, onPageCha
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DrawerActionButton(text: String, icon: androidx.compose.ui.graphics.vector.ImageVector, modifier: Modifier = Modifier, onClick: () -> Unit) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
val source = remember { MutableInteractionSource() }
|
||||
val hot = source.collectIsFocusedAsState().value || source.collectIsHoveredAsState().value
|
||||
val shape = RoundedCornerShape(8.dp)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = modifier.fillMaxWidth().heightIn(min = 52.dp).clip(shape)
|
||||
.background(if (hot) pal.signal.copy(alpha = 0.16f) else colors.surfaceVariant)
|
||||
.glideBorder(hot, shape, pal.signal)
|
||||
.hoverable(source).clickable(interactionSource = source, indication = LocalIndication.current, onClick = onClick)
|
||||
.controllerConfirm(onClick = onClick).padding(horizontal = 16.dp, vertical = 10.dp),
|
||||
) {
|
||||
Icon(icon, contentDescription = null, tint = colors.onBackground, modifier = Modifier.size(22.dp))
|
||||
Text(text, fontSize = 15.sp, fontWeight = FontWeight.SemiBold, color = colors.onBackground)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DrawerOutlineButton(text: String, modifier: Modifier = Modifier, onClick: () -> Unit) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
@@ -636,58 +681,60 @@ private fun DrawerBumper(label: String, description: String, modifier: Modifier
|
||||
/**
|
||||
* The drawer's page tabs, QAM-style: one icon per page standing in a line in page order. The
|
||||
* selected one steps forward - full size, bright, a soft glow - while the others step back to
|
||||
* half size and fade, and the whole line leans a little toward the selection. LB / RB still turn
|
||||
* a little smaller and fade, and the whole line leans a little toward the selection. LB / RB still turn
|
||||
* the page (the buttons beside it and the pad's bumpers); a tab can be tapped or picked with the
|
||||
* pad like the dots it replaces.
|
||||
*/
|
||||
@Composable
|
||||
private fun DrawerPageTabs(page: Int, modifier: Modifier = Modifier, compact: Boolean = false, onSelect: (Int) -> Unit) {
|
||||
private fun DrawerPageTabs(page: SessionDrawerPage, pages: List<SessionDrawerPage>, modifier: Modifier = Modifier, compact: Boolean = false, onSelect: (SessionDrawerPage) -> Unit) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
val count = drawerPageIcons.size
|
||||
val count = pages.size
|
||||
val selectedIndex = pages.indexOf(page)
|
||||
val middle = (count - 1) / 2f
|
||||
val motion = Motion.tw<Float>(340, easing = FastOutSlowInEasing)
|
||||
androidx.compose.foundation.layout.BoxWithConstraints(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = modifier.height(if (compact) 44.dp else 64.dp).clipToBounds(),
|
||||
) {
|
||||
val tab = if (compact) 40f else 56f
|
||||
val tab = if (compact) 36f else 44f
|
||||
// Fit the line to the room between LB and RB. Worst case is an end tab selected: full size
|
||||
// ([tab] dp) and pushed outward by the lean, which scales with the spacing. So
|
||||
// (count-1) * spacing * (1 + lean/64) + 56 <= width
|
||||
// - never more than the mock's 64 dp, never tighter than 32 dp.
|
||||
val spacing = ((maxWidth.value - tab) / ((count - 1) * (1f + DRAWER_TAB_LEAN_DP / DRAWER_TAB_SPACING_DP))).coerceIn(32f, DRAWER_TAB_SPACING_DP)
|
||||
// (count-1) * spacing * (1 + lean/spacingMax) + tab <= width
|
||||
val spacing = ((maxWidth.value - tab) / ((count - 1) * (1f + DRAWER_TAB_LEAN_DP / DRAWER_TAB_SPACING_DP))).coerceIn(24f, DRAWER_TAB_SPACING_DP)
|
||||
val leanStep = DRAWER_TAB_LEAN_DP * spacing / DRAWER_TAB_SPACING_DP
|
||||
val lean by animateFloatAsState(-(page - middle) * leanStep, motion, label = "tabLean")
|
||||
drawerPageIcons.forEachIndexed { index, icon ->
|
||||
val selected = index == page
|
||||
val source = remember { MutableInteractionSource() }
|
||||
val focused = source.collectIsFocusedAsState().value
|
||||
val scale by animateFloatAsState(if (selected) 1f else DRAWER_TAB_SIDE_SCALE, motion, label = "tabScale")
|
||||
val alpha by animateFloatAsState(if (selected) 1f else DRAWER_TAB_SIDE_ALPHA, motion, label = "tabAlpha")
|
||||
val glow by animateFloatAsState(if (selected) 1f else 0f, motion, label = "tabGlow")
|
||||
val pageDescription = stringResource(R.string.drawer_page_desc, stringResource(drawerPageTitles[index]))
|
||||
val tint by animateColorAsState(if (selected) colors.onBackground else colors.onSurfaceVariant, Motion.tw(340, easing = FastOutSlowInEasing), label = "tabTint")
|
||||
val select = { onSelect(index) }
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.offset(x = ((index - middle) * spacing + lean).dp)
|
||||
.size(tab.dp)
|
||||
.graphicsLayer { scaleX = scale; scaleY = scale; this.alpha = alpha }
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(
|
||||
Brush.radialGradient(
|
||||
listOf(pal.signal.copy(alpha = 0.34f * glow), pal.signal.copy(alpha = 0.10f * glow), Color.Transparent),
|
||||
),
|
||||
)
|
||||
.glideBorder(focused, RoundedCornerShape(16.dp), colors.onBackground)
|
||||
.semantics { contentDescription = pageDescription }
|
||||
.hoverable(source)
|
||||
.clickable(interactionSource = source, indication = LocalIndication.current, onClick = select)
|
||||
.controllerConfirm(onClick = select),
|
||||
) {
|
||||
Icon(icon, contentDescription = null, tint = tint, modifier = Modifier.size(if (compact) 22.dp else 30.dp))
|
||||
val lean by animateFloatAsState(-(selectedIndex - middle) * leanStep, motion, label = "tabLean")
|
||||
pages.forEachIndexed { index, item ->
|
||||
androidx.compose.runtime.key(item) {
|
||||
val selected = item == page
|
||||
val source = remember { MutableInteractionSource() }
|
||||
val focused = source.collectIsFocusedAsState().value
|
||||
val scale by animateFloatAsState(if (selected) 1f else DRAWER_TAB_SIDE_SCALE, motion, label = "tabScale")
|
||||
val alpha by animateFloatAsState(if (selected) 1f else DRAWER_TAB_SIDE_ALPHA, motion, label = "tabAlpha")
|
||||
val glow by animateFloatAsState(if (selected) 1f else 0f, motion, label = "tabGlow")
|
||||
val pageDescription = stringResource(R.string.drawer_page_desc, stringResource(item.title))
|
||||
val tint by animateColorAsState(if (selected) colors.onBackground else colors.onSurfaceVariant, Motion.tw(340, easing = FastOutSlowInEasing), label = "tabTint")
|
||||
val select = { onSelect(item) }
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.offset(x = ((index - middle) * spacing + lean).dp)
|
||||
.size(tab.dp)
|
||||
.graphicsLayer { scaleX = scale; scaleY = scale; this.alpha = alpha }
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(
|
||||
Brush.radialGradient(
|
||||
listOf(pal.signal.copy(alpha = 0.34f * glow), pal.signal.copy(alpha = 0.10f * glow), Color.Transparent),
|
||||
),
|
||||
)
|
||||
.glideBorder(focused, RoundedCornerShape(16.dp), colors.onBackground)
|
||||
.semantics { contentDescription = pageDescription }
|
||||
.hoverable(source)
|
||||
.clickable(interactionSource = source, indication = LocalIndication.current, onClick = select)
|
||||
.controllerConfirm(onClick = select),
|
||||
) {
|
||||
Icon(item.icon, contentDescription = null, tint = tint, modifier = Modifier.size(if (compact) 22.dp else 30.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -696,54 +743,114 @@ private fun DrawerPageTabs(page: Int, modifier: Modifier = Modifier, compact: Bo
|
||||
/** The Look row's value when the rows below match no Look. */
|
||||
private const val LOOK_CUSTOM = "custom"
|
||||
|
||||
/**
|
||||
* Screen effects on the Effects page: a Look row that moves every row under it, then the rows
|
||||
* themselves: switches and sliders the d-pad steps (SliderRow), so a pad drives them.
|
||||
*/
|
||||
/** Presets and simple effects first; detailed adjustments stay behind Advanced. */
|
||||
@Composable
|
||||
private fun ScreenEffectsGroup(host: MenuHost, a: DrawerActions, track: (String) -> Modifier) {
|
||||
private fun ScreenEffectsGroup(
|
||||
host: MenuHost, a: DrawerActions, advancedExpanded: Boolean, onAdvancedToggle: () -> Unit,
|
||||
track: (String) -> Modifier,
|
||||
) {
|
||||
val e = a.effects
|
||||
val look = ScreenEffectLooks.match(e, a.upscaler)
|
||||
val preset = ScreenEffectLooks.match(e, a.upscaler)
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
SettingsGroup(stringResource(R.string.drawer_effects)) {
|
||||
val looks = ScreenEffectLooks.LOOKS.map { it.name to stringResource(it.label) }
|
||||
ChoiceRow(host, "look", stringResource(R.string.drawer_look),
|
||||
look?.let { stringResource(it.descRes) } ?: stringResource(R.string.drawer_effects_hint),
|
||||
if (look == null) listOf(LOOK_CUSTOM to stringResource(R.string.drawer_look_custom)) + looks else looks,
|
||||
look?.name ?: LOOK_CUSTOM, chipModifier = track("look"), hintLines = 2) { name ->
|
||||
val presets = ScreenEffectLooks.LOOKS.map { it.name to stringResource(it.label) }
|
||||
ChoiceRow(host, "look", stringResource(R.string.drawer_look), null,
|
||||
if (preset == null) listOf(LOOK_CUSTOM to stringResource(R.string.drawer_look_custom)) + presets else presets,
|
||||
preset?.name ?: LOOK_CUSTOM, chipModifier = track("look")) { name ->
|
||||
ScreenEffectLooks.LOOKS.firstOrNull { it.name == name }?.let { picked ->
|
||||
picked.scalingMode?.let(a.onUpscaler)
|
||||
a.onEffects(picked.effects)
|
||||
}
|
||||
}
|
||||
ToggleRow(host, "cas", stringResource(R.string.drawer_cas), null, e.cas, chipModifier = track("cas")) { a.onEffects(e.copy(cas = it)) }
|
||||
SliderRow(stringResource(R.string.drawer_cas_level), null, e.casLevel, 0..100, step = 5, enabled = e.cas,
|
||||
format = { "$it%" }, modifier = track("cas-level")) { a.onEffects(e.copy(casLevel = it)) }
|
||||
ToggleRow(host, "fake-hdr", stringResource(R.string.drawer_fake_hdr), null, e.hdr, chipModifier = track("fake-hdr")) { a.onEffects(e.copy(hdr = it)) }
|
||||
ToggleRow(host, "deband", stringResource(R.string.drawer_deband), null, e.deband, chipModifier = track("deband")) { a.onEffects(e.copy(deband = it)) }
|
||||
SliderRow(stringResource(R.string.drawer_deband_strength), null, e.debandStrength, 0..200, step = 5, enabled = e.deband,
|
||||
format = { "$it%" }, modifier = track("deband-strength")) { a.onEffects(e.copy(debandStrength = it)) }
|
||||
SliderRow(stringResource(R.string.drawer_brightness), null, e.brightness, -100..100, step = 2,
|
||||
format = ::signed, modifier = track("brightness")) { a.onEffects(e.copy(brightness = it)) }
|
||||
SliderRow(stringResource(R.string.drawer_contrast), null, e.contrast, -100..100, step = 2,
|
||||
format = ::signed, modifier = track("contrast")) { a.onEffects(e.copy(contrast = it)) }
|
||||
SliderRow(stringResource(R.string.drawer_gamma), null, (e.gamma * 100f).roundToInt(), 50..300, step = 5,
|
||||
format = { String.format(Locale.US, "%.2f", it / 100f) }, modifier = track("gamma")) { a.onEffects(e.copy(gamma = it / 100f)) }
|
||||
SliderRow(stringResource(R.string.drawer_saturation), null, e.saturation, 0..200, step = 5,
|
||||
format = { "$it%" }, modifier = track("saturation")) { a.onEffects(e.copy(saturation = it)) }
|
||||
ToggleRow(host, "fxaa", stringResource(R.string.drawer_fxaa), null, e.fxaa, chipModifier = track("fxaa")) { a.onEffects(e.copy(fxaa = it)) }
|
||||
ToggleRow(host, "toon", stringResource(R.string.drawer_toon), null, e.toon, chipModifier = track("toon")) { a.onEffects(e.copy(toon = it)) }
|
||||
ToggleRow(host, "crt", stringResource(R.string.drawer_crt), null, e.crt, chipModifier = track("crt")) { a.onEffects(e.copy(crt = it)) }
|
||||
ToggleRow(host, "ntsc", stringResource(R.string.drawer_ntsc), null, e.ntsc, chipModifier = track("ntsc")) { a.onEffects(e.copy(ntsc = it)) }
|
||||
ToggleRow(host, "fake-hdr", stringResource(R.string.drawer_fake_hdr), null, e.hdr, chipModifier = track("fake-hdr")) { a.onEffects(e.copy(hdr = it)) }
|
||||
}
|
||||
val source = remember { MutableInteractionSource() }
|
||||
val hot = source.collectIsFocusedAsState().value || source.collectIsHoveredAsState().value
|
||||
val rotation by animateFloatAsState(if (advancedExpanded) 180f else 0f, Motion.tw(180), label = "advancedArrow")
|
||||
val description = stringResource(if (advancedExpanded) R.string.widgets_expanded else R.string.widgets_collapsed)
|
||||
val toggle = { host.open = null; onAdvancedToggle() }
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
modifier = track("effects-advanced").fillMaxWidth().padding(top = 12.dp).heightIn(min = 48.dp)
|
||||
.glideBorder(hot, RoundedCornerShape(8.dp), pal.signal)
|
||||
.hoverable(source).clickable(interactionSource = source, indication = LocalIndication.current, role = Role.Button, onClick = toggle)
|
||||
.controllerConfirm(onClick = toggle).semantics { stateDescription = description }.padding(horizontal = 4.dp),
|
||||
) {
|
||||
Text(stringResource(R.string.drawer_effects_advanced).uppercase(), fontSize = 12.sp,
|
||||
fontWeight = FontWeight.SemiBold, letterSpacing = 1.5.sp, color = colors.onSurfaceVariant)
|
||||
Box(Modifier.weight(1f).height(1.dp).background(pal.line))
|
||||
Icon(Icons.Outlined.ExpandMore, contentDescription = null, tint = colors.onSurfaceVariant,
|
||||
modifier = Modifier.size(22.dp).rotate(rotation))
|
||||
}
|
||||
AnimatedVisibility(advancedExpanded) {
|
||||
Column(Modifier.fillMaxWidth().clip(RoundedCornerShape(14.dp)).background(colors.surface).border(1.dp, pal.line, RoundedCornerShape(14.dp))) {
|
||||
ToggleRow(host, "cas", stringResource(R.string.drawer_cas), null, e.cas, chipModifier = track("cas")) { a.onEffects(e.copy(cas = it)) }
|
||||
if (e.cas) SliderRow(stringResource(R.string.drawer_cas_level), null, e.casLevel, 0..100, step = 5,
|
||||
format = { "$it%" }, modifier = track("cas-level")) { a.onEffects(e.copy(casLevel = it)) }
|
||||
ToggleRow(host, "deband", stringResource(R.string.drawer_deband), null, e.deband, chipModifier = track("deband")) { a.onEffects(e.copy(deband = it)) }
|
||||
if (e.deband) SliderRow(stringResource(R.string.drawer_deband_strength), null, e.debandStrength, 0..200, step = 5,
|
||||
format = { "$it%" }, modifier = track("deband-strength")) { a.onEffects(e.copy(debandStrength = it)) }
|
||||
SliderRow(stringResource(R.string.drawer_brightness), null, e.brightness, -100..100, step = 2,
|
||||
format = ::signed, modifier = track("brightness")) { a.onEffects(e.copy(brightness = it)) }
|
||||
SliderRow(stringResource(R.string.drawer_contrast), null, e.contrast, -100..100, step = 2,
|
||||
format = ::signed, modifier = track("contrast")) { a.onEffects(e.copy(contrast = it)) }
|
||||
SliderRow(stringResource(R.string.drawer_gamma), null, (e.gamma * 100f).roundToInt(), 50..300, step = 5,
|
||||
format = { String.format(Locale.US, "%.2f", it / 100f) }, modifier = track("gamma")) { a.onEffects(e.copy(gamma = it / 100f)) }
|
||||
SliderRow(stringResource(R.string.drawer_saturation), null, e.saturation, 0..200, step = 5,
|
||||
format = { "$it%" }, modifier = track("saturation")) { a.onEffects(e.copy(saturation = it)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Texture filtering on the Effects page: DXVK options for the next DirectX 9-11 launch. */
|
||||
/** Secondary-display modes are mutually exclusive and apply immediately. */
|
||||
@Composable
|
||||
private fun SecondScreenGroup(host: MenuHost, a: DrawerActions, track: (String) -> Modifier) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
val modes = listOf(SecondScreenMode.NONE, SecondScreenMode.KEYBOARD_TRACKPAD, SecondScreenMode.TERMINAL) +
|
||||
if (com.droiddeck.launcher.session.SessionState.deckPad) listOf(SecondScreenMode.DECK_CONTROLS) else emptyList()
|
||||
SettingsGroup(stringResource(R.string.drawer_second_screen_mode)) {
|
||||
Column(Modifier.fillMaxWidth().selectableGroup()) {
|
||||
modes.forEach { mode ->
|
||||
val selected = a.secondScreenMode == mode
|
||||
val source = remember { MutableInteractionSource() }
|
||||
val hot = source.collectIsFocusedAsState().value || source.collectIsHoveredAsState().value
|
||||
val pick = { host.open = null; a.onSecondScreenMode(mode) }
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = track("second-screen:${mode.id}").fillMaxWidth().heightIn(min = 56.dp)
|
||||
.background(if (hot) pal.signal.copy(alpha = 0.12f) else Color.Transparent)
|
||||
.glideBorder(hot, RoundedCornerShape(8.dp), pal.signal)
|
||||
.hoverable(source)
|
||||
.selectable(selected, interactionSource = source, indication = null, role = Role.RadioButton, onClick = pick)
|
||||
.controllerConfirm(onClick = pick)
|
||||
.padding(horizontal = 14.dp, vertical = 8.dp),
|
||||
) {
|
||||
Text(stringResource(mode.label), fontSize = 15.sp, color = colors.onBackground, modifier = Modifier.weight(1f))
|
||||
RadioButton(selected, onClick = null,
|
||||
colors = RadioButtonDefaults.colors(selectedColor = pal.signal, unselectedColor = colors.onSurfaceVariant))
|
||||
}
|
||||
}
|
||||
}
|
||||
if (a.secondScreenDisplays.size > 1) ChoiceRow(host, "second-screen-display", stringResource(R.string.drawer_page_display), null,
|
||||
a.secondScreenDisplays.map { it.id to it.label }, a.selectedSecondScreenDisplay,
|
||||
chipModifier = track("second-screen-display"), onPick = a.onSecondScreenDisplay)
|
||||
}
|
||||
}
|
||||
|
||||
/** Texture filtering on the Games page: DXVK options for the next DirectX 9-11 launch. */
|
||||
@Composable
|
||||
private fun TextureFilteringGroup(host: MenuHost, a: DrawerActions, track: (String) -> Modifier) {
|
||||
SettingsGroup(stringResource(R.string.drawer_texture)) {
|
||||
ChoiceRow(host, "anisotropy", stringResource(R.string.drawer_anisotropy), stringResource(R.string.drawer_texture_hint),
|
||||
ChoiceRow(host, "anisotropy", stringResource(R.string.drawer_anisotropy), null,
|
||||
SessionPrefs.textureAnisotropyChoices(LocalContext.current), a.textureAnisotropy, chipModifier = track("anisotropy"), onPick = a.onTextureAnisotropy)
|
||||
ChoiceRow(host, "texture-sharpness", stringResource(R.string.drawer_texture_sharpness), stringResource(R.string.drawer_texture_hint),
|
||||
ChoiceRow(host, "texture-sharpness", stringResource(R.string.drawer_texture_sharpness), null,
|
||||
SessionPrefs.textureLodBiasChoices(LocalContext.current), a.textureLodBias, note = stringResource(R.string.drawer_texture_sharpness_note),
|
||||
chipModifier = track("texture-sharpness"), onPick = a.onTextureLodBias)
|
||||
}
|
||||
@@ -751,16 +858,8 @@ private fun TextureFilteringGroup(host: MenuHost, a: DrawerActions, track: (Stri
|
||||
|
||||
private fun signed(v: Int) = if (v > 0) "+$v" else "$v"
|
||||
|
||||
/** Display, Effects, Controls, Components, Settings. */
|
||||
const val DRAWER_PAGES = 5
|
||||
|
||||
private const val DRAWER_PAGE_EFFECTS = 1
|
||||
private const val DRAWER_PAGE_CONTROLS = 2
|
||||
|
||||
private const val DRAWER_PAGE_COMPONENTS = 3
|
||||
|
||||
/**
|
||||
* The drawer's Components tab: quick swaps between what is already installed, per Proton. A swap
|
||||
* The Games tab's components: quick swaps between what is already installed, per Proton. A swap
|
||||
* into the Proton a running game uses waits until that game closes; downloading, importing and
|
||||
* deleting stay on the Components page in the app.
|
||||
*/
|
||||
@@ -884,14 +983,14 @@ private fun <T> DrawerStackedChoice(
|
||||
Box(Modifier.fillMaxWidth().height(1.dp).background(pal.line))
|
||||
}
|
||||
|
||||
/** The tab row's measure, from the approved mock: side tabs at half size, a little faded. */
|
||||
private const val DRAWER_TAB_SPACING_DP = 64f
|
||||
/** A close row of icons: inactive tabs stay legible while the active tab leads. */
|
||||
private const val DRAWER_TAB_SPACING_DP = 48f
|
||||
|
||||
private const val DRAWER_TAB_LEAN_DP = 12f
|
||||
private const val DRAWER_TAB_LEAN_DP = 8f
|
||||
|
||||
private const val DRAWER_TAB_SIDE_SCALE = 0.5f
|
||||
private const val DRAWER_TAB_SIDE_SCALE = 0.8f
|
||||
|
||||
private const val DRAWER_TAB_SIDE_ALPHA = 0.7f
|
||||
private const val DRAWER_TAB_SIDE_ALPHA = 0.8f
|
||||
|
||||
/** Room between the Stop pill and the box that steps out under it: the pull. */
|
||||
private val StepPull = 6.dp
|
||||
|
||||
@@ -29,6 +29,7 @@ import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.stateDescription
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
@@ -64,6 +65,7 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -87,6 +89,10 @@ import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.semantics.ProgressBarRangeInfo
|
||||
import androidx.compose.ui.semantics.progressBarRangeInfo
|
||||
import androidx.compose.ui.semantics.setProgress
|
||||
import androidx.compose.ui.semantics.disabled
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import kotlin.math.roundToInt
|
||||
import androidx.compose.ui.input.InputMode
|
||||
@@ -511,8 +517,16 @@ fun ToggleSwitch(checked: Boolean, enabled: Boolean = true, label: String? = nul
|
||||
fun SliderRow(
|
||||
label: String, hint: String?, value: Int, range: IntRange, step: Int = 1, enabled: Boolean = true,
|
||||
format: (Int) -> String = { it.toString() }, modifier: Modifier = Modifier, onChange: (Int) -> Unit,
|
||||
) = SettingsRow(label, hint) {
|
||||
ValueSlider(value, range, step, enabled, label, format, modifier, onChange)
|
||||
) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
ValueSlider(value, range, step, enabled, label, format,
|
||||
modifier = modifier.padding(horizontal = 6.dp, vertical = 3.dp), onChange = onChange)
|
||||
if (hint != null) Text(hint, fontSize = 12.5.sp, color = colors.onSurfaceVariant,
|
||||
modifier = Modifier.padding(start = 14.dp, end = 14.dp, bottom = 8.dp))
|
||||
Box(Modifier.fillMaxWidth().height(1.dp).background(pal.line))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -522,34 +536,39 @@ fun ValueSlider(
|
||||
) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
val src = remember { MutableInteractionSource() }
|
||||
val hot = src.collectIsFocusedAsState().value || src.collectIsHoveredAsState().value
|
||||
val source = remember { MutableInteractionSource() }
|
||||
val hot = source.collectIsFocusedAsState().value || source.collectIsHoveredAsState().value
|
||||
val currentOnChange by rememberUpdatedState(onChange)
|
||||
val span = (range.last - range.first).coerceAtLeast(1)
|
||||
val fraction = ((value - range.first).toFloat() / span).coerceIn(0f, 1f)
|
||||
val shape = RoundedCornerShape(99.dp)
|
||||
val thumbInset = with(LocalDensity.current) { 8.dp.toPx() }
|
||||
var trackPx by remember { mutableIntStateOf(0) }
|
||||
val text = format(value)
|
||||
// A step from the d-pad lands on the grid of [step] from the range's start; a Look's own
|
||||
// value, which need not be on it, is kept until the user moves.
|
||||
val shape = RoundedCornerShape(8.dp)
|
||||
// Preserve off-grid preset values until the user adjusts; every input snaps to the same step.
|
||||
fun stepped(v: Int) = (((v - range.first).toFloat() / step).roundToInt() * step + range.first).coerceIn(range.first, range.last)
|
||||
fun fromX(x: Float) { if (trackPx > 0) onChange(stepped(range.first + (x / trackPx * span).roundToInt())) }
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier
|
||||
.heightIn(min = 48.dp)
|
||||
fun fromX(x: Float) {
|
||||
val width = trackPx - 2 * thumbInset
|
||||
if (width > 0) {
|
||||
val position = ((x - thumbInset) / width).coerceIn(0f, 1f)
|
||||
currentOnChange(stepped(range.first + (position * span).roundToInt()))
|
||||
}
|
||||
}
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth().heightIn(min = 72.dp)
|
||||
.clip(shape).background(if (hot) pal.signal.copy(alpha = 0.12f) else Color.Transparent)
|
||||
.glideBorder(hot, shape, pal.signal)
|
||||
.alpha(if (enabled) 1f else 0.5f)
|
||||
.hoverable(src)
|
||||
.focusable(enabled, src)
|
||||
.hoverable(source).focusable(enabled, source)
|
||||
.onPreviewKeyEvent { event ->
|
||||
val keyEvent = event.nativeKeyEvent
|
||||
val left = keyEvent.keyCode == KeyEvent.KEYCODE_DPAD_LEFT
|
||||
if (!left && keyEvent.keyCode != KeyEvent.KEYCODE_DPAD_RIGHT) {
|
||||
false
|
||||
} else {
|
||||
// Every down counts, repeats included, so a held direction keeps stepping.
|
||||
if (!left && keyEvent.keyCode != KeyEvent.KEYCODE_DPAD_RIGHT) false
|
||||
else {
|
||||
// Repeated key-down events let a held direction keep adjusting.
|
||||
if (enabled && keyEvent.action == KeyEvent.ACTION_DOWN) {
|
||||
val next = stepped(value + if (left) -step else step)
|
||||
if (next != value) onChange(next)
|
||||
if (next != value) currentOnChange(next)
|
||||
}
|
||||
true
|
||||
}
|
||||
@@ -558,28 +577,33 @@ fun ValueSlider(
|
||||
stateDescription = text
|
||||
progressBarRangeInfo = ProgressBarRangeInfo(value.toFloat(), range.first.toFloat()..range.last.toFloat())
|
||||
if (label != null) contentDescription = label
|
||||
},
|
||||
if (!enabled) disabled()
|
||||
setProgress { next ->
|
||||
if (!enabled) false else { currentOnChange(stepped(next.roundToInt())); true }
|
||||
}
|
||||
}
|
||||
.padding(horizontal = 8.dp, vertical = 6.dp),
|
||||
) {
|
||||
Text(text, fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = colors.onBackground, textAlign = TextAlign.End,
|
||||
maxLines = 1, modifier = Modifier.width(56.dp))
|
||||
Spacer(Modifier.width(10.dp))
|
||||
Box(
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
modifier = Modifier.size(width = 164.dp, height = 30.dp).clip(shape).background(colors.surfaceVariant)
|
||||
.glideBorder(hot, shape, colors.onBackground, pal.line2)
|
||||
.onSizeChanged { trackPx = it.width }
|
||||
.pointerInput(enabled, range, step) { if (enabled) detectTapGestures { fromX(it.x) } }
|
||||
.pointerInput(enabled, range, step) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
if (label != null) Text(label, fontSize = 15.sp, fontWeight = FontWeight.SemiBold,
|
||||
color = colors.onBackground, modifier = Modifier.weight(1f))
|
||||
else Spacer(Modifier.weight(1f))
|
||||
Text(text, fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = colors.onBackground, maxLines = 1)
|
||||
}
|
||||
Canvas(
|
||||
Modifier.fillMaxWidth().height(40.dp).onSizeChanged { trackPx = it.width }
|
||||
.pointerInput(enabled, range, step, thumbInset) { if (enabled) detectTapGestures { fromX(it.x) } }
|
||||
.pointerInput(enabled, range, step, thumbInset) {
|
||||
if (enabled) detectHorizontalDragGestures { change, _ -> fromX(change.position.x); change.consume() }
|
||||
},
|
||||
) {
|
||||
Box(Modifier.fillMaxWidth(fraction).fillMaxHeight().background(pal.signal))
|
||||
Box(
|
||||
Modifier.padding(4.dp).size(22.dp)
|
||||
.graphicsLayer { translationX = fraction * (trackPx - 30.dp.toPx()).coerceAtLeast(0f) }
|
||||
.clip(CircleShape)
|
||||
.background(pal.onSignal),
|
||||
)
|
||||
val start = Offset(thumbInset, size.height / 2f)
|
||||
val end = Offset(size.width - thumbInset, start.y)
|
||||
val thumb = Offset(start.x + (end.x - start.x) * fraction, start.y)
|
||||
val fill = if (hot) pal.signal else colors.onBackground
|
||||
drawLine(colors.onSurfaceVariant.copy(alpha = 0.35f), start, end, strokeWidth = 4.dp.toPx(), cap = StrokeCap.Round)
|
||||
if (fraction > 0) drawLine(fill, start, thumb, strokeWidth = 4.dp.toPx(), cap = StrokeCap.Round)
|
||||
drawCircle(fill, radius = 7.dp.toPx(), center = thumb)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -627,6 +651,9 @@ fun SettingsPage(
|
||||
scroll: androidx.compose.foundation.ScrollState? = null,
|
||||
scrollContent: Boolean = true,
|
||||
compactLayout: Boolean = false,
|
||||
/** Controls that remain below the title while the settings scroll, such as a tab strip. */
|
||||
header: (@Composable () -> Unit)? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
val scrollState = scroll ?: rememberScrollState()
|
||||
@@ -634,7 +661,7 @@ fun SettingsPage(
|
||||
val dim by animateFloatAsState(if (host.open != null) 0.6f else 1f, Motion.tw(220), label = "pageDim")
|
||||
val narrow = LocalNarrowPane.current && !compactLayout
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().padding(
|
||||
modifier = modifier.fillMaxSize().padding(
|
||||
horizontal = if (compactLayout) 14.dp else if (narrow) 16.dp else 22.dp,
|
||||
vertical = if (compactLayout) 6.dp else if (narrow) 12.dp else 18.dp,
|
||||
),
|
||||
@@ -699,6 +726,7 @@ fun SettingsPage(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (header != null) header()
|
||||
Rise(3, Modifier.weight(1f).fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
||||
@@ -477,6 +477,7 @@
|
||||
<string name="drawer_page_effects">Efectos</string>
|
||||
<string name="drawer_page_controls">Controles</string>
|
||||
<string name="drawer_page_components">Componentes</string>
|
||||
<string name="drawer_page_controller">Mando</string>
|
||||
<string name="drawer_page_settings">Ajustes</string>
|
||||
<string name="drawer_page_desc">Página %1$s</string>
|
||||
<string name="drawer_steam">Steam</string>
|
||||
@@ -492,8 +493,11 @@
|
||||
<string name="drawer_scaling_sharpness">Nitidez del escalado</string>
|
||||
<string name="drawer_effects">Efectos de pantalla</string>
|
||||
<string name="drawer_effects_hint">Se aplica al instante. Usa algo de GPU mientras haya un efecto activo.</string>
|
||||
<string name="drawer_look">Estilo</string>
|
||||
<string name="drawer_look">Ajustes predefinidos</string>
|
||||
<string name="drawer_look_custom">Personalizado</string>
|
||||
<string name="drawer_effects_advanced">Avanzado</string>
|
||||
<string name="widgets_expanded">Expandido</string>
|
||||
<string name="widgets_collapsed">Contraído</string>
|
||||
<string name="drawer_cas">Nitidez (CAS)</string>
|
||||
<string name="drawer_cas_level">Nivel de nitidez</string>
|
||||
<string name="drawer_fake_hdr">HDR simulado</string>
|
||||
@@ -517,6 +521,7 @@
|
||||
<string name="drawer_pc_keyboard">Teclado de PC</string>
|
||||
<string name="drawer_android_keyboard">Android</string>
|
||||
<string name="drawer_second_screen">Segunda pantalla</string>
|
||||
<string name="drawer_second_screen_mode">Modo</string>
|
||||
<string name="drawer_second_screen_shows">Muestra</string>
|
||||
<string name="drawer_android_apps">Apps de Android</string>
|
||||
<string name="drawer_launch_app">Abrir una app</string>
|
||||
@@ -2050,4 +2055,25 @@
|
||||
|
||||
<!-- %1$s is the limit's state (e.g. Child-process limit is on), %2$s what to do, %3$s the ADB command -->
|
||||
<string name="phantom_warning">%1$s. %2$s\n\n%3$s</string>
|
||||
<string name="display_session">Pantalla de la sesión</string>
|
||||
<string name="display_resolution">Resolución</string>
|
||||
<string name="display_game_fit">Ajuste de juegos 16:9</string>
|
||||
<string name="display_game_fit_hint">Cómo se ajustan los juegos 16:9 a esta pantalla. Próxima sesión.</string>
|
||||
<string name="display_game_fit_unavailable">Elige Igual a la pantalla o un tamaño personalizado más estrecho que 16:9.</string>
|
||||
<string name="display_preserve">Mantener proporciones</string>
|
||||
<string name="display_stretch">Estirar a la pantalla</string>
|
||||
<string name="display_image_scaling">Escalado de imagen</string>
|
||||
<string name="display_filter">Filtro de escalado</string>
|
||||
<string name="display_filter_hint">Compartido por Steam y Escritorio.</string>
|
||||
<string name="display_filter_live">Se aplica al instante. Compartido por Steam y Escritorio.</string>
|
||||
<string name="display_filter_note">FSR, GSR y NIS se aplican al ampliar la imagen de la sesión. Solo nitidez también funciona sin ampliar.</string>
|
||||
<string name="display_sharpness">Nitidez del filtro</string>
|
||||
<string name="display_frame_rate">Velocidad de fotogramas</string>
|
||||
<string name="display_window_compatibility">Compatibilidad de ventanas de juego</string>
|
||||
<string name="display_force_fullscreen">Forzar ventanas de juego a pantalla completa</string>
|
||||
<string name="display_force_fullscreen_hint">Mantiene los juegos a pantalla completa tras cambios de foco. Desactívalo si un juego se ve pequeño. Se aplica al instante durante una sesión.</string>
|
||||
<string name="display_force_fullscreen_live">Mantiene los juegos a pantalla completa tras cambios de foco. Se aplica al instante.</string>
|
||||
<string name="display_look_filter">%1$s Cambia el escalado a %2$s.</string>
|
||||
<string name="display_match_screen">Igual a la pantalla (%1$d×%2$d)</string>
|
||||
<string name="mode_tab_steam">Steam</string>
|
||||
</resources>
|
||||
@@ -475,6 +475,7 @@
|
||||
<string name="drawer_page_effects">エフェクト</string>
|
||||
<string name="drawer_page_controls">コントロール</string>
|
||||
<string name="drawer_page_components">コンポーネント</string>
|
||||
<string name="drawer_page_controller">コントローラー</string>
|
||||
<string name="drawer_page_settings">設定</string>
|
||||
<string name="drawer_page_desc">%1$sページ</string>
|
||||
<string name="drawer_steam">Steam</string>
|
||||
@@ -490,8 +491,11 @@
|
||||
<string name="drawer_scaling_sharpness">スケーリングのシャープネス</string>
|
||||
<string name="drawer_effects">画面エフェクト</string>
|
||||
<string name="drawer_effects_hint">即時反映。エフェクトがオンの間はGPUを少し消費します。</string>
|
||||
<string name="drawer_look">スタイル</string>
|
||||
<string name="drawer_look">プリセット</string>
|
||||
<string name="drawer_look_custom">カスタム</string>
|
||||
<string name="drawer_effects_advanced">詳細設定</string>
|
||||
<string name="widgets_expanded">展開</string>
|
||||
<string name="widgets_collapsed">折りたたみ</string>
|
||||
<string name="drawer_cas">シャープ化(CAS)</string>
|
||||
<string name="drawer_cas_level">シャープ化レベル</string>
|
||||
<string name="drawer_fake_hdr">疑似HDR</string>
|
||||
@@ -515,6 +519,7 @@
|
||||
<string name="drawer_pc_keyboard">PCキーボード</string>
|
||||
<string name="drawer_android_keyboard">Android</string>
|
||||
<string name="drawer_second_screen">セカンドスクリーン</string>
|
||||
<string name="drawer_second_screen_mode">モード</string>
|
||||
<string name="drawer_second_screen_shows">表示</string>
|
||||
<string name="drawer_android_apps">Androidアプリ</string>
|
||||
<string name="drawer_launch_app">アプリを起動</string>
|
||||
@@ -2012,4 +2017,25 @@
|
||||
|
||||
<!-- %1$s is the limit's state (e.g. Child-process limit is on), %2$s what to do, %3$s the ADB command -->
|
||||
<string name="phantom_warning">%1$s。%2$s\n\n%3$s</string>
|
||||
<string name="display_session">セッションの表示</string>
|
||||
<string name="display_resolution">解像度</string>
|
||||
<string name="display_game_fit">16:9ゲームの表示</string>
|
||||
<string name="display_game_fit_hint">16:9のゲームをこのディスプレイにどう合わせるか。次回のセッションから。</string>
|
||||
<string name="display_game_fit_unavailable">「画面に合わせる」か、16:9より縦長のカスタムサイズを選んでください。</string>
|
||||
<string name="display_preserve">縦横比を保持</string>
|
||||
<string name="display_stretch">画面いっぱいに引き伸ばす</string>
|
||||
<string name="display_image_scaling">画像のスケーリング</string>
|
||||
<string name="display_filter">スケーリングフィルター</string>
|
||||
<string name="display_filter_hint">Steamとデスクトップで共有されます。</string>
|
||||
<string name="display_filter_live">すぐに反映されます。Steamとデスクトップで共有されます。</string>
|
||||
<string name="display_filter_note">FSR、GSR、NISはセッションの映像を拡大するときに機能します。「シャープのみ」は拡大しなくても機能します。</string>
|
||||
<string name="display_sharpness">フィルターのシャープネス</string>
|
||||
<string name="display_frame_rate">フレームレート</string>
|
||||
<string name="display_window_compatibility">ゲームウィンドウの互換性</string>
|
||||
<string name="display_force_fullscreen">ゲームウィンドウを強制的に全画面にする</string>
|
||||
<string name="display_force_fullscreen_hint">フォーカスが変わってもゲームを全画面のまま保ちます。ゲームが小さく表示される場合はオフにしてください。セッション中もすぐに反映されます。</string>
|
||||
<string name="display_force_fullscreen_live">フォーカスが変わってもゲームを全画面のまま保ちます。すぐに反映されます。</string>
|
||||
<string name="display_look_filter">%1$s スケーリングを%2$sに設定します。</string>
|
||||
<string name="display_match_screen">画面に合わせる(%1$d×%2$d)</string>
|
||||
<string name="mode_tab_steam">Steam</string>
|
||||
</resources>
|
||||
@@ -475,6 +475,7 @@
|
||||
<string name="drawer_page_effects">효과</string>
|
||||
<string name="drawer_page_controls">컨트롤</string>
|
||||
<string name="drawer_page_components">구성 요소</string>
|
||||
<string name="drawer_page_controller">컨트롤러</string>
|
||||
<string name="drawer_page_settings">설정</string>
|
||||
<string name="drawer_page_desc">%1$s 페이지</string>
|
||||
<string name="drawer_steam">Steam</string>
|
||||
@@ -490,8 +491,11 @@
|
||||
<string name="drawer_scaling_sharpness">스케일링 선명도</string>
|
||||
<string name="drawer_effects">화면 효과</string>
|
||||
<string name="drawer_effects_hint">즉시 적용됩니다. 효과가 하나라도 켜져 있으면 GPU 시간을 약간 사용합니다.</string>
|
||||
<string name="drawer_look">스타일</string>
|
||||
<string name="drawer_look">프리셋</string>
|
||||
<string name="drawer_look_custom">사용자 정의</string>
|
||||
<string name="drawer_effects_advanced">고급</string>
|
||||
<string name="widgets_expanded">펼쳐짐</string>
|
||||
<string name="widgets_collapsed">접힘</string>
|
||||
<string name="drawer_cas">샤프닝(CAS)</string>
|
||||
<string name="drawer_cas_level">샤프닝 강도</string>
|
||||
<string name="drawer_fake_hdr">가상 HDR</string>
|
||||
@@ -515,6 +519,7 @@
|
||||
<string name="drawer_pc_keyboard">PC 키보드</string>
|
||||
<string name="drawer_android_keyboard">Android</string>
|
||||
<string name="drawer_second_screen">보조 화면</string>
|
||||
<string name="drawer_second_screen_mode">모드</string>
|
||||
<string name="drawer_second_screen_shows">표시</string>
|
||||
<string name="drawer_android_apps">Android 앱</string>
|
||||
<string name="drawer_launch_app">앱 실행</string>
|
||||
@@ -2012,4 +2017,25 @@
|
||||
|
||||
<!-- %1$s is the limit's state (e.g. Child-process limit is on), %2$s what to do, %3$s the ADB command -->
|
||||
<string name="phantom_warning">%1$s. %2$s\n\n%3$s</string>
|
||||
<string name="display_session">세션 디스플레이</string>
|
||||
<string name="display_resolution">해상도</string>
|
||||
<string name="display_game_fit">16:9 게임 맞춤</string>
|
||||
<string name="display_game_fit_hint">16:9 게임을 이 디스플레이에 맞추는 방식입니다. 다음 세션부터 적용됩니다.</string>
|
||||
<string name="display_game_fit_unavailable">화면에 맞춤 또는 16:9보다 좁은 사용자 정의 크기를 선택하세요.</string>
|
||||
<string name="display_preserve">비율 유지</string>
|
||||
<string name="display_stretch">화면에 맞게 늘이기</string>
|
||||
<string name="display_image_scaling">이미지 스케일링</string>
|
||||
<string name="display_filter">스케일링 필터</string>
|
||||
<string name="display_filter_hint">Steam과 데스크톱에서 공유됩니다.</string>
|
||||
<string name="display_filter_live">즉시 적용됩니다. Steam과 데스크톱에서 공유됩니다.</string>
|
||||
<string name="display_filter_note">FSR, GSR, NIS는 세션 이미지를 확대할 때 적용됩니다. 샤프닝 전용은 확대하지 않아도 작동합니다.</string>
|
||||
<string name="display_sharpness">필터 선명도</string>
|
||||
<string name="display_frame_rate">프레임 속도</string>
|
||||
<string name="display_window_compatibility">게임 창 호환성</string>
|
||||
<string name="display_force_fullscreen">게임 창 강제 전체 화면</string>
|
||||
<string name="display_force_fullscreen_hint">포커스가 바뀌어도 게임을 전체 화면으로 유지합니다. 게임이 작게 표시되면 끄세요. 세션 중에도 즉시 적용됩니다.</string>
|
||||
<string name="display_force_fullscreen_live">포커스가 바뀌어도 게임을 전체 화면으로 유지합니다. 즉시 적용됩니다.</string>
|
||||
<string name="display_look_filter">%1$s 스케일링을 %2$s(으)로 설정합니다.</string>
|
||||
<string name="display_match_screen">화면에 맞춤(%1$d×%2$d)</string>
|
||||
<string name="mode_tab_steam">Steam</string>
|
||||
</resources>
|
||||
@@ -475,6 +475,7 @@
|
||||
<string name="drawer_page_effects">画面效果</string>
|
||||
<string name="drawer_page_controls">控制</string>
|
||||
<string name="drawer_page_components">组件</string>
|
||||
<string name="drawer_page_controller">手柄</string>
|
||||
<string name="drawer_page_settings">设置</string>
|
||||
<string name="drawer_page_desc">%1$s 页面</string>
|
||||
<string name="drawer_steam">Steam</string>
|
||||
@@ -490,8 +491,11 @@
|
||||
<string name="drawer_scaling_sharpness">缩放锐度</string>
|
||||
<string name="drawer_effects">屏幕效果</string>
|
||||
<string name="drawer_effects_hint">实时生效。任何效果开启期间会占用少量 GPU 时间。</string>
|
||||
<string name="drawer_look">观感</string>
|
||||
<string name="drawer_look">预设</string>
|
||||
<string name="drawer_look_custom">自定义</string>
|
||||
<string name="drawer_effects_advanced">高级</string>
|
||||
<string name="widgets_expanded">已展开</string>
|
||||
<string name="widgets_collapsed">已折叠</string>
|
||||
<string name="drawer_cas">锐化(CAS)</string>
|
||||
<string name="drawer_cas_level">锐化级别</string>
|
||||
<string name="drawer_fake_hdr">伪 HDR</string>
|
||||
@@ -515,6 +519,7 @@
|
||||
<string name="drawer_pc_keyboard">PC 键盘</string>
|
||||
<string name="drawer_android_keyboard">Android</string>
|
||||
<string name="drawer_second_screen">副屏</string>
|
||||
<string name="drawer_second_screen_mode">模式</string>
|
||||
<string name="drawer_second_screen_shows">显示</string>
|
||||
<string name="drawer_android_apps">Android 应用</string>
|
||||
<string name="drawer_launch_app">启动应用</string>
|
||||
@@ -2012,4 +2017,25 @@
|
||||
|
||||
<!-- %1$s is the limit's state (e.g. Child-process limit is on), %2$s what to do, %3$s the ADB command -->
|
||||
<string name="phantom_warning">%1$s。%2$s\n\n%3$s</string>
|
||||
<string name="display_session">会话显示</string>
|
||||
<string name="display_resolution">分辨率</string>
|
||||
<string name="display_game_fit">16:9 游戏适配</string>
|
||||
<string name="display_game_fit_hint">16:9 游戏如何适配此屏幕。下次会话生效。</string>
|
||||
<string name="display_game_fit_unavailable">请选择“匹配屏幕”或比 16:9 更窄的自定义尺寸。</string>
|
||||
<string name="display_preserve">保持比例</string>
|
||||
<string name="display_stretch">拉伸至屏幕</string>
|
||||
<string name="display_image_scaling">画面缩放</string>
|
||||
<string name="display_filter">缩放滤镜</string>
|
||||
<string name="display_filter_hint">由 Steam 和桌面共享。</string>
|
||||
<string name="display_filter_live">即时生效。由 Steam 和桌面共享。</string>
|
||||
<string name="display_filter_note">FSR、GSR 和 NIS 在放大会话画面时生效。“仅锐化”在不放大时也可用。</string>
|
||||
<string name="display_sharpness">滤镜锐度</string>
|
||||
<string name="display_frame_rate">帧率</string>
|
||||
<string name="display_window_compatibility">游戏窗口兼容性</string>
|
||||
<string name="display_force_fullscreen">强制游戏窗口全屏</string>
|
||||
<string name="display_force_fullscreen_hint">焦点切换后仍让游戏保持全屏。如果游戏画面变小,请关闭此项。会话中即时生效。</string>
|
||||
<string name="display_force_fullscreen_live">焦点切换后仍让游戏保持全屏。即时生效。</string>
|
||||
<string name="display_look_filter">%1$s 将缩放设为 %2$s。</string>
|
||||
<string name="display_match_screen">匹配屏幕(%1$d×%2$d)</string>
|
||||
<string name="mode_tab_steam">Steam</string>
|
||||
</resources>
|
||||
@@ -475,6 +475,7 @@
|
||||
<string name="drawer_page_effects">效果</string>
|
||||
<string name="drawer_page_controls">控制</string>
|
||||
<string name="drawer_page_components">元件</string>
|
||||
<string name="drawer_page_controller">控制器</string>
|
||||
<string name="drawer_page_settings">設定</string>
|
||||
<string name="drawer_page_desc">%1$s頁面</string>
|
||||
<string name="drawer_steam">Steam</string>
|
||||
@@ -490,8 +491,11 @@
|
||||
<string name="drawer_scaling_sharpness">縮放銳利度</string>
|
||||
<string name="drawer_effects">畫面效果</string>
|
||||
<string name="drawer_effects_hint">即時生效。開啟任何效果都會略為佔用 GPU 時間。</string>
|
||||
<string name="drawer_look">風格</string>
|
||||
<string name="drawer_look">預設</string>
|
||||
<string name="drawer_look_custom">自訂</string>
|
||||
<string name="drawer_effects_advanced">進階</string>
|
||||
<string name="widgets_expanded">已展開</string>
|
||||
<string name="widgets_collapsed">已收合</string>
|
||||
<string name="drawer_cas">銳化 (CAS)</string>
|
||||
<string name="drawer_cas_level">銳化程度</string>
|
||||
<string name="drawer_fake_hdr">模擬 HDR</string>
|
||||
@@ -515,6 +519,7 @@
|
||||
<string name="drawer_pc_keyboard">電腦鍵盤</string>
|
||||
<string name="drawer_android_keyboard">Android</string>
|
||||
<string name="drawer_second_screen">第二螢幕</string>
|
||||
<string name="drawer_second_screen_mode">模式</string>
|
||||
<string name="drawer_second_screen_shows">顯示</string>
|
||||
<string name="drawer_android_apps">Android 應用程式</string>
|
||||
<string name="drawer_launch_app">啟動應用程式</string>
|
||||
@@ -2012,4 +2017,25 @@
|
||||
|
||||
<!-- %1$s is the limit's state (e.g. Child-process limit is on), %2$s what to do, %3$s the ADB command -->
|
||||
<string name="phantom_warning">%1$s。%2$s\n\n%3$s</string>
|
||||
<string name="display_session">會話顯示</string>
|
||||
<string name="display_resolution">解像度</string>
|
||||
<string name="display_game_fit">16:9 遊戲顯示方式</string>
|
||||
<string name="display_game_fit_hint">16:9 遊戲如何配合此螢幕。下次會話生效。</string>
|
||||
<string name="display_game_fit_unavailable">請選擇「配合螢幕」或比 16:9 更窄的自訂尺寸。</string>
|
||||
<string name="display_preserve">保持比例</string>
|
||||
<string name="display_stretch">拉伸至螢幕</string>
|
||||
<string name="display_image_scaling">畫面縮放</string>
|
||||
<string name="display_filter">縮放濾鏡</string>
|
||||
<string name="display_filter_hint">Steam 和桌面共用。</string>
|
||||
<string name="display_filter_live">即時生效。Steam 和桌面共用。</string>
|
||||
<string name="display_filter_note">FSR、GSR 和 NIS 會在放大會話畫面時套用。「只銳化」不放大也能使用。</string>
|
||||
<string name="display_sharpness">濾鏡銳利度</string>
|
||||
<string name="display_frame_rate">幀率</string>
|
||||
<string name="display_window_compatibility">遊戲視窗兼容性</string>
|
||||
<string name="display_force_fullscreen">強制遊戲視窗全螢幕</string>
|
||||
<string name="display_force_fullscreen_hint">焦點轉換後仍令遊戲保持全螢幕。如遊戲畫面變小,請關閉此項。會話中即時生效。</string>
|
||||
<string name="display_force_fullscreen_live">焦點轉換後仍令遊戲保持全螢幕。即時生效。</string>
|
||||
<string name="display_look_filter">%1$s 將縮放設為 %2$s。</string>
|
||||
<string name="display_match_screen">配合螢幕(%1$d×%2$d)</string>
|
||||
<string name="mode_tab_steam">Steam</string>
|
||||
</resources>
|
||||
@@ -475,6 +475,7 @@
|
||||
<string name="drawer_page_effects">效果</string>
|
||||
<string name="drawer_page_controls">控制</string>
|
||||
<string name="drawer_page_components">元件</string>
|
||||
<string name="drawer_page_controller">控制器</string>
|
||||
<string name="drawer_page_settings">設定</string>
|
||||
<string name="drawer_page_desc">%1$s 頁面</string>
|
||||
<string name="drawer_steam">Steam</string>
|
||||
@@ -490,8 +491,11 @@
|
||||
<string name="drawer_scaling_sharpness">縮放銳利度</string>
|
||||
<string name="drawer_effects">畫面效果</string>
|
||||
<string name="drawer_effects_hint">即時套用。開啟任何效果時會佔用少量 GPU 時間。</string>
|
||||
<string name="drawer_look">風格</string>
|
||||
<string name="drawer_look">預設集</string>
|
||||
<string name="drawer_look_custom">自訂</string>
|
||||
<string name="drawer_effects_advanced">進階</string>
|
||||
<string name="widgets_expanded">已展開</string>
|
||||
<string name="widgets_collapsed">已收合</string>
|
||||
<string name="drawer_cas">銳化(CAS)</string>
|
||||
<string name="drawer_cas_level">銳化程度</string>
|
||||
<string name="drawer_fake_hdr">模擬 HDR</string>
|
||||
@@ -515,6 +519,7 @@
|
||||
<string name="drawer_pc_keyboard">電腦鍵盤</string>
|
||||
<string name="drawer_android_keyboard">Android</string>
|
||||
<string name="drawer_second_screen">第二螢幕</string>
|
||||
<string name="drawer_second_screen_mode">模式</string>
|
||||
<string name="drawer_second_screen_shows">顯示</string>
|
||||
<string name="drawer_android_apps">Android 應用程式</string>
|
||||
<string name="drawer_launch_app">啟動應用程式</string>
|
||||
@@ -2012,4 +2017,25 @@
|
||||
|
||||
<!-- %1$s is the limit's state (e.g. Child-process limit is on), %2$s what to do, %3$s the ADB command -->
|
||||
<string name="phantom_warning">%1$s。%2$s\n\n%3$s</string>
|
||||
<string name="display_session">工作階段顯示</string>
|
||||
<string name="display_resolution">解析度</string>
|
||||
<string name="display_game_fit">16:9 遊戲顯示方式</string>
|
||||
<string name="display_game_fit_hint">16:9 遊戲如何配合此螢幕。下次工作階段生效。</string>
|
||||
<string name="display_game_fit_unavailable">請選擇「符合螢幕」或比 16:9 更窄的自訂尺寸。</string>
|
||||
<string name="display_preserve">維持比例</string>
|
||||
<string name="display_stretch">延展至螢幕</string>
|
||||
<string name="display_image_scaling">畫面縮放</string>
|
||||
<string name="display_filter">縮放濾鏡</string>
|
||||
<string name="display_filter_hint">Steam 和桌面共用。</string>
|
||||
<string name="display_filter_live">立即生效。Steam 和桌面共用。</string>
|
||||
<string name="display_filter_note">FSR、GSR 和 NIS 會在放大工作階段畫面時套用。「僅銳化」不放大也能使用。</string>
|
||||
<string name="display_sharpness">濾鏡銳利度</string>
|
||||
<string name="display_frame_rate">影格率</string>
|
||||
<string name="display_window_compatibility">遊戲視窗相容性</string>
|
||||
<string name="display_force_fullscreen">強制遊戲視窗全螢幕</string>
|
||||
<string name="display_force_fullscreen_hint">焦點變更後仍讓遊戲保持全螢幕。若遊戲畫面變小,請關閉此項。工作階段中立即生效。</string>
|
||||
<string name="display_force_fullscreen_live">焦點變更後仍讓遊戲保持全螢幕。立即生效。</string>
|
||||
<string name="display_look_filter">%1$s 將縮放設為 %2$s。</string>
|
||||
<string name="display_match_screen">符合螢幕(%1$d×%2$d)</string>
|
||||
<string name="mode_tab_steam">Steam</string>
|
||||
</resources>
|
||||
@@ -476,6 +476,7 @@
|
||||
<string name="drawer_page_effects">Effects</string>
|
||||
<string name="drawer_page_controls">Controls</string>
|
||||
<string name="drawer_page_components">Components</string>
|
||||
<string name="drawer_page_controller">Controller</string>
|
||||
<string name="drawer_page_settings">Settings</string>
|
||||
<string name="drawer_page_desc">%1$s page</string>
|
||||
<string name="drawer_steam">Steam</string>
|
||||
@@ -491,8 +492,11 @@
|
||||
<string name="drawer_scaling_sharpness">Scaling sharpness</string>
|
||||
<string name="drawer_effects">Screen effects</string>
|
||||
<string name="drawer_effects_hint">Live. Costs a little GPU time while any effect is on.</string>
|
||||
<string name="drawer_look">Look</string>
|
||||
<string name="drawer_look">Presets</string>
|
||||
<string name="drawer_look_custom">Custom</string>
|
||||
<string name="drawer_effects_advanced">Advanced</string>
|
||||
<string name="widgets_expanded">Expanded</string>
|
||||
<string name="widgets_collapsed">Collapsed</string>
|
||||
<string name="drawer_cas">Sharpening (CAS)</string>
|
||||
<string name="drawer_cas_level">Sharpening level</string>
|
||||
<string name="drawer_fake_hdr">Fake HDR</string>
|
||||
@@ -516,6 +520,7 @@
|
||||
<string name="drawer_pc_keyboard">PC keyboard</string>
|
||||
<string name="drawer_android_keyboard">Android</string>
|
||||
<string name="drawer_second_screen">Second screen</string>
|
||||
<string name="drawer_second_screen_mode">Mode</string>
|
||||
<string name="drawer_second_screen_shows">Shows</string>
|
||||
<string name="drawer_android_apps">Android apps</string>
|
||||
<string name="drawer_launch_app">Launch an app</string>
|
||||
@@ -1902,4 +1907,25 @@
|
||||
<string name="driver_tag_bundle">ANDROID + LINUX</string>
|
||||
<!-- %1$s is the limit's state (e.g. Child-process limit is on), %2$s what to do, %3$s the ADB command -->
|
||||
<string name="phantom_warning">%1$s. %2$s\n\n%3$s</string>
|
||||
<string name="display_session">Session display</string>
|
||||
<string name="display_resolution">Resolution</string>
|
||||
<string name="display_game_fit">16:9 game fit</string>
|
||||
<string name="display_game_fit_hint">How 16:9 games fit this display. Next session.</string>
|
||||
<string name="display_game_fit_unavailable">Choose Match screen or a custom size narrower than 16:9.</string>
|
||||
<string name="display_preserve">Keep proportions</string>
|
||||
<string name="display_stretch">Stretch to display</string>
|
||||
<string name="display_image_scaling">Image scaling</string>
|
||||
<string name="display_filter">Scaling filter</string>
|
||||
<string name="display_filter_hint">Shared by Steam and Desktop.</string>
|
||||
<string name="display_filter_live">Applies live. Shared by Steam and Desktop.</string>
|
||||
<string name="display_filter_note">FSR, GSR and NIS apply when enlarging the session image. Sharpen only also works without enlargement.</string>
|
||||
<string name="display_sharpness">Filter sharpness</string>
|
||||
<string name="display_frame_rate">Frame rate</string>
|
||||
<string name="display_window_compatibility">Game window compatibility</string>
|
||||
<string name="display_force_fullscreen">Force game windows fullscreen</string>
|
||||
<string name="display_force_fullscreen_hint">Keeps games fullscreen after focus changes. Turn off if a game draws small. Applies live during a session.</string>
|
||||
<string name="display_force_fullscreen_live">Keeps games fullscreen after focus changes. Applies live.</string>
|
||||
<string name="display_look_filter">%1$s Sets scaling to %2$s.</string>
|
||||
<string name="display_match_screen">Match screen (%1$d×%2$d)</string>
|
||||
<string name="mode_tab_steam">Steam</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.droiddeck.launcher.session
|
||||
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
|
||||
class SessionDisplayTest {
|
||||
@Test fun explicitPresetsAndMatchScreenResolveWithoutAnyAspectSetting() {
|
||||
assertEquals(1280 to 720, SessionDisplay.resolveChoice(1440 to 1080, "1280x720"))
|
||||
assertEquals(1600 to 900, SessionDisplay.resolveChoice(1280 to 720, "1600x900"))
|
||||
assertEquals(1440 to 1080, SessionDisplay.resolveChoice(1080 to 1440, SessionDisplay.MATCH_SCREEN))
|
||||
assertEquals(2400 to 1080, SessionDisplay.resolveChoice(1080 to 2400, SessionDisplay.MATCH_SCREEN))
|
||||
}
|
||||
|
||||
@Test fun panelOptionReplacesTheIdenticalPresetAndTracksPanelChanges() {
|
||||
val thor = SessionDisplay.resolutionOptions(1920 to 1080)
|
||||
assertEquals(listOf("1280x720", "1600x900", SessionDisplay.MATCH_SCREEN), thor)
|
||||
val small = SessionDisplay.resolutionOptions(1280 to 720)
|
||||
assertFalse(small.contains("1280x720"))
|
||||
assertEquals(1280 to 720, SessionDisplay.resolveChoice(1280 to 720, SessionDisplay.MATCH_SCREEN))
|
||||
assertEquals(2560 to 1440, SessionDisplay.resolveChoice(2560 to 1440, SessionDisplay.MATCH_SCREEN))
|
||||
}
|
||||
|
||||
@Test fun resolutionCapsPreserveTheSelectedShapeWithoutExceedingPanelHeight() {
|
||||
assertEquals(1280 to 720, SessionDisplay.resolve(1920 to 1080, 720, SessionPrefs.SHAPE_AUTO))
|
||||
assertEquals(960 to 720, SessionDisplay.resolve(1440 to 1080, 720, SessionPrefs.SHAPE_EXACT))
|
||||
assertEquals(1280 to 720, SessionDisplay.resolve(1440 to 1080, 720, SessionPrefs.SHAPE_AUTO))
|
||||
assertEquals(1280 to 720, SessionDisplay.resolve(1280 to 720, 1080, SessionPrefs.SHAPE_AUTO))
|
||||
}
|
||||
|
||||
@Test fun noCapUsesPanelHeightWhileCustomOverridesBothCapAndAspect() {
|
||||
assertEquals(1440 to 1080, SessionDisplay.resolve(1440 to 1080, 0, SessionPrefs.SHAPE_EXACT))
|
||||
assertEquals(1920 to 1080, SessionDisplay.resolve(1440 to 1080, 0, SessionPrefs.SHAPE_WIDE))
|
||||
assertEquals(1024 to 768, SessionDisplay.resolve(1920 to 1080, 720, SessionPrefs.SHAPE_WIDE, 1024 to 768))
|
||||
}
|
||||
|
||||
@Test fun orientationAndOddDimensionsKeepTheExistingEvenLandscapeDisplay() {
|
||||
assertEquals(1920 to 1080, SessionDisplay.resolve(1081 to 1921, 0, SessionPrefs.SHAPE_EXACT))
|
||||
assertEquals(1600 to 720, SessionDisplay.resolve(2400 to 1080, 720, SessionPrefs.SHAPE_AUTO))
|
||||
}
|
||||
|
||||
@Test fun gameStretchAvailabilityMatchesTheRuntimeIncludingRounding() {
|
||||
assertTrue(SessionDisplay.canStretch16x9(960 to 720))
|
||||
assertFalse(SessionDisplay.canStretch16x9(1280 to 720))
|
||||
assertFalse(SessionDisplay.canStretch16x9(1600 to 720))
|
||||
val wide = SessionDisplay.resolve(1440 to 1080, 0, SessionPrefs.SHAPE_WIDE)
|
||||
assertFalse(SessionDisplay.canStretch16x9(wide))
|
||||
}
|
||||
|
||||
@Test fun removedFilterAliasesRetainTheirEquivalentBehavior() {
|
||||
assertEquals(0, SessionPrefs.canonicalUpscaler(1))
|
||||
assertEquals(4, SessionPrefs.canonicalUpscaler(5))
|
||||
assertEquals(0, SessionPrefs.canonicalUpscaler(99))
|
||||
assertFalse(SessionPrefs.upscalerHasSharpness(0))
|
||||
assertFalse(SessionPrefs.upscalerHasSharpness(1))
|
||||
assertFalse(SessionPrefs.upscalerHasSharpness(2))
|
||||
for (mode in 3..8) assertTrue(SessionPrefs.upscalerHasSharpness(mode))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.droiddeck.launcher.session
|
||||
|
||||
import android.content.Context
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [28], manifest = Config.NONE)
|
||||
class SessionScalingPrefsTest {
|
||||
@Before fun clearSettings() {
|
||||
RuntimeEnvironment.getApplication().getSharedPreferences("session", Context.MODE_PRIVATE).edit().clear().commit()
|
||||
}
|
||||
|
||||
@Test fun explicitLegacyAspectAndCapKeepTheirSizeUntilAResolutionIsPicked() {
|
||||
val context = RuntimeEnvironment.getApplication()
|
||||
val prefs = context.getSharedPreferences("session", Context.MODE_PRIVATE)
|
||||
prefs.edit().putString("shape", SessionPrefs.SHAPE_EXACT).putInt("resolutionCap.steam", 900).commit()
|
||||
assertEquals("1200x900", SessionPrefs.resolutionChoice(context, "steam", 1440 to 1080))
|
||||
SessionPrefs.setResolutionChoice(context, "steam", "1600x900")
|
||||
prefs.edit().putString("shape", SessionPrefs.SHAPE_WIDE).commit()
|
||||
assertEquals("1600x900", SessionPrefs.resolutionChoice(context, "steam", 1440 to 1080))
|
||||
assertTrue(SessionPrefs.resolutionChosen(context, "steam"))
|
||||
}
|
||||
|
||||
@Test fun matchScreenOverridesLegacyCustomAndAspectWithoutAffectingTheOtherMode() {
|
||||
val context = RuntimeEnvironment.getApplication()
|
||||
val prefs = context.getSharedPreferences("session", Context.MODE_PRIVATE)
|
||||
prefs.edit().putString("shape", SessionPrefs.SHAPE_WIDE).putString("customRes.steam", "1024x768")
|
||||
.putString("customRes.lxqt", "800x600").commit()
|
||||
assertEquals("1024x768", SessionPrefs.resolutionChoice(context, "steam", 1440 to 1080))
|
||||
SessionPrefs.setResolutionChoice(context, "steam", SessionDisplay.MATCH_SCREEN)
|
||||
val choice = SessionPrefs.resolutionChoice(context, "steam", 1440 to 1080)
|
||||
assertEquals(1440 to 1080, SessionDisplay.resolveChoice(1440 to 1080, choice))
|
||||
assertEquals("800x600", SessionPrefs.resolutionChoice(context, "lxqt", 1440 to 1080))
|
||||
}
|
||||
|
||||
@Test fun freshSettingsUseTheConcreteDefaultAndCustomSizesAreNormalized() {
|
||||
val context = RuntimeEnvironment.getApplication()
|
||||
assertEquals("1280x720", SessionPrefs.resolutionChoice(context, "steam", 2400 to 1080))
|
||||
SessionPrefs.setResolutionChoice(context, "steam", "1025×769")
|
||||
assertEquals("1024x768", SessionPrefs.resolutionChoice(context, "steam", 2400 to 1080))
|
||||
}
|
||||
|
||||
@Test fun legacySavedFiltersStillResolveToAVisibleEquivalentChoice() {
|
||||
val context = RuntimeEnvironment.getApplication()
|
||||
val prefs = context.getSharedPreferences("session", Context.MODE_PRIVATE)
|
||||
prefs.edit().putInt("upscaler", 1).putInt("upscaleSharpness", 65).commit()
|
||||
assertEquals(0, SessionPrefs.upscaler(context))
|
||||
prefs.edit().putInt("upscaler", 5).commit()
|
||||
assertEquals(4, SessionPrefs.upscaler(context))
|
||||
assertEquals(65, SessionPrefs.upscaleSharpness(context))
|
||||
SessionPrefs.setUpscaler(context, 5)
|
||||
assertEquals(4, prefs.getInt("upscaler", -1))
|
||||
SessionPrefs.setUpscaler(context, 1)
|
||||
assertEquals(0, prefs.getInt("upscaler", -1))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.droiddeck.launcher.ui
|
||||
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
|
||||
class SessionDrawerPageTest {
|
||||
@Test fun singleScreenNavigationStartsWithControllerAndSkipsSecondScreen() {
|
||||
val pages = sessionDrawerPages(false)
|
||||
assertEquals(SessionDrawerPage.CONTROLLER, pages.first())
|
||||
assertFalse(SessionDrawerPage.SECOND_SCREEN in pages)
|
||||
assertEquals(SessionDrawerPage.DISPLAY, pages.step(SessionDrawerPage.CONTROLLER, 1))
|
||||
assertEquals(SessionDrawerPage.SESSION, pages.step(SessionDrawerPage.CONTROLLER, -1))
|
||||
assertEquals(SessionDrawerPage.CONTROLLER, pages.step(SessionDrawerPage.SESSION, 1))
|
||||
}
|
||||
|
||||
@Test fun connectedSecondaryDisplayAddsItsPageAfterControllerAndPreservesOtherPages() {
|
||||
val connected = sessionDrawerPages(true)
|
||||
assertEquals(SessionDrawerPage.SECOND_SCREEN, connected.step(SessionDrawerPage.CONTROLLER, 1))
|
||||
assertEquals(SessionDrawerPage.DISPLAY, connected.step(SessionDrawerPage.SECOND_SCREEN, 1))
|
||||
assertEquals(sessionDrawerPages(false), connected.filter { it != SessionDrawerPage.SECOND_SCREEN })
|
||||
for (pages in listOf(connected, sessionDrawerPages(false))) {
|
||||
assertEquals(SessionDrawerPage.EFFECTS, pages.step(SessionDrawerPage.DISPLAY, 1))
|
||||
assertEquals(SessionDrawerPage.GAMES, pages.step(SessionDrawerPage.EFFECTS, 1))
|
||||
assertEquals(SessionDrawerPage.SESSION, pages.step(SessionDrawerPage.GAMES, 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user