Add a keyboard mapping tab to the options screen - #472
Merged
Conversation
Split the options screen into two tabs, Display and Keyboard, one layer each, with a shared tab strip that switches between them on Q/E or the gamepad shoulder buttons. The Keyboard tab lists the rebindable actions with their current key and captures a new one on confirm. Rebinding is requested from the layer as an atomic flag; the key capture, the binding write and the settings write all run on the main thread inside InputManager::update(). The capture frame resolves no actions, so the captured key cannot also fire the action it is being bound to. Overrides persist as input.<action> and reapply after the defaults are built; 0 means no override, so Reset to Defaults just writes zeros. Menu navigation is not rebindable: losing it strands the player on the screen. Escape cancels a capture, so an action cannot be bound back to Escape by hand. Layers that asked whether the options screen was open by testing OptionLayer::isActive() now ask Maze::isOptionsOpen(); with the screen split in two, the old test read as closed on the Keyboard tab and woke the intro, exit and maze layers behind it. Prompt sprites for Q, E, LB and RB come from Kenney Input Prompts (CC0), from the same folders as the sprites already in the tree. The sprite cache moves out of keyhints.cpp so the tab strip and the hint bar share it.
The active device switched back to KeyboardMouse on held key state. The Xbox 360 driver aliases the D-pad to arrow keys, so the ghost key was still down on the frame after a gamepad button released and flipped the device on every press. Claim the device on a fresh key or mouse button edge instead. The special cases for Escape and the arrow keys were only there to order against the gamepad check and are now covered by the generic loop.
The active tab now carries the same red fill the menu rows use for the selected row, with a small dot before the label, and the red underline is gone. No border and no rounded corners, so the strip stays lighter than a menu row. Widen the gaps between the prompt icons and the labels, and between the labels themselves, so the strip does not read as one run of text.
Cut a comment that restated the line under it, and shorten two others.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Splits the options screen into two tabs, Display and Keyboard, one layer each, with a shared tab strip at the top. Switch tabs with
Q/E, the gamepad shoulder buttons, or by clicking a tab.The active tab takes the red fill the menu rows use for a selected row (
{0.84, 0.04, 0.04, 0.14}) and a small white dot before its label. No border, no rounded corners, so the strip stays lighter than a menu row. The dot is the sharedQuadwith a half-width corner radius, so it costs no new primitive.The Keyboard tab lists the rebindable actions with their current key, plus Reset to Defaults and Return.
How rebinding works
The layer asks for a rebind by setting an atomic flag. The key capture, the binding write and the settings write all run on the main thread inside
InputManager::update(), becauseSettingsis not thread safe and menu layers run on the render thread. The capture frame resolves no actions, so the captured key cannot also fire the action it is being bound to.Overrides persist as
input.<action>and are re-applied at the end ofbuildDefaultBindings().0means "no override", so Reset to Defaults writes zeros and the defaults survive.Deliberate limits, marked in the source:
Fix that came with the split
ExitLayer,IntroLayerandMazeLayereach asked whether the options screen was open by testingOptionLayer::isActive(). With the screen split across two layers that test read as "closed" on the Keyboard tab, so those layers woke up behind it:ExitLayerswallowed the first Escape, and from the intro menu, Enter-to-rebind would have started a new game. All five call sites now go throughMaze::isOptionsOpen().Gamepad mode no longer bounces back
Using the gamepad in a menu switched the prompts straight back to keyboard glyphs.
InputManager::updateActiveDevice()claimed the device for KeyboardMouse on held key state. The Xbox 360 driver aliases the D-pad to arrow keys, so the ghost key was still down on the frame after the gamepad button released, and the device flipped on every press.Keyboard and mouse now claim the device on a fresh press edge or real cursor motion. A key or mouse button still takes the device from the gamepad the moment you press it. The special cases for Escape and the arrow keys only ordered themselves against the gamepad check, which the generic loop already covers, so they are gone. 19 lines out, 6 in.
Assets
keyboard_q,keyboard_e,xbox_lbandxbox_rbfrom Kenney Input Prompts (CC0), taken from the sameDefaultfolders as the sprites already in the tree. The prompt sprite cache moved out ofkeyhints.cppintogame::ui::promptSprite()so the tab strip and the hint bar share one cache.Testing
cmake --build out/build/ci-windows-release --target gameandpre-commit run --all-filesare both clean.I drove the app end to end with synthesised key events, then checked the rendered frame and the observable state: the settings file, the window rect, and whether the process exits.
Ereaches the Keyboard tab,Qreturns to Display.Kthen writesinput.moveForward = 75. Escape cancels and restores the previous key.0.K, quit, relaunched: in game,Ktoggled the window 1936x1119 → 1920x1080 → back, so the stored override was re-applied over the defaultF.Gamepad stickiness was measured against a physical Xbox 360 pad, since no virtual-pad driver is installed. A watcher sampled the window every 0.4 s, logging the
XInputGetStatebutton word,GetCursorPosdeltas andGetAsyncKeyState, and classified the hint bar by template diff against a known keyboard frame and a known gamepad frame:btn=0x0001(D-pad Up) andGetAsyncKeyState(VK_UP)true at the same instant.mouseMoved=7, 18, 20, 296…), which is the intended switch.I reset the bindings to defaults afterwards, so no test state is left behind.
The tab strip starts at the first row's left edge, so it lines up with the menu block rather than centring on the window. Callers pass that edge in from their own Yoga layout, which keeps the strip and the rows together on resize.
Checked at 1920x1080 windowed. Other resolutions are untested. The gamepad prompts do render, confirmed in the stickiness run above.