Android: make the settings usable again, and declutter the library menus

CollapsibleSection had stopped collapsing. Its own comment describes "a
tappable header that shows or hides its content ... to de-bloat long
settings lists", but the body rendered a plain title and called content()
unconditionally, with initiallyExpanded marked UNUSED_PARAMETER. All 20
sections across Pad, Renderer, Performance and Fixes were therefore
permanently open, which is why the settings became unusable in landscape:
not too many options, but every option in a tab on screen at once.
Restoring it shortens four tabs at a stroke. State is rememberSaveable so
a rotation no longer re-collapses what was just opened, landscape being
where the long lists hurt most; Renderer opens on Display & Resolution
and Performance on Speedhacks so neither looks empty.

The Fixes tab is now Advanced, and absorbed the GameDB fixes from
Performance and the whole Recompiler tab. Both are debugging controls that
the GameDB already applies per game, so they do not belong in the tab
people open to change speed. Fourteen tabs become twelve; the search index
and the per-tab reset ownership moved with the settings, so searching
"Skip MPEG" lands on Advanced and resetting Performance no longer resets
fixes it does not show.

Skip BIOS now defaults on. "How do I skip the boot animation" is one of
the most-asked questions in the Discord and desktop PCSX2 fast-boots too.
Only fresh installs change: the saved JSON always carries the key, so
nobody's boot behaviour is altered by an update.

The library overflow menu drops from eleven rows to eight with separators.
Open navigation goes entirely (the hamburger is on the same toolbar) and
Game names in grid moves to App settings beside cover size and opacity,
where every other library-appearance preference already lives. Setup /
Change Folders and Exit MOVE to the drawer rather than being deleted -
each had exactly one entry point, so removing them would have stranded
anyone needing to change their ROM folder and left no in-app quit. Exit
keeps its confirmation and gets a real power-symbol drawable: the "⏻"
glyph is absent from most Android system fonts and rendered as tofu.

About leaves the settings tab strip for the drawer, next to the other
links. It is a read-only page that was costing a tab slot on every visit.

Installed patches and cheats can be collapsed, so a pnach carrying
hundreds of downloaded codes can be folded away.

