diff --git a/README.md b/README.md
index ec59e86..9079e8f 100644
--- a/README.md
+++ b/README.md
@@ -76,7 +76,11 @@ The launcher supports several compatibility options for Linux:
- **discord-rpc**: Discord Rich Presence integration
- **extract-zip**: ZIP archive extraction
- **Tailwind CSS**: UI styling (via CDN)
-- **UI Sounds**: Using the free version of [JDSherbert's Ultimate UI SFX Pack on itch.io](https://jdsherbert.itch.io/ultimate-ui-sfx-pack)
+
+## Assets
+
+- Controller button sprites: [greatdocbrown](https://greatdocbrown.itch.io/gamepad-ui)
+- UI Sounds: Using the free version of [JDSherbert's Ultimate UI SFX Pack on itch.io](https://jdsherbert.itch.io/ultimate-ui-sfx-pack)
## Development
diff --git a/index.html b/index.html
index d20df4b..5740caf 100644
--- a/index.html
+++ b/index.html
@@ -274,6 +274,16 @@
+
+
+
+
diff --git a/renderer.js b/renderer.js
index f33b165..c52a9c3 100644
--- a/renderer.js
+++ b/renderer.js
@@ -617,6 +617,52 @@ function applyRepoPreset() {
repoInput.value = presetSelect.value;
}
+
+function applyControllerLayoutPresetState(layoutMode = 'auto') {
+ const presets = document.querySelectorAll('.controller-layout-preset');
+ const activeLayout = layoutMode === 'nintendo' ? 'nintendo' : 'xbox';
+ presets.forEach((preset) => {
+ const isActive = preset.dataset.layout === activeLayout;
+ preset.classList.toggle('active', isActive);
+ preset.setAttribute('aria-pressed', isActive ? 'true' : 'false');
+ });
+}
+
+function initControllerLayoutPresets() {
+ const layoutSelect = document.getElementById('controller-layout-select');
+ if (!layoutSelect) return;
+
+ const presets = document.querySelectorAll('.controller-layout-preset');
+ presets.forEach((preset) => {
+ const pressOn = () => preset.classList.add('is-pressed');
+ const pressOff = () => preset.classList.remove('is-pressed');
+
+ preset.addEventListener('pointerdown', pressOn);
+ preset.addEventListener('pointerup', pressOff);
+ preset.addEventListener('pointerleave', pressOff);
+ preset.addEventListener('blur', pressOff);
+
+ preset.addEventListener('keydown', (event) => {
+ if (event.key === 'Enter' || event.key === ' ') pressOn();
+ });
+ preset.addEventListener('keyup', (event) => {
+ if (event.key === 'Enter' || event.key === ' ') pressOff();
+ });
+
+ preset.addEventListener('click', () => {
+ const selectedLayout = preset.dataset.layout || 'xbox';
+ layoutSelect.value = selectedLayout;
+ applyControllerLayoutPresetState(selectedLayout);
+ });
+ });
+
+ layoutSelect.addEventListener('change', () => {
+ applyControllerLayoutPresetState(layoutSelect.value || 'auto');
+ });
+
+ applyControllerLayoutPresetState(layoutSelect.value || 'auto');
+}
+
window.onload = async () => {
try {
await migrateLegacyConfig();
@@ -641,6 +687,7 @@ window.onload = async () => {
if (serverCheck) serverCheck.checked = currentInstance.isServer;
if (installInput) installInput.value = currentInstance.installPath;
if (controllerLayoutSelect) controllerLayoutSelect.value = await Store.get('legacy_controller_layout_mode', 'auto');
+ initControllerLayoutPresets();
syncRepoPresetFromInput();
if (process.platform === 'linux' || process.platform === 'darwin') {
@@ -1349,6 +1396,7 @@ async function toggleOptions(show) {
const savedLayoutMode = await Store.get('legacy_controller_layout_mode', 'auto');
layoutSelect.value = savedLayoutMode;
GamepadManager.setControlLayoutMode(savedLayoutMode);
+ applyControllerLayoutPresetState(savedLayoutMode);
}
syncRepoPresetFromInput();
document.activeElement?.blur(); modal.style.display = 'flex'; modal.style.opacity = '1';
@@ -1471,6 +1519,7 @@ async function saveOptions() {
await Store.set('legacy_steamdeck_mode', isSteamDeckMode);
await Store.set('legacy_controller_layout_mode', controllerLayoutMode);
GamepadManager.setControlLayoutMode(controllerLayoutMode);
+ applyControllerLayoutPresetState(controllerLayoutMode);
applyTheme(isClassic);
applySteamDeckMode(isSteamDeckMode);
await saveInstancesToStore(); toggleOptions(false); fetchGitHubData(); updatePlayButtonText(); showToast("Settings Saved");
@@ -1716,7 +1765,10 @@ async function loadControllerLayoutMode() {
const mode = await Store.get('legacy_controller_layout_mode', 'auto');
GamepadManager.setControlLayoutMode(mode);
const select = document.getElementById('controller-layout-select');
- if (select) select.value = mode;
+ if (select) {
+ select.value = mode;
+ applyControllerLayoutPresetState(mode);
+ }
}
function applySteamDeckMode(enabled) {
diff --git a/style.css b/style.css
index 9901299..8a14551 100644
--- a/style.css
+++ b/style.css
@@ -452,6 +452,18 @@ body.steamdeck-mode .nav-item:focus {
box-shadow: 0 0 0 2px rgba(255,255,255,0.95), 0 0 22px rgba(255,255,255,0.7), 0 0 34px rgba(90,170,255,0.36) !important;
}
+
+body.steamdeck-mode .controller-layout-preset {
+ min-height: 62px;
+ font-size: 22px;
+}
+
+body.steamdeck-mode .controller-layout-icon {
+ transform: scale(1.3);
+ transform-origin: left center;
+ margin-right: 4px;
+}
+
body.steamdeck-mode .sidebar {
width: 420px;
}
@@ -671,6 +683,92 @@ body.steamdeck-mode .sidebar {
border-color: #fff !important;
}
+
+.controller-layout-presets {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 10px;
+ margin-top: 12px;
+}
+
+.controller-layout-preset {
+ width: 100%;
+ min-height: 52px;
+ border: 2px solid #555;
+ background: #0a0a0a;
+ color: #fff;
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ gap: 12px;
+ padding: 8px 12px;
+ cursor: pointer;
+ font-size: 19px;
+ text-align: left;
+}
+
+.controller-layout-preset:hover,
+.controller-layout-preset:focus {
+ border-color: #fff;
+}
+
+.controller-layout-preset.active {
+ border-color: #55ff55;
+ box-shadow: inset 0 0 0 1px #55ff55;
+ background: #111;
+}
+
+.controller-layout-icon {
+ --sprite-x: -51px;
+ --sprite-y: -531px;
+ --sprite-w: 26px;
+ --sprite-h: 26px;
+ --sprite-pressed-x: -51px;
+ --sprite-pressed-y: -563px;
+ width: var(--sprite-w);
+ height: var(--sprite-h);
+ flex: 0 0 var(--sprite-w);
+ display: inline-block;
+ overflow: hidden;
+ image-rendering: pixelated;
+ background-repeat: no-repeat;
+ background-size: 560px 640px;
+ background-position: var(--sprite-x) var(--sprite-y);
+}
+
+.controller-layout-preset:hover .controller-layout-icon,
+.controller-layout-preset:focus .controller-layout-icon,
+.controller-layout-preset.active .controller-layout-icon,
+.controller-layout-preset.is-pressed .controller-layout-icon {
+ background-position: var(--sprite-pressed-x) var(--sprite-pressed-y);
+}
+
+.controller-layout-icon-xbox {
+ background-image: url('gdb-xbox-2.png');
+ /* action_button_189 (idle) + action_button_228 (pressed) from spritesheet JSON */
+ --sprite-x: -51px;
+ --sprite-y: -531px;
+ --sprite-w: 26px;
+ --sprite-h: 26px;
+ --sprite-pressed-x: -51px;
+ --sprite-pressed-y: -563px;
+}
+
+.controller-layout-icon-switch {
+ background-image: url('gdb-switch-2.png');
+ /* Switch sheet uses the same atlas layout coordinates */
+ --sprite-x: -51px;
+ --sprite-y: -531px;
+ --sprite-w: 26px;
+ --sprite-h: 26px;
+ --sprite-pressed-x: -51px;
+ --sprite-pressed-y: -563px;
+}
+
+.controller-layout-text {
+ line-height: 1.2;
+}
+
#server-checkbox.focused {
outline: 2px solid #fff;
}