X Tutup
Skip to content

📱 Virtual Gamepad Editor#1152

Open
zees-dev wants to merge 15 commits intoEmulatorJS:mainfrom
zees-dev:feat/edit-virtual-gamepad
Open

📱 Virtual Gamepad Editor#1152
zees-dev wants to merge 15 commits intoEmulatorJS:mainfrom
zees-dev:feat/edit-virtual-gamepad

Conversation

@zees-dev
Copy link
Contributor

@zees-dev zees-dev commented Jan 3, 2026

Description

Implements a visual editor for the virtual gamepad on mobile devices, allowing users to fully customize their control layout.

Closes #588

Features

  • 🎮 Move Controls: Drag any button, D-pad, or joystick to reposition it anywhere on screen
  • 📐 Resize Controls: Use resize handles to scale elements up or down
  • ↩️ Undo/Redo: Full history support for all edit operations
  • 🔄 Reset to Default: Restore the original control layout
  • 🧹 Clear Changes: Discard all changes made during the current edit session
  • 💾 Save & Exit: Persist custom layouts to storage

Technical Implementation

Overlay-Based Architecture

Rather than manipulating the actual gamepad elements directly (which proved to be computationally intensive), this implementation creates an overlay layer during edit mode:

  • Overlay elements mirror the original controls' positions and visual styles
  • Users interact with the lightweight overlay elements
  • Changes are applied back to the original controls only on save
  • This approach provides smooth, responsive editing even on lower-end mobile devices

New Files

  • data/src/virtualGamepadEditor.js - Self-contained editor module with:
    • VirtualGamepadEditor - Main orchestrator class
    • OverlayElement - Per-element state and interaction handling
    • HistoryManager - Command pattern implementation for undo/redo

Modified Files

  • data/src/emulator.js - Integration hooks, layout saving/loading, zone event refactoring
  • data/emulator.css - Edit mode styles, toolbar, resize handles
  • data/localization/en.json - New localization strings
  • data/loader.js - Load the new editor module

Key Design Decisions

  1. Non-intrusive: Edit mode can be entered/exited cleanly with no side effects
  2. Pause on edit: Emulator pauses during editing to prevent accidental input
  3. Per-game layouts: Layouts are saved as part of core-specific settings
  4. Nipplejs recreation: Joystick zones are properly destroyed and recreated with new positions/sizes

Screenshot

Screenshot 2026-01-03 at 6 06 24 PM Screenshot 2026-01-03 at 6 06 52 PM Screenshot 2026-01-03 at 6 10 24 PM

Usage

  1. Enter fullscreen mode on a touch device
  2. Open Settings menu
  3. Navigate to "Virtual Gamepad" section
  4. Tap "Edit Virtual Gamepad"
  5. Drag controls to reposition, use corner handles to resize
  6. Tap the save button (💾) to apply changes

Future Improvements

  • Separate layouts for portrait and landscape orientations
  • Button visibility toggle (show/hide individual controls)
  • Opacity adjustment per control
  • Loading/saving layouts from presets/templates?

Testing

Tested on:

  • Android Chrome
  • Desktop (mobile mode)

Breaking Changes

None. Existing virtual gamepad functionality remains unchanged. Custom layouts are opt-in.

@michael-j-green
Copy link
Contributor

Hey, @zees-dev thanks for the submission!

Quick note: this edits some sections of emulator.js that are being updated by #1106.

I'm not sure which order that @ethanaobrien wants to apply these changes in, but one of us will have to refactor things. Given my change in #1106 migrates a lot of code to external classes, I think it might be this one. Hopefully when the time comes the refactoring won't be too onerous.

On to the review:

How much of this code did you write vs AI? The PR itself feels very AI-ish (lots of use of emoji) - and while AI assisted code is allowed, "vibe coding" is not.

What is the purpose of the findElementId function? Best I can tell, this could have just been a .includes on the element's classList property. You appear to be looking for a class that begins with "b_" but then specify it with the fallback parameter anyway.

Why couldn't you use document.getElementsByClassName() or document.querySelectorAll() to select the elements you want to work with by class?

@ethanaobrien: In data/src/virtualGamepadEditor.js some icons have been defined in code. I thought about this when I was redo-ing some of the toolbar code - I think moving all icons to either standalone files or as consts in a separate JS file is the best way to go. It makes code more readable to not have to scroll past tons of SVG definitions.

Moving the images to their own files does make it a bit easier for devs to customise the emulator with their own icons a bit easier - though it also means we're managing extra files in the package.

@zees-dev
Copy link
Contributor Author

zees-dev commented Jan 4, 2026

Thanks for the quick look/review!

AI usage:
The provided code is mostly (I would say 95%) written by AI.
I don't see much emoji usage in code - other than PR description (intentional).
Initially there was a lot of code in the emulatorjs file itself so I migrated most of this to a standalone file.

Review:
I have reviewed most of code - but honestly not too deeply.
I have run multiple independent instances of AI to review (git diff against main) - based on this there don't seem to be any major/critical issues.
I could do more here (and will do more here)