Reported by a community member on Discord.
This commit is contained in:
jpolo1224
2026-07-26 14:53:48 -04:00
committed by jpolo1224
parent 2c4f5500c7
commit 7c05cfd117
15 changed files with 266 additions and 154 deletions
@@ -167,8 +167,13 @@ data class Settings(
val enableWideScreenPatches: Boolean = false,
/** EmuCore/EnableNoInterlacingPatches — no-interlacing patches. */
val enableNoInterlacingPatches: Boolean = false,
/** EmuCore/EnableFastBoot — skip BIOS splash and boot straight to the game. */
val enableFastBoot: Boolean = false,
/** EmuCore/EnableFastBoot — skip BIOS splash and boot straight to the game.
*
* Default ON: "how do I skip the boot animation" is one of the most-asked questions in the
* Discord, and desktop PCSX2 fast-boots by default too. Only fresh installs are affected —
* the saved JSON always carries this key, so anyone who already has a value keeps it rather
* than having their boot behaviour changed under them by an update. */
val enableFastBoot: Boolean = true,
/** EmuCore/HostFs — host: filesystem access in the VM, for ELF/homebrew and mods
* (e.g. modded Persona 3 FES). Per-game capable; applies on the next game boot. */
val hostFs: Boolean = false,
@@ -35,7 +35,6 @@ enum class SettingsCategory {
OnScreen,
Skins,
Advanced,
Recompiler,
Patches,
About,
}
@@ -68,6 +68,10 @@ import com.armsx2.ui.settings.controllerFocusable
// gold trophy rather than the flat monochrome nav glyphs.
private val TrophyGold = Color(0xFFFFC93C)
// Exit is the only row here that ends your session, so it gets the standard power-red rather
// than the neutral row tint — the same reason the trophy keeps its gold.
private val ExitRed = Color(0xFFE60012)
// Community/project links for the drawer's About section. Plain https on purpose: Android App
// Links hand these to the Discord/GitHub apps when they're installed and fall back to the
// browser when they aren't, so there's no app-specific scheme to special-case.
@@ -162,6 +166,27 @@ fun NavigationDrawer(
private fun DrawerContent(selected: AppRoute, onNavigate: (AppRoute) -> Unit, onDismiss: () -> Unit) {
val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE
val context = LocalContext.current
// Exit moved here from the library overflow menu; it keeps its confirmation, which is the whole
// point of the row — quitting mid-session without one loses whatever is not saved.
val exitConfirm = androidx.compose.runtime.remember { androidx.compose.runtime.mutableStateOf(false) }
if (exitConfirm.value) {
androidx.compose.material3.AlertDialog(
onDismissRequest = { exitConfirm.value = false },
title = { androidx.compose.material3.Text(str("games.exit.title")) },
text = { androidx.compose.material3.Text(str("games.exit.message")) },
confirmButton = {
androidx.compose.material3.TextButton(onClick = {
exitConfirm.value = false
MainActivityRuntime.exitApp()
}) { androidx.compose.material3.Text(str("games.toolbar.exit")) }
},
dismissButton = {
androidx.compose.material3.TextButton(onClick = { exitConfirm.value = false }) {
androidx.compose.material3.Text(str("action.cancel"))
}
},
)
}
// Intuitive glyphs that read as what they do — matching the in-game overlay's emoji-icon style
// (the old box-drawing characters like ▦ ◉ ⌁ ✦ were unclear per tester feedback).
val primary = listOf(
@@ -174,6 +199,10 @@ private fun DrawerContent(selected: AppRoute, onNavigate: (AppRoute) -> Unit, on
DrawerItem("action.settings", "⚙️", AppRoute.Settings()),
)
val managers = listOf(
// Moved off the library overflow menu, which was its only entry point. Sits first, beside
// BIOS Location: both answer "where are my files".
DrawerItem("games.overflow.setup", "📂",
onAction = { MainActivityRuntime.reopenSetup(); onDismiss() }),
DrawerItem("setup.step.bios.title", "📀", AppRoute.BiosManager()),
DrawerItem("memcard.title", "💾", AppRoute.MemoryCardManager),
DrawerItem("savestate.title.loadManage", "📥", AppRoute.SaveManager),
@@ -189,6 +218,15 @@ private fun DrawerContent(selected: AppRoute, onNavigate: (AppRoute) -> Unit, on
DrawerItem("about.github", "🐙", iconRes = com.armsx2.R.drawable.ic_github,
onAction = { openExternalUrl(context, GithubUrl); onDismiss() }),
DrawerItem("about.website", "🌐", onAction = { openExternalUrl(context, WebsiteUrl); onDismiss() }),
// About left the settings tab strip: it is a read-only page, not a setting, and it sat in
// the tab row costing a slot on every settings visit.
DrawerItem("about.title", "", AppRoute.About),
)
// Exit gets its own trailing section. It was briefly filed under ABOUT, next to the Discord and
// GitHub links, where nobody would think to look for "quit".
val session = listOf(
DrawerItem("games.toolbar.exit", "", iconRes = com.armsx2.R.drawable.ic_power,
iconTint = ExitRed, onAction = { exitConfirm.value = true }),
)
Column(
@@ -216,6 +254,10 @@ private fun DrawerContent(selected: AppRoute, onNavigate: (AppRoute) -> Unit, on
HorizontalDivider(color = MaterialTheme.colorScheme.outline.copy(alpha = 0.45f))
Spacer(Modifier.height(14.dp))
DrawerSection(str("about.section.header"), about, selected, onNavigate)
Spacer(Modifier.height(14.dp))
HorizontalDivider(color = MaterialTheme.colorScheme.outline.copy(alpha = 0.45f))
Spacer(Modifier.height(14.dp))
DrawerSection(str("games.section.app"), session, selected, onNavigate)
}
}
@@ -758,9 +758,6 @@ private fun LibraryOverflowMenu(
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold,
)
LibraryOverflowItem("", str("games.overflow.openNavigation")) {
closeThen(onOpenNavigation)
}
LibraryOverflowItem(
glyph = "AZ",
label = str("games.overflow.sortTitle"),
@@ -775,6 +772,7 @@ private fun LibraryOverflowMenu(
) {
closeThen { onSort(HomeSort.RecentlyPlayed) }
}
OverflowSeparator()
LibraryOverflowItem(
glyph = if (use3dCovers) "3D" else "2D",
label = str("games.overflow.coverStyle"),
@@ -782,13 +780,6 @@ private fun LibraryOverflowMenu(
) {
closeThen(onToggleCoverStyle)
}
LibraryOverflowItem(
glyph = "Aa",
label = str("games.overflow.gridNames"),
trailing = if (showGridNames) str("common.on") else str("common.off"),
) {
closeThen(onToggleGridNames)
}
LibraryOverflowItem(
glyph = "Aa",
label = str("games.overflow.customNames"),
@@ -810,6 +801,7 @@ private fun LibraryOverflowMenu(
) {
closeThen(onToggleShowHidden)
}
OverflowSeparator()
LibraryOverflowItem("", str("games.background.choose")) {
closeThen(onChooseBackground)
}
@@ -818,15 +810,17 @@ private fun LibraryOverflowMenu(
closeThen(onClearBackground)
}
}
LibraryOverflowItem("", str("games.overflow.setup")) {
closeThen { MainActivityRuntime.reopenSetup() }
}
LibraryOverflowItem("", str("games.toolbar.exit")) {
closeThen(onExitApp)
}
}
}
@Composable
private fun OverflowSeparator() {
androidx.compose.material3.HorizontalDivider(
modifier = Modifier.padding(horizontal = 18.dp, vertical = 6.dp),
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.30f),
)
}
@Composable
private fun LibraryOverflowItem(
glyph: String,
@@ -401,11 +401,33 @@ private fun CollapsibleOnlineSection(
@Composable
private fun PatchFiles(state: PatchManagerUiState, viewModel: PatchManagerViewModel, modifier: Modifier) {
// The whole installed list folds away. A pnach pulled from the downloader can carry hundreds of
// codes, and with the list open there was no way past it to the rest of the screen.
var listOpen by androidx.compose.runtime.saveable.rememberSaveable { mutableStateOf(true) }
Column(modifier) {
SectionTitle(str("patches.installedHeader"), state.files.size.toString())
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { listOpen = !listOpen },
verticalAlignment = Alignment.CenterVertically,
) {
SectionTitle(
str("patches.installedHeader"),
state.files.size.toString(),
Modifier.weight(1f),
)
if (state.files.isNotEmpty()) {
Text(
if (listOpen) "" else "",
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(horizontal = 8.dp),
)
}
}
if (state.files.isEmpty()) {
PatchFilesEmptyState()
} else {
} else if (listOpen) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
state.files.forEach { file ->
val expanded = state.localExpandedPath == file.absolutePath
@@ -501,6 +501,15 @@ fun AppTab() {
onChange = LibraryChromePreferences::setShowRecents,
)
// Moved off the library overflow menu, where it was the odd one out: every other
// library-appearance preference already lives here beside cover size and opacity.
ToggleRow(
label = str("games.overflow.gridNames"),
value = com.armsx2.GridLabels.show.value,
description = str("app.library.gridNames.desc"),
onChange = { com.armsx2.GridLabels.set(it) },
)
IntSliderRow(
label = str("app.library.coverSize"),
value = (com.armsx2.ui.UiScale.coverScale.value * 100f).toInt().coerceIn(75, 250),
@@ -11,7 +11,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.armsx2.config.Settings
@@ -449,9 +452,84 @@ fun FixesTab(state: MutableState<Settings>) {
onChange = { apply(s.copy(swThreadsHeight = it)) },
)
}
SettingsDivider()
// GameDB fixes moved here from Performance: they are per-game compatibility switches the
// GameDB already applies automatically, so they belong with the other advanced knobs
// rather than in the tab people open to change speed settings.
CollapsibleSection(str("perf.gamedbFixes.title")) {
HelpText(str("perf.gamedbFixes.help"))
ToggleRow(str("perf.fix.skipBios"), s.enableFastBoot, description = str("perf.fix.skipBios.desc")) { apply(s.copy(enableFastBoot = it)) }
ToggleRow(str("perf.fix.gamedbFixes"), s.enableGameFixes, description = str("perf.fix.gamedbFixes.desc")) { apply(s.copy(enableGameFixes = it)) }
ToggleRow(str("perf.fix.skipMpeg"), s.gamefixSkipMpeg, description = str("perf.fix.skipMpeg.desc")) { apply(s.copy(enableGameFixes = true, gamefixSkipMpeg = it)) }
if (s.gamefixSkipMpeg) HelpText(str("perf.fix.skipMpeg.warning"))
ToggleRow(str("perf.fix.fmvSoftware"), s.gamefixSoftwareRendererFmv, description = str("perf.fix.fmvSoftware.desc")) { apply(s.copy(enableGameFixes = true, gamefixSoftwareRendererFmv = it)) }
ToggleRow(str("perf.fix.eeTiming"), s.gamefixEETiming, description = str("perf.fix.eeTiming.desc")) { apply(s.copy(enableGameFixes = true, gamefixEETiming = it)) }
ToggleRow(str("perf.fix.instantDma"), s.gamefixInstantDma, description = str("perf.fix.instantDma.desc")) { apply(s.copy(enableGameFixes = true, gamefixInstantDma = it)) }
ToggleRow(str("perf.fix.blitFps"), s.gamefixBlitInternalFps, description = str("perf.fix.blitFps.desc")) { apply(s.copy(enableGameFixes = true, gamefixBlitInternalFps = it)) }
ToggleRow(str("perf.fix.fpuMultiply"), s.gamefixFpuMul, description = str("perf.fix.fpuMultiply.desc")) { apply(s.copy(enableGameFixes = true, gamefixFpuMul = it)) }
ToggleRow(str("perf.fix.ophFlag"), s.gamefixOphFlag, description = str("perf.fix.ophFlag.desc")) { apply(s.copy(enableGameFixes = true, gamefixOphFlag = it)) }
ToggleRow(str("perf.fix.gifFifo"), s.gamefixGifFifo, description = str("perf.fix.gifFifo.desc")) { apply(s.copy(enableGameFixes = true, gamefixGifFifo = it)) }
ToggleRow(str("perf.fix.dmaBusy"), s.gamefixDmaBusy, description = str("perf.fix.dmaBusy.desc")) { apply(s.copy(enableGameFixes = true, gamefixDmaBusy = it)) }
ToggleRow(str("perf.fix.vif1Stall"), s.gamefixVif1Stall, description = str("perf.fix.vif1Stall.desc")) { apply(s.copy(enableGameFixes = true, gamefixVif1Stall = it)) }
ToggleRow(str("perf.fix.iBit"), s.gamefixIbit, description = str("perf.fix.iBit.desc")) { apply(s.copy(enableGameFixes = true, gamefixIbit = it)) }
ToggleRow(str("perf.fix.fullVu0Sync"), s.gamefixFullVu0Sync, description = str("perf.fix.fullVu0Sync.desc")) { apply(s.copy(enableGameFixes = true, gamefixFullVu0Sync = it)) }
ToggleRow(str("perf.fix.vuAddSub"), s.gamefixVuAddSub, description = str("perf.fix.vuAddSub.desc")) { apply(s.copy(enableGameFixes = true, gamefixVuAddSub = it)) }
ToggleRow(str("perf.fix.vuOverflow"), s.gamefixVuOverflow, description = str("perf.fix.vuOverflow.desc")) { apply(s.copy(enableGameFixes = true, gamefixVuOverflow = it)) }
ToggleRow(str("perf.fix.extraXgkick"), s.gamefixXgkick, description = str("perf.fix.extraXgkick.desc")) { apply(s.copy(enableGameFixes = true, gamefixXgkick = it)) }
ToggleRow(str("perf.fix.goemonTlb"), s.gamefixGoemonTlb, description = str("perf.fix.goemonTlb.desc")) { apply(s.copy(enableGameFixes = true, gamefixGoemonTlb = it)) }
ToggleRow(str("perf.fix.vuSync"), s.gamefixVuSync, description = str("perf.fix.vuSync.desc")) { apply(s.copy(enableGameFixes = true, gamefixVuSync = it)) }
}
SettingsDivider()
RecompilerSection(state)
Spacer(Modifier.height(8.dp))
}
}
// CollapsibleSection now lives in SettingsWidgets.kt (shared by the Fixes / Pad /
// Performance / Renderer tabs).
/** The former standalone Recompiler tab, folded in as a section. Turning a recompiler off drops
* that processor to an interpreter correct but far slower so it is a debugging control, not
* something to browse past on the way to a speed setting. */
@Composable
private fun RecompilerSection(state: MutableState<Settings>) {
val settings = state.value
fun apply(updated: Settings) = InGameOverlay.saveSettings(updated)
CollapsibleSection(str("tab.recompiler")) {
Text(
str("jit.recompiler.warning"),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp),
)
ToggleRow("EE (R5900)", settings.recEE) { apply(settings.copy(recEE = it)) }
ToggleRow("IOP (R3000)", settings.recIOP) { apply(settings.copy(recIOP = it)) }
ToggleRow("VU0", settings.recVU0) { apply(settings.copy(recVU0 = it)) }
ToggleRow("VU1", settings.recVU1) { apply(settings.copy(recVU1 = it)) }
ToggleRow("Fastmem", settings.enableFastmem) { apply(settings.copy(enableFastmem = it)) }
Spacer(Modifier.height(14.dp))
Text(
str("jit.diagnostics.header"),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.titleSmall,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
)
// Seed from the REAL native flag: a plain `remember { false }` meant leaving the screen
// destroyed the state and the switch came back OFF while the native flag was still on.
var eeDiff by remember {
mutableStateOf(
runCatching { kr.co.iefriends.pcsx2.NativeApp.isEeDiffVerify() }.getOrDefault(false)
)
}
ToggleRow(
label = str("jit.eeDiffVerify.label"),
value = eeDiff,
description = str("jit.eeDiffVerify.description"),
) {
eeDiff = it
runCatching { kr.co.iefriends.pcsx2.NativeApp.setEeDiffVerify(it) }
}
}
}
@@ -212,7 +212,7 @@ fun PerformanceTab(state: MutableState<Settings>) {
onChange = { apply(s.copy(affinityMode = it)) },
)
SettingsDivider()
CollapsibleSection(str("perf.speedhacks.title"), initiallyExpanded = false) {
CollapsibleSection(str("perf.speedhacks.title"), initiallyExpanded = true) {
IntSliderRow(
label = str("perf.eeCycleRate.label"),
value = s.eeCycleRate,
@@ -348,30 +348,6 @@ fun PerformanceTab(state: MutableState<Settings>) {
)
}
SettingsDivider()
CollapsibleSection(str("perf.gamedbFixes.title")) {
HelpText(str("perf.gamedbFixes.help"))
ToggleRow(str("perf.fix.skipBios"), s.enableFastBoot, description = str("perf.fix.skipBios.desc")) { apply(s.copy(enableFastBoot = it)) }
ToggleRow(str("perf.fix.gamedbFixes"), s.enableGameFixes, description = str("perf.fix.gamedbFixes.desc")) { apply(s.copy(enableGameFixes = it)) }
ToggleRow(str("perf.fix.skipMpeg"), s.gamefixSkipMpeg, description = str("perf.fix.skipMpeg.desc")) { apply(s.copy(enableGameFixes = true, gamefixSkipMpeg = it)) }
if (s.gamefixSkipMpeg) HelpText(str("perf.fix.skipMpeg.warning"))
ToggleRow(str("perf.fix.fmvSoftware"), s.gamefixSoftwareRendererFmv, description = str("perf.fix.fmvSoftware.desc")) { apply(s.copy(enableGameFixes = true, gamefixSoftwareRendererFmv = it)) }
ToggleRow(str("perf.fix.eeTiming"), s.gamefixEETiming, description = str("perf.fix.eeTiming.desc")) { apply(s.copy(enableGameFixes = true, gamefixEETiming = it)) }
ToggleRow(str("perf.fix.instantDma"), s.gamefixInstantDma, description = str("perf.fix.instantDma.desc")) { apply(s.copy(enableGameFixes = true, gamefixInstantDma = it)) }
ToggleRow(str("perf.fix.blitFps"), s.gamefixBlitInternalFps, description = str("perf.fix.blitFps.desc")) { apply(s.copy(enableGameFixes = true, gamefixBlitInternalFps = it)) }
ToggleRow(str("perf.fix.fpuMultiply"), s.gamefixFpuMul, description = str("perf.fix.fpuMultiply.desc")) { apply(s.copy(enableGameFixes = true, gamefixFpuMul = it)) }
ToggleRow(str("perf.fix.ophFlag"), s.gamefixOphFlag, description = str("perf.fix.ophFlag.desc")) { apply(s.copy(enableGameFixes = true, gamefixOphFlag = it)) }
ToggleRow(str("perf.fix.gifFifo"), s.gamefixGifFifo, description = str("perf.fix.gifFifo.desc")) { apply(s.copy(enableGameFixes = true, gamefixGifFifo = it)) }
ToggleRow(str("perf.fix.dmaBusy"), s.gamefixDmaBusy, description = str("perf.fix.dmaBusy.desc")) { apply(s.copy(enableGameFixes = true, gamefixDmaBusy = it)) }
ToggleRow(str("perf.fix.vif1Stall"), s.gamefixVif1Stall, description = str("perf.fix.vif1Stall.desc")) { apply(s.copy(enableGameFixes = true, gamefixVif1Stall = it)) }
ToggleRow(str("perf.fix.iBit"), s.gamefixIbit, description = str("perf.fix.iBit.desc")) { apply(s.copy(enableGameFixes = true, gamefixIbit = it)) }
ToggleRow(str("perf.fix.fullVu0Sync"), s.gamefixFullVu0Sync, description = str("perf.fix.fullVu0Sync.desc")) { apply(s.copy(enableGameFixes = true, gamefixFullVu0Sync = it)) }
ToggleRow(str("perf.fix.vuAddSub"), s.gamefixVuAddSub, description = str("perf.fix.vuAddSub.desc")) { apply(s.copy(enableGameFixes = true, gamefixVuAddSub = it)) }
ToggleRow(str("perf.fix.vuOverflow"), s.gamefixVuOverflow, description = str("perf.fix.vuOverflow.desc")) { apply(s.copy(enableGameFixes = true, gamefixVuOverflow = it)) }
ToggleRow(str("perf.fix.extraXgkick"), s.gamefixXgkick, description = str("perf.fix.extraXgkick.desc")) { apply(s.copy(enableGameFixes = true, gamefixXgkick = it)) }
ToggleRow(str("perf.fix.goemonTlb"), s.gamefixGoemonTlb, description = str("perf.fix.goemonTlb.desc")) { apply(s.copy(enableGameFixes = true, gamefixGoemonTlb = it)) }
ToggleRow(str("perf.fix.vuSync"), s.gamefixVuSync, description = str("perf.fix.vuSync.desc")) { apply(s.copy(enableGameFixes = true, gamefixVuSync = it)) }
}
SettingsDivider()
CollapsibleSection(str("perf.advancedSpeedhacks.title")) {
Spacer(Modifier.height(8.dp))
ToggleRow(str("perf.hack.mtvu"), s.mtvu, description = str("perf.hack.mtvu.desc")) { apply(s.copy(mtvu = it)) }
@@ -1,65 +0,0 @@
package com.armsx2.ui.settings
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.armsx2.config.Settings
import com.armsx2.i18n.str
import com.armsx2.ui.InGameOverlay
import kr.co.iefriends.pcsx2.NativeApp
@Composable
fun RecompilerTab(state: MutableState<Settings>) {
val settings = state.value
ControllerAutoScroll(settingsScrollState())
fun apply(updated: Settings) = InGameOverlay.saveSettings(updated)
Column(Modifier.fillMaxWidth()) {
Text(
str("jit.recompiler.warning"),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(bottom = 8.dp),
)
ToggleRow("EE (R5900)", settings.recEE) { apply(settings.copy(recEE = it)) }
ToggleRow("IOP (R3000)", settings.recIOP) { apply(settings.copy(recIOP = it)) }
ToggleRow("VU0", settings.recVU0) { apply(settings.copy(recVU0 = it)) }
ToggleRow("VU1", settings.recVU1) { apply(settings.copy(recVU1 = it)) }
ToggleRow("Fastmem", settings.enableFastmem) { apply(settings.copy(enableFastmem = it)) }
Spacer(Modifier.height(14.dp))
Text(
str("jit.diagnostics.header"),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.titleSmall,
modifier = Modifier.padding(horizontal = 4.dp, vertical = 4.dp),
)
// Seed from the REAL native flag. This was a plain `remember { false }`, so leaving the
// screen destroyed the state and the switch came back showing OFF while the native flag
// was still on — the toggle was reporting fiction.
var eeDiff by remember {
mutableStateOf(runCatching { NativeApp.isEeDiffVerify() }.getOrDefault(false))
}
ToggleRow(
label = str("jit.eeDiffVerify.label"),
value = eeDiff,
description = str("jit.eeDiffVerify.description"),
) {
eeDiff = it
runCatching { NativeApp.setEeDiffVerify(it) }
}
}
}
@@ -102,7 +102,7 @@ fun RendererTab(state: MutableState<Settings>) {
modifier = Modifier
.fillMaxWidth(),
) {
CollapsibleSection(str("renderer.section.displayResolution"), initiallyExpanded = false) {
CollapsibleSection(str("renderer.section.displayResolution"), initiallyExpanded = true) {
// Graphics API (OpenGL / Vulkan) + Vulkan custom-driver picker.
// from the removed first-run setup renderer page into settings.
RendererBackendSection(state)
@@ -515,18 +515,37 @@ fun SettingsDivider() {
/** Collapsible settings section: a tappable header ( collapsed / expanded) that
* shows or hides its content. Controller-focusable so a gamepad can open it. Shared
* by the Fixes / Pad / Performance / Renderer tabs to de-bloat long settings lists.
* [initiallyExpanded] lets a tab open its most-used section by default. */
* [initiallyExpanded] lets a tab open its most-used section by default.
*
* This is the de-bloating that the settings screens were built around, and it had stopped
* happening: the header rendered as plain text and `content()` was called unconditionally, with
* `initiallyExpanded` marked UNUSED_PARAMETER. Every tab was therefore one flat list of every
* option it owns the reason the settings became unusable in landscape. Restoring it shortens
* Pad / Renderer / Performance / Advanced all at once.
*
* State is [rememberSaveable] so a rotation does not re-collapse what the user just opened
* landscape being exactly where the long lists hurt most. */
@Composable
fun CollapsibleSection(
title: String,
@Suppress("UNUSED_PARAMETER")
initiallyExpanded: Boolean = false,
content: @Composable () -> Unit,
) {
var expanded by androidx.compose.runtime.saveable.rememberSaveable(title) {
mutableStateOf(initiallyExpanded)
}
val toggle = {
expanded = !expanded
com.armsx2.MenuSfx.play(
if (expanded) com.armsx2.MenuSfx.Event.TOGGLE_ON else com.armsx2.MenuSfx.Event.TOGGLE_OFF
)
}
Spacer(Modifier.height(12.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.controllerFocusable("section.$title", onConfirm = toggle)
.clickable(onClick = toggle)
.padding(horizontal = 8.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically,
) {
@@ -537,8 +556,16 @@ fun CollapsibleSection(
fontWeight = FontWeight.Bold,
modifier = Modifier.weight(1f),
)
Text(
if (expanded) "" else "",
color = MaterialTheme.colorScheme.primary,
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
)
}
content()
// Collapsed content is not composed at all, so its rows also drop out of the controller-focus
// registry — a pad cannot land on a setting the user cannot see.
if (expanded) content()
}
@Composable
@@ -25,8 +25,8 @@ import org.json.JSONObject
internal val SETTINGS_CATEGORY_FIELDS: Map<SettingsCategory, List<String>> = mapOf(
// PerformanceTab.kt
SettingsCategory.Performance to listOf(
"eeClampMode", "eeCycleRate", "eeCycleSkip", "eeFpuRoundMode", "enableFastBoot",
"enableGameFixes", "fastCDVD", "fpsLimit", "frameSkip", "framerateNtsc", "frameratePal",
"eeClampMode", "eeCycleRate", "eeCycleSkip", "eeFpuRoundMode",
"fastCDVD", "fpsLimit", "frameSkip", "framerateNtsc", "frameratePal",
"intcStat", "mtvu", "nominalSpeedPercent", "skipDuplicateFrames", "vu0RoundMode",
"vu1Instant", "vu1RoundMode", "vuClampMode", "vuDeferredWrites", "vuFlagHack",
"vuNeonFusions", "vuSkipStallSim", "waitLoop",
@@ -65,8 +65,16 @@ internal val SETTINGS_CATEGORY_FIELDS: Map<SettingsCategory, List<String>> = map
"osdShowMessages", "osdShowResolution", "osdShowSettings", "osdShowSpeed",
"osdShowVersion", "osdShowVps",
),
// FixesTab.kt
// FixesTab.kt — also owns the GameDB fixes and the recompiler toggles, which moved here
// from Performance and from the retired Recompiler tab.
SettingsCategory.Advanced to listOf(
"enableFastBoot", "enableGameFixes",
"gamefixBlitInternalFps", "gamefixDmaBusy", "gamefixEETiming", "gamefixFpuMul",
"gamefixFullVu0Sync", "gamefixGifFifo", "gamefixGoemonTlb", "gamefixIbit",
"gamefixInstantDma", "gamefixOphFlag", "gamefixSkipMpeg",
"gamefixSoftwareRendererFmv", "gamefixVif1Stall", "gamefixVuAddSub",
"gamefixVuOverflow", "gamefixVuSync", "gamefixXgkick",
"enableFastmem", "recEE", "recIOP", "recVU0", "recVU1",
"alignSprite", "antiBlur", "autoFlush", "autoFlushSw", "bilinearUpscale", "cpuClutRender",
"cpuFramebufferConversion", "cpuSpriteRenderBw", "cpuSpriteRenderLevel", "cropBottom",
"cropLeft", "cropRight", "cropTop", "displayZoom", "disableDepthEmulation", "disableFramebufferFetch",
@@ -81,10 +89,6 @@ internal val SETTINGS_CATEGORY_FIELDS: Map<SettingsCategory, List<String>> = map
"syncToHostRefresh", "textureInsideRt", "textureOffsetX", "textureOffsetY",
"unscaledPaletteDraw", "useBlitSwapChain", "vsyncQueueSize",
),
// RecompilerTab.kt
SettingsCategory.Recompiler to listOf(
"enableFastmem", "recEE", "recIOP", "recVU0", "recVU1",
),
// Controls / Hotkeys / Skins / General / Info / Patches / About own no Settings fields —
// Controls keeps its binds and tunables in ControllerMappings and has its own reset row.
)
@@ -67,7 +67,6 @@ import com.armsx2.ui.settings.NetworkTab
import com.armsx2.ui.settings.OverlayTab
import com.armsx2.ui.settings.PadTab
import com.armsx2.ui.settings.PerformanceTab
import com.armsx2.ui.settings.RecompilerTab
import com.armsx2.ui.settings.RendererTab
import com.armsx2.ui.settings.SegmentedRow
import com.armsx2.ui.settings.SkinsTab
@@ -338,7 +337,7 @@ private fun SettingsCategoryBar(
) {
// Row + horizontalScroll (NOT LazyRow): controllerFocusable registers each tab via a
// SideEffect that only runs for COMPOSED children. A LazyRow leaves every off-screen
// tab (Skins / Fixes / Recompiler, past On-Screen) unregistered and unreachable, so
// tab (Skins / Advanced, past On-Screen) unregistered and unreachable, so
// the controller got stuck at the last visible tab. A plain Row composes them all;
// each selected chip's bringIntoView then scrolls it into view as the selector moves.
val sections = settingsSections().filterNot {
@@ -417,9 +416,7 @@ private fun settingsSections() = listOf(
SettingsSection(SettingsCategory.OnScreen, "tab.overlay", ""),
SettingsSection(SettingsCategory.Skins, "tab.skins", ""),
SettingsSection(SettingsCategory.Advanced, "tab.fixes", ""),
SettingsSection(SettingsCategory.Recompiler, "tab.recompiler", ""),
SettingsSection(SettingsCategory.Patches, "tab.patches", ""),
SettingsSection(SettingsCategory.About, "about.title", ""),
)
@Composable
@@ -436,7 +433,6 @@ private fun CategoryContent(category: SettingsCategory, viewModel: SettingsViewM
SettingsCategory.OnScreen -> OverlayTab(viewModel.settings)
SettingsCategory.Skins -> SkinsTab(viewModel.settings)
SettingsCategory.Advanced -> FixesTab(viewModel.settings)
SettingsCategory.Recompiler -> RecompilerTab(viewModel.settings)
SettingsCategory.Patches -> com.armsx2.ui.patches.PatchesSettingsTab(viewModel.uiState.value.game)
SettingsCategory.About -> Unit
}
@@ -455,7 +451,6 @@ internal fun categoryTitle(category: SettingsCategory): String = when (category)
SettingsCategory.OnScreen -> str("tab.overlay")
SettingsCategory.Skins -> str("tab.skins")
SettingsCategory.Advanced -> str("tab.fixes")
SettingsCategory.Recompiler -> str("tab.recompiler")
SettingsCategory.Patches -> str("patches.dialog.patchesAndCheats")
SettingsCategory.About -> str("about.title")
}
@@ -17,6 +17,7 @@ internal val SETTINGS_SEARCH_INDEX: List<SettingsSearchEntry> = listOf(
SettingsSearchEntry("app.library.search", true, SettingsCategory.General),
SettingsSearchEntry("app.library.recents", true, SettingsCategory.General),
SettingsSearchEntry("app.library.coverSize", true, SettingsCategory.General),
SettingsSearchEntry("games.overflow.gridNames", true, SettingsCategory.General),
SettingsSearchEntry("app.backup.export", true, SettingsCategory.General),
SettingsSearchEntry("app.backup.import", true, SettingsCategory.General),
SettingsSearchEntry("app.blockHome", true, SettingsCategory.General),
@@ -26,25 +27,25 @@ internal val SETTINGS_SEARCH_INDEX: List<SettingsSearchEntry> = listOf(
SettingsSearchEntry("app.bgColor", true, SettingsCategory.General),
SettingsSearchEntry("app.menuSfx", true, SettingsCategory.General),
SettingsSearchEntry("update.includeNightly", true, SettingsCategory.General),
SettingsSearchEntry("perf.fix.skipBios", true, SettingsCategory.Performance),
SettingsSearchEntry("perf.fix.gamedbFixes", true, SettingsCategory.Performance),
SettingsSearchEntry("perf.fix.skipMpeg", true, SettingsCategory.Performance),
SettingsSearchEntry("perf.fix.fmvSoftware", true, SettingsCategory.Performance),
SettingsSearchEntry("perf.fix.eeTiming", true, SettingsCategory.Performance),
SettingsSearchEntry("perf.fix.instantDma", true, SettingsCategory.Performance),
SettingsSearchEntry("perf.fix.blitFps", true, SettingsCategory.Performance),
SettingsSearchEntry("perf.fix.fpuMultiply", true, SettingsCategory.Performance),
SettingsSearchEntry("perf.fix.ophFlag", true, SettingsCategory.Performance),
SettingsSearchEntry("perf.fix.gifFifo", true, SettingsCategory.Performance),
SettingsSearchEntry("perf.fix.dmaBusy", true, SettingsCategory.Performance),
SettingsSearchEntry("perf.fix.vif1Stall", true, SettingsCategory.Performance),
SettingsSearchEntry("perf.fix.iBit", true, SettingsCategory.Performance),
SettingsSearchEntry("perf.fix.fullVu0Sync", true, SettingsCategory.Performance),
SettingsSearchEntry("perf.fix.vuAddSub", true, SettingsCategory.Performance),
SettingsSearchEntry("perf.fix.vuOverflow", true, SettingsCategory.Performance),
SettingsSearchEntry("perf.fix.extraXgkick", true, SettingsCategory.Performance),
SettingsSearchEntry("perf.fix.goemonTlb", true, SettingsCategory.Performance),
SettingsSearchEntry("perf.fix.vuSync", true, SettingsCategory.Performance),
SettingsSearchEntry("perf.fix.skipBios", true, SettingsCategory.Advanced),
SettingsSearchEntry("perf.fix.gamedbFixes", true, SettingsCategory.Advanced),
SettingsSearchEntry("perf.fix.skipMpeg", true, SettingsCategory.Advanced),
SettingsSearchEntry("perf.fix.fmvSoftware", true, SettingsCategory.Advanced),
SettingsSearchEntry("perf.fix.eeTiming", true, SettingsCategory.Advanced),
SettingsSearchEntry("perf.fix.instantDma", true, SettingsCategory.Advanced),
SettingsSearchEntry("perf.fix.blitFps", true, SettingsCategory.Advanced),
SettingsSearchEntry("perf.fix.fpuMultiply", true, SettingsCategory.Advanced),
SettingsSearchEntry("perf.fix.ophFlag", true, SettingsCategory.Advanced),
SettingsSearchEntry("perf.fix.gifFifo", true, SettingsCategory.Advanced),
SettingsSearchEntry("perf.fix.dmaBusy", true, SettingsCategory.Advanced),
SettingsSearchEntry("perf.fix.vif1Stall", true, SettingsCategory.Advanced),
SettingsSearchEntry("perf.fix.iBit", true, SettingsCategory.Advanced),
SettingsSearchEntry("perf.fix.fullVu0Sync", true, SettingsCategory.Advanced),
SettingsSearchEntry("perf.fix.vuAddSub", true, SettingsCategory.Advanced),
SettingsSearchEntry("perf.fix.vuOverflow", true, SettingsCategory.Advanced),
SettingsSearchEntry("perf.fix.extraXgkick", true, SettingsCategory.Advanced),
SettingsSearchEntry("perf.fix.goemonTlb", true, SettingsCategory.Advanced),
SettingsSearchEntry("perf.fix.vuSync", true, SettingsCategory.Advanced),
SettingsSearchEntry("perf.hack.mtvu", true, SettingsCategory.Performance),
SettingsSearchEntry("perf.hack.instantVu1", true, SettingsCategory.Performance),
SettingsSearchEntry("perf.hack.vuFlagHack", true, SettingsCategory.Performance),
@@ -74,7 +75,7 @@ internal val SETTINGS_SEARCH_INDEX: List<SettingsSearchEntry> = listOf(
SettingsSearchEntry("perf.vu0RoundMode.label", true, SettingsCategory.Performance),
SettingsSearchEntry("perf.vu1RoundMode.label", true, SettingsCategory.Performance),
SettingsSearchEntry("perf.speedhacks.title", true, SettingsCategory.Performance),
SettingsSearchEntry("perf.gamedbFixes.title", true, SettingsCategory.Performance),
SettingsSearchEntry("perf.gamedbFixes.title", true, SettingsCategory.Advanced),
SettingsSearchEntry("perf.advancedSpeedhacks.title", true, SettingsCategory.Performance),
SettingsSearchEntry("VSync", false, SettingsCategory.Graphics),
SettingsSearchEntry("renderer.shadeboost.label", true, SettingsCategory.Graphics),
@@ -248,12 +249,12 @@ internal val SETTINGS_SEARCH_INDEX: List<SettingsSearchEntry> = listOf(
SettingsSearchEntry("fixes.section.upscaling", true, SettingsCategory.Advanced),
SettingsSearchEntry("fixes.section.hardware", true, SettingsCategory.Advanced),
SettingsSearchEntry("fixes.section.software", true, SettingsCategory.Advanced),
SettingsSearchEntry("EE (R5900)", false, SettingsCategory.Recompiler),
SettingsSearchEntry("IOP (R3000)", false, SettingsCategory.Recompiler),
SettingsSearchEntry("VU0", false, SettingsCategory.Recompiler),
SettingsSearchEntry("VU1", false, SettingsCategory.Recompiler),
SettingsSearchEntry("Fastmem", false, SettingsCategory.Recompiler),
SettingsSearchEntry("jit.eeDiffVerify.label", true, SettingsCategory.Recompiler),
SettingsSearchEntry("EE (R5900)", false, SettingsCategory.Advanced),
SettingsSearchEntry("IOP (R3000)", false, SettingsCategory.Advanced),
SettingsSearchEntry("VU0", false, SettingsCategory.Advanced),
SettingsSearchEntry("VU1", false, SettingsCategory.Advanced),
SettingsSearchEntry("Fastmem", false, SettingsCategory.Advanced),
SettingsSearchEntry("jit.eeDiffVerify.label", true, SettingsCategory.Advanced),
SettingsSearchEntry("patches.enablePatches.label", true, SettingsCategory.Patches),
SettingsSearchEntry("patches.cheats.label", true, SettingsCategory.Patches),
SettingsSearchEntry("patches.widescreen.label", true, SettingsCategory.Patches),
@@ -0,0 +1,25 @@
<!-- Tabler Icons (MIT) - power. Standard IEC power symbol: a broken ring with a vertical bar.
Replaces the "⏻" text glyph, which most Android system fonts do not carry and which therefore
rendered as a tofu box in the drawer. Stroke-based like ic_discord/ic_github; tinted red at the
call site since Exit is the one destructive row in the drawer. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFFFF">
<path
android:strokeColor="#FFFFFFFF"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:fillColor="#00000000"
android:pathData="M7 6a7.75 7.75 0 1 0 10 0" />
<path
android:strokeColor="#FFFFFFFF"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:fillColor="#00000000"
android:pathData="M12 4l0 8" />
</vector>