I havent validated the fallbacks use - In fact i don't think it's needed and will update the code accordingly (thanks for your quick review).

Detailed response - How it works

At a high level this is a virtual gamepad layout persistence system; it lets users customize touch control positions and saves those customizations across sessions.

  1. Each element (button/dpad/joystick) gets a unique b_-prefixed class as an ID (e.g., b_A, b_dpad_left)
  2. When users edit positions, layouts are saved to virtualGamepadLayout keyed by these IDs
  3. On load, applyVirtualGamepadLayout() queries elements by type, extracts their b_ ID, and applies saved positions

Re: findElementId and querySelectorAll
The layout is stored as { "b_A": {...}, "b_Start": {...} } and persisted to localStorage. To apply saved positions, we need the string key (e.g., "b_A") to do the lookup.
querySelectorAll returns DOM elements, not strings. Even if we selected by [class*="b_"], we'd still need to extract the actual class name to use as the dictionary key. That's what findElementId does.

Testing:
I have extensively tested it using desktop browser (mobile views in chrome dev tools) - and also on an actual mobile device by serving it on LAN.

High-level approach:
As for the approach - I have validated more than 3-4 times that we cannot use the original components since that has major performance issues (moving/dragging, etc.) - and its simply easier to add overlay/new components in edit mode - from which the changes can be used to update the original gamepad components.
We are using stack(s) for the state changes in edit mode - this allows for the undo, redo, clear, etc.

Why this implementation is good?
As for implementations; i haven't seen any in the wild which allow for such freedom (dragging, resizing, undo, redo, persistence - including the joystick).
^ But all this understandably does come at a cost (complexity) - which i've tried to maximally abstract to the new file.

Potential next steps:
Get some quick reviews; from AI is fine too.
I'm open to any feedback - the intention is to align with current codebase and architecture.
So if you do have plans to merge in other changes first - do let me know the order - and i will try rebase over the last planned PR in your list - and refactor the code accordingly.

@zees-dev
Copy link
Contributor Author

@michael-j-green - any updates

@michael-j-green
Copy link
Contributor

@zees-dev: I've requested a review from the repo maintainer.

data[i].elem.removeEventListener(data[i].listener, data[i].cb);
}
}
findElementId(classList) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this seem redundant given there is already a built in Javascript function to do this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replied in previous comment:

Re: findElementId and querySelectorAll
The layout is stored as { "b_A": {...}, "b_Start": {...} } and persisted to localStorage. To apply saved positions, we need the string key (e.g., "b_A") to do the lookup.
querySelectorAll returns DOM elements, not strings. Even if we selected by [class*="b_"], we'd still need to extract the actual class name to use as the dictionary key. That's what findElementId does.

@zees-dev zees-dev marked this pull request as draft January 27, 2026 20:13
@zees-dev
Copy link
Contributor Author

I have been testing this for a bit now; there are some minor issues which need ot be resolved.
For example - the DPAD for SNES is not edittable.

Also I think we should support orientation based configuration.
^ I.e. portrait mode config and persistance should be seperate to landscape - the respective config should load upon reloads/restarts.

zees-dev added 11 commits March 1, 2026 12:51
- added jsdocs for class
- added jsdocs for all functions in EJS_VirtualGamepadEditor class
- added jsdocs for class
- added jsdocs for all functions in EJS_OverlayElement class
- added jsdocs for class
- added jsdocs for all functions in EJS_HistoryManager class
- fixed issue with ghost selection area on top of d-pad and joystick
- improved preview
- removed white button outlines in editmode; only has dashed outline
- virtual gamepad edit mode visual improvements
@zees-dev zees-dev force-pushed the feat/edit-virtual-gamepad branch from d84c734 to 447b247 Compare March 1, 2026 21:08
@zees-dev zees-dev marked this pull request as ready for review March 1, 2026 21:11
@zees-dev
Copy link
Contributor Author

zees-dev commented Mar 1, 2026

I have now validated that the D-pad seems to be edittable aswell.
Additionally joystick and d-pad ui is preserved in virtual gamepad edit mode.

As for orientation-specific edit controls; im hoping that can be addressed in a future PR (if required).

…ecture

- replace command-style undo/redo with snapshot-based history (past/present/future)
- initialize editor state from an initial snapshot and commit changes uniformly after drag/resize/reset/clear
- register virtual gamepad controls once in emulator and reuse that registry in editor overlay setup and layout apply
- remove generated fallback control ids and persist/edit only explicitly identified controls
- streamline pointer interaction handling with Pointer Events first and legacy touch/mouse fallback
- normalize virtual gamepad editor toolbar localization keys to plain action labels
@michael-j-green
Copy link
Contributor

FYI: not sure if you've done it or not, but there's been a few pushes to main in the last few days, so you may need to check that you don't have any merge conflicts.

@zees-dev
Copy link
Contributor Author

zees-dev commented Mar 3, 2026

FYI: not sure if you've done it or not, but there's been a few pushes to main in the last few days, so you may need to check that you don't have any merge conflicts.

should be rebased on current main

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Virtual gamepad editor

2 participants

X Tutup