feat(menu): add compact TV-style tuning bar for live setting tweaks #2378

Merged
B16B0SS merged 2 commits from feat/menu-tuning-bar into main 2026-11-11 20:01:48 +00:00
B16B0SS commented 2026-11-11 16:43:02 +00:00 (Migrated from github.com)

Why is this PR needed?

The full settings menu covers ~80% of the screen, making it difficult to gauge real-time visual changes (sharpness, resolution scaling, upscaler presets) during live gameplay. This PR introduces a compact TV-style tuning bar at the bottom of the screen (~10% height) so settings can be adjusted with over 90% of the game scene visible. It can be invoked from the normal settings menu or as a special keybind directly from the game.

What changes?
Compact Tuning Bar (draw_tune_bar): Renders a sleek bottom card showing the current tab, active setting row, value editor, and navigation hints without full-screen dimming.

Two Entry / Exit Paths:
Option A (In-menu toggle): Pressing the configured modifier key (Right Shift by default) or controller X/Square switches smoothly between the full menu and the tuning bar.
Option B (Direct game shortcut): Holding the configured key while pressing Insert or F1 jumps directly from the game into the compact tuning bar; Esc / B closes straight back to the game.

Controls Configuration (Alt menu shortcut): Adds an entry under the Controls tab to choose between Right Shift (default) and Right Alt, leaving left-hand gaming keys (WASD, sprint/roll) completely free from interference.

Deduplicated Rendering (row_value_cell): Sliders, toggles, choices, and subpage links share a single unified cell renderer between the full menu and tuning bar, preventing double maintenance when upstream adds new options.

Dynamic Prompts: Menu prompt bar shows [RShift] Tune or [RAlt] Tune (and controller West button dot) dynamically.

New Test: Added tests/test_menu_tune.py covering Option A/B triggers, prompt glyphs, and non-dimmed drawing.

Why is this PR needed? The full settings menu covers ~80% of the screen, making it difficult to gauge real-time visual changes (sharpness, resolution scaling, upscaler presets) during live gameplay. This PR introduces a compact TV-style tuning bar at the bottom of the screen (~10% height) so settings can be adjusted with over 90% of the game scene visible. It can be invoked from the normal settings menu or as a special keybind directly from the game. What changes? Compact Tuning Bar (draw_tune_bar): Renders a sleek bottom card showing the current tab, active setting row, value editor, and navigation hints without full-screen dimming. Two Entry / Exit Paths: Option A (In-menu toggle): Pressing the configured modifier key (Right Shift by default) or controller X/Square switches smoothly between the full menu and the tuning bar. Option B (Direct game shortcut): Holding the configured key while pressing Insert or F1 jumps directly from the game into the compact tuning bar; Esc / B closes straight back to the game. Controls Configuration (Alt menu shortcut): Adds an entry under the Controls tab to choose between Right Shift (default) and Right Alt, leaving left-hand gaming keys (WASD, sprint/roll) completely free from interference. Deduplicated Rendering (row_value_cell): Sliders, toggles, choices, and subpage links share a single unified cell renderer between the full menu and tuning bar, preventing double maintenance when upstream adds new options. Dynamic Prompts: Menu prompt bar shows [RShift] Tune or [RAlt] Tune (and controller West button dot) dynamically. New Test: Added tests/test_menu_tune.py covering Option A/B triggers, prompt glyphs, and non-dimmed drawing.
B16B0SS commented 2026-11-11 16:44:13 +00:00 (Migrated from github.com)
2026-11-11T12:37:40,197050645-04:00 2026-11-11T12:38:51,784596023-04:00 2026-11-11T12:43:53,514887464-04:00
<img width="1271" height="713" alt="2026-11-11T12:37:40,197050645-04:00" src="https://github.com/user-attachments/assets/171c56da-4143-4ec8-aa53-e3beef4c693b" /> <img width="1268" height="708" alt="2026-11-11T12:38:51,784596023-04:00" src="https://github.com/user-attachments/assets/9d04a7a4-98f9-4e3d-bccd-d6f6b9f2defb" /> <img width="1266" height="710" alt="2026-11-11T12:43:53,514887464-04:00" src="https://github.com/user-attachments/assets/ccda4541-707b-4833-8b05-9e2eeb4578a9" />
hckrman101 (Migrated from github.com) reviewed 2026-11-11 16:54:56 +00:00
hckrman101 (Migrated from github.com) left a comment

Thanks, a bar you can tune with while still seeing the picture is a nice idea, and pulling the value cell out into row_value_cell keeps the two views from drifting. The screenshots in the thread help. The settings key is saved by row name (alt_menu_shortcut= in hookshot_options.ini), so no option id is used and nothing collides with main or the open menu PRs (#2177, #2304, #2197, #2119 touch other hunks). When the menu is closed the new code only drains the menu's own key queue, so game input isn't touched.

A few things to fix before it goes in (details inline):

  • Right Shift + Tab toggles the bar. Shift+Tab is the existing "previous tab" key. With Right Shift the Shift press arrives a frame or more before Tab, so !press[12] doesn't catch it and the menu flips into (or out of) tune mode before the tab changes.
  • "Right Alt" can't toggle on Windows. The queued presses come from WM_KEYDOWN in host/display.c, but a plain Alt press arrives as WM_SYSKEYDOWN, which isn't recorded. So take_key_press(VK_RMENU/VK_MENU) never fires there. Holding it with Insert still works (that path uses GetAsyncKeyState). Linux is fine, because display_sdl.c maps both keys.
  • The dim is dropped for every page while tune_mode is set, not only for the bar. Open a page from the bar (Controls, Teleport, Inventory) or get the restart question, and the full-screen page draws straight over the undimmed game.
  • The full menu's Debug-tab prompts lose their confirm entry for tools that both change and confirm (tools 0, 1 and 7 "Toggle", 4 "Set", 6 "Apply", 9 "Switch"). Before this PR the prompts showed Change and the confirm action together.

Nits:

  • The PR says X/Square, but pad(HOS_PAD_NORTH) toggles the bar too, and the glyph only lights the West dot.
  • row_can_tune and menu_is_tuning() are never used.
  • On the Motion page the bar's header reads "Settings" (tab_title gets TAB_MOTION).
  • "Alt menu shortcut" reads like a second key for opening the menu. Something like "Tuning bar key" says what the row does.
  • The new test only greps for exact source lines, so it pins the wording and doesn't test behaviour. That's fine, but a small compiled check of the toggle and the dim would catch the issues above.

CI hasn't run yet: the workflows are waiting for approval (action_required). The static checks I looked at that cut code out of vk_menu.cpp (test_menu_keys' key-poll block, test_audio_volume's tab_titles regex, the fps tests) still match the new layout, but this needs a real run. Since this adds a new UI mode and a new keybind, whether it belongs in the menu is NotRexed's call.

Thanks, a bar you can tune with while still seeing the picture is a nice idea, and pulling the value cell out into `row_value_cell` keeps the two views from drifting. The screenshots in the thread help. The settings key is saved by row name (`alt_menu_shortcut=` in hookshot_options.ini), so no option id is used and nothing collides with main or the open menu PRs (#2177, #2304, #2197, #2119 touch other hunks). When the menu is closed the new code only drains the menu's own key queue, so game input isn't touched. A few things to fix before it goes in (details inline): - **Right Shift + Tab toggles the bar.** Shift+Tab is the existing "previous tab" key. With Right Shift the Shift press arrives a frame or more before Tab, so `!press[12]` doesn't catch it and the menu flips into (or out of) tune mode before the tab changes. - **"Right Alt" can't toggle on Windows.** The queued presses come from `WM_KEYDOWN` in host/display.c, but a plain Alt press arrives as `WM_SYSKEYDOWN`, which isn't recorded. So `take_key_press(VK_RMENU/VK_MENU)` never fires there. Holding it with Insert still works (that path uses `GetAsyncKeyState`). Linux is fine, because display_sdl.c maps both keys. - **The dim is dropped for every page while `tune_mode` is set**, not only for the bar. Open a page from the bar (Controls, Teleport, Inventory) or get the restart question, and the full-screen page draws straight over the undimmed game. - **The full menu's Debug-tab prompts lose their confirm entry** for tools that both change and confirm (tools 0, 1 and 7 "Toggle", 4 "Set", 6 "Apply", 9 "Switch"). Before this PR the prompts showed Change and the confirm action together. Nits: - The PR says X/Square, but `pad(HOS_PAD_NORTH)` toggles the bar too, and the glyph only lights the West dot. - `row_can_tune` and `menu_is_tuning()` are never used. - On the Motion page the bar's header reads "Settings" (`tab_title` gets `TAB_MOTION`). - "Alt menu shortcut" reads like a second key for opening the menu. Something like "Tuning bar key" says what the row does. - The new test only greps for exact source lines, so it pins the wording and doesn't test behaviour. That's fine, but a small compiled check of the toggle and the dim would catch the issues above. CI hasn't run yet: the workflows are waiting for approval (action_required). The static checks I looked at that cut code out of vk_menu.cpp (test_menu_keys' key-poll block, test_audio_volume's `tab_titles` regex, the fps tests) still match the new layout, but this needs a real run. Since this adds a new UI mode and a new keybind, whether it belongs in the menu is NotRexed's call.
hckrman101 (Migrated from github.com) commented 2026-11-11 16:54:56 +00:00

Small regression: before, a Debug tool that both changes and confirms (cheats 0, 1, 4, 6, 7, 9: "Toggle", "Set", "Apply", "Switch") showed Change and its confirm prompt. This branch shows only Change. Adding if (selr.cheat >= 0) if (const char* says = cheat_confirm(selr.cheat)) prompt_list[prompts_n++] = {ui::kConfirm, says}; here would keep it (there's room in the 7).

Small regression: before, a Debug tool that both changes and confirms (cheats 0, 1, 4, 6, 7, 9: "Toggle", "Set", "Apply", "Switch") showed Change *and* its confirm prompt. This branch shows only Change. Adding `if (selr.cheat >= 0) if (const char* says = cheat_confirm(selr.cheat)) prompt_list[prompts_n++] = {ui::kConfirm, says};` here would keep it (there's room in the 7).
hckrman101 (Migrated from github.com) commented 2026-11-11 16:54:56 +00:00

This skips the dim for every page while tune_mode is set. From the bar, Enter on Controls opens page 1, Teleport and Inventory open pages 3 and 4, and closing with a restart-needed change gives page 5. All of those then draw full-screen over the undimmed game. if (!(tune_mode && page == 0)) would keep the dim for them.

This skips the dim for every page while `tune_mode` is set. From the bar, Enter on Controls opens page 1, Teleport and Inventory open pages 3 and 4, and closing with a restart-needed change gives page 5. All of those then draw full-screen over the undimmed game. `if (!(tune_mode && page == 0))` would keep the dim for them.
hckrman101 (Migrated from github.com) commented 2026-11-11 16:54:56 +00:00

On Windows, take_key_press only sees keys noted from WM_KEYDOWN (host/display.c). A plain Alt press comes as WM_SYSKEYDOWN, so with "Right Alt" chosen this never returns true and the in-menu toggle does nothing. Noting VK_MENU in the WM_SYSKEYDOWN case would fix that, or the tune key could be edge-detected from GetAsyncKeyState like the Linux path. (Releasing a lone Alt also sends SC_KEYMENU to the window, which is worth a quick check in the game.)

On Windows, `take_key_press` only sees keys noted from `WM_KEYDOWN` (host/display.c). A plain Alt press comes as `WM_SYSKEYDOWN`, so with "Right Alt" chosen this never returns true and the in-menu toggle does nothing. Noting `VK_MENU` in the `WM_SYSKEYDOWN` case would fix that, or the tune key could be edge-detected from `GetAsyncKeyState` like the Linux path. (Releasing a lone Alt also sends `SC_KEYMENU` to the window, which is worth a quick check in the game.)
hckrman101 (Migrated from github.com) commented 2026-11-11 16:54:56 +00:00

With Right Shift as the key, Shift+Tab (previous tab) toggles tune mode: the Shift press lands a frame or more before Tab, so press[12] is still false when tune_press fires. You could fire on the release of the key when nothing else was pressed while it was held, or skip the toggle while Tab is down or was pressed during the hold. Also, HOS_PAD_NORTH toggles too, but the description and the glyph only mention X/Square (West).

With Right Shift as the key, Shift+Tab (previous tab) toggles tune mode: the Shift press lands a frame or more before Tab, so `press[12]` is still false when `tune_press` fires. You could fire on the *release* of the key when nothing else was pressed while it was held, or skip the toggle while Tab is down or was pressed during the hold. Also, `HOS_PAD_NORTH` toggles too, but the description and the glyph only mention X/Square (West).
B16B0SS commented 2026-11-11 17:13:04 +00:00 (Migrated from github.com)
https://github.com/user-attachments/assets/72170716-1cad-41bb-a9fd-01dfd8272aa7
B16B0SS (Migrated from github.com) reviewed 2026-11-11 18:06:01 +00:00
B16B0SS (Migrated from github.com) commented 2026-11-11 18:06:01 +00:00

change to release fire

change to release fire
B16B0SS (Migrated from github.com) reviewed 2026-11-11 18:06:39 +00:00
B16B0SS (Migrated from github.com) commented 2026-11-11 18:06:39 +00:00

noted, and switched to right ctrl just to avoid the entire issue

noted, and switched to right ctrl just to avoid the entire issue
B16B0SS (Migrated from github.com) reviewed 2026-11-11 18:07:28 +00:00
B16B0SS (Migrated from github.com) commented 2026-11-11 18:07:27 +00:00

got it - adjusted to check for page zero to add the dim on secondary screens executed through the compact menu

got it - adjusted to check for page zero to add the dim on secondary screens executed through the compact menu
B16B0SS (Migrated from github.com) reviewed 2026-11-11 18:17:28 +00:00
B16B0SS (Migrated from github.com) commented 2026-11-11 18:17:28 +00:00

I interpreted "prompt" as "hint" here , I think that is what we are looking for. I have added this back in

I interpreted "prompt" as "hint" here , I think that is what we are looking for. I have added this back in
B16B0SS commented 2026-11-11 18:18:28 +00:00 (Migrated from github.com)

Hey - made the changes you requested - sidestepped right alt and chose right ctrl. Otherwise took your suggestions and pulled head back into so its ready to merge should you like the work

oh and went with "Compact menu"

Hey - made the changes you requested - sidestepped right alt and chose right ctrl. Otherwise took your suggestions and pulled head back into so its ready to merge should you like the work oh and went with "Compact menu"
hckrman101 (Migrated from github.com) reviewed 2026-11-11 18:25:29 +00:00
hckrman101 (Migrated from github.com) left a comment

Thanks for the quick turnaround, checked the new head against my last review.

Fixed:

  • Shift+Tab: the toggle now fires on the key's release and is cancelled if Tab (or a tab/arrow/Enter/Esc press or a click) happens while it's held, so previous-tab no longer flips the bar. North no longer toggles, only West, which matches the glyph.
  • Windows Alt: sidestepped by switching to Right Ctrl. GetAsyncKeyState(VK_RCONTROL) works on both Windows and the Linux key table (display_sdl.c fills VK_RCONTROL), so the in-menu toggle works on both.
  • Dimming: !(tune_mode && page == 0) keeps the dim for Controls, Teleport, Inventory and the restart question.
  • Debug prompts: the confirm prompt (Toggle/Set/Apply/Switch) is back next to Change.
  • Nits: row_can_tune is gone, the bar says "Motion" on the Motion page, and "Compact menu key" is a clear row name.

New, small:

  • row_value_cell has a branch the old full-menu code didn't: Debug tools with no value but a confirm (Apply x2, Open list, Switch age, Open) now show that word in their value cell instead of the ▸ arrow, in the full menu too (inline). Fine if intended, but it changes the Debug tab's look outside the bar, so worth a line in the description.
  • Nit: in the bar, Restart and Quit get an "Open" prompt (!r.value && r.cheat < 0), where the full menu shows the row's name / "Confirm".
  • Nit: the test's fixture compiles copies of the toggle and dim logic rather than the code in vk_menu.cpp, so it would still pass if menu_tick drifted. Cutting the real snippet out (as other menu tests do) would make it bite.

CI: the build and test workflows (Windows builder, game-free source regressions, Linux AppImage) are waiting for a maintainer to approve the run, so nothing beyond the title checks has run yet. No conflicts with main.

The bugs I raised are fixed. Whether this UI mode belongs in the menu is still NotRexed's call, so I'm leaving the decision with them.

Thanks for the quick turnaround, checked the new head against my last review. Fixed: - Shift+Tab: the toggle now fires on the key's release and is cancelled if Tab (or a tab/arrow/Enter/Esc press or a click) happens while it's held, so previous-tab no longer flips the bar. North no longer toggles, only West, which matches the glyph. - Windows Alt: sidestepped by switching to Right Ctrl. `GetAsyncKeyState(VK_RCONTROL)` works on both Windows and the Linux key table (display_sdl.c fills `VK_RCONTROL`), so the in-menu toggle works on both. - Dimming: `!(tune_mode && page == 0)` keeps the dim for Controls, Teleport, Inventory and the restart question. - Debug prompts: the confirm prompt (Toggle/Set/Apply/Switch) is back next to Change. - Nits: `row_can_tune` is gone, the bar says "Motion" on the Motion page, and "Compact menu key" is a clear row name. New, small: - `row_value_cell` has a branch the old full-menu code didn't: Debug tools with no value but a confirm (Apply x2, Open list, Switch age, Open) now show that word in their value cell instead of the ▸ arrow, in the full menu too (inline). Fine if intended, but it changes the Debug tab's look outside the bar, so worth a line in the description. - Nit: in the bar, Restart and Quit get an "Open" prompt (`!r.value && r.cheat < 0`), where the full menu shows the row's name / "Confirm". - Nit: the test's fixture compiles copies of the toggle and dim logic rather than the code in vk_menu.cpp, so it would still pass if menu_tick drifted. Cutting the real snippet out (as other menu tests do) would make it bite. CI: the build and test workflows (Windows builder, game-free source regressions, Linux AppImage) are waiting for a maintainer to approve the run, so nothing beyond the title checks has run yet. No conflicts with main. The bugs I raised are fixed. Whether this UI mode belongs in the menu is still NotRexed's call, so I'm leaving the decision with them.
@ -2000,3 +1957,3 @@
} else {
const std::string v = cheat_value(r.cheat);
choice_cell(c, va, vb, v.empty() ? buttons[r.cheat] : v, selected, cheat_chooses(r.cheat), cheat_chooses(r.cheat), r.cheat == 9);
choice_cell(c, va, vb, v.empty() ? cheat_button_text(r.cheat) : v, selected, cheat_chooses(r.cheat), cheat_chooses(r.cheat), r.cheat == 9);
hckrman101 (Migrated from github.com) commented 2026-11-11 18:25:29 +00:00

This branch is new compared with the old inline code: before, a Debug tool with an empty value fell through to !r.value and drew the ▸ arrow; now cheats 2, 3, 8, 9 and 10 show "Apply" / "Open list" / "Switch" / "Open" as their value in the full menu as well. Fine if that's the intent, just flagging that it changes the full Debug tab too.

This branch is new compared with the old inline code: before, a Debug tool with an empty value fell through to `!r.value` and drew the ▸ arrow; now cheats 2, 3, 8, 9 and 10 show "Apply" / "Open list" / "Switch" / "Open" as their value in the full menu as well. Fine if that's the intent, just flagging that it changes the full Debug tab too.
B16B0SS commented 2026-11-11 19:50:45 +00:00 (Migrated from github.com)

Thanks for the feedback an kind words. Here is what I have for you
1 - restart/quit - fixed so that on first selection the prompt says the action (enter or quit) and on enter you get "confirm", just like the F1 menu
2 - updated the test to use real code so it actually is a test
3 - unified the button labels under a single cheat_button_text() method so both the full menu and compact bar pull from the same table

let me know if there is anyhting else

Thanks for the feedback an kind words. Here is what I have for you 1 - restart/quit - fixed so that on first selection the prompt says the action (enter or quit) and on enter you get "confirm", just like the F1 menu 2 - updated the test to use real code so it actually is a test 3 - unified the button labels under a single cheat_button_text() method so both the full menu and compact bar pull from the same table let me know if there is anyhting else
hckrman101 (Migrated from github.com) reviewed 2026-11-11 19:51:56 +00:00
hckrman101 (Migrated from github.com) left a comment

Thanks for the fast follow-up, checked 7aed804b against c44fbb62 (the head was rebased onto main, so I compared the patches).

Fixed since my last review:

  • The test now cuts the real toggle and dim code out of vk_menu.cpp, so it would catch drift in menu_tick.
  • Restart and Quit in the bar: the hint shows the row's name first and "Confirm" on the pending press, like the full menu.
  • Debug button labels: cheat_button_text() is now the one table both the full menu and row_value_cell read, and the full Debug tab draws the same words as before this PR (Mark, Return, Choose map, Switch, Edit items). That also settles my earlier note about the changed Debug look, so nothing needs adding to the description.

Still open (small):

  • The new test is not declared for tests/run_all.py. It compiles a fixture, but has no RUN_ALL_ARGS = ["--cxx", "{cxx}"] (see test_menu_keys.py), so it always uses g++, which the llvm-mingw Windows runner may not have. Please add the line; it already takes --cxx.

Not blocking: the settings key is saved by row name (no option id), so no collision. The three build and test workflows are still waiting for a maintainer to approve the run (action_required), so none of the source checks have run on this head. I read the other tests that cut code from vk_menu.cpp (test_menu_keys, test_audio_volume's tab_titles) against the new layout and they still match, but a real run is needed.

Whether this UI mode and keybind belong in the menu is still NotRexed's call, so I am leaving the decision with them.

Thanks for the fast follow-up, checked 7aed804b against c44fbb62 (the head was rebased onto main, so I compared the patches). Fixed since my last review: - The test now cuts the real toggle and dim code out of vk_menu.cpp, so it would catch drift in `menu_tick`. - Restart and Quit in the bar: the hint shows the row's name first and "Confirm" on the pending press, like the full menu. - Debug button labels: `cheat_button_text()` is now the one table both the full menu and `row_value_cell` read, and the full Debug tab draws the same words as before this PR (Mark, Return, Choose map, Switch, Edit items). That also settles my earlier note about the changed Debug look, so nothing needs adding to the description. Still open (small): - The new test is not declared for `tests/run_all.py`. It compiles a fixture, but has no `RUN_ALL_ARGS = ["--cxx", "{cxx}"]` (see test_menu_keys.py), so it always uses `g++`, which the llvm-mingw Windows runner may not have. Please add the line; it already takes `--cxx`. Not blocking: the settings key is saved by row name (no option id), so no collision. The three build and test workflows are still waiting for a maintainer to approve the run (action_required), so none of the source checks have run on this head. I read the other tests that cut code from vk_menu.cpp (test_menu_keys, test_audio_volume's `tab_titles`) against the new layout and they still match, but a real run is needed. Whether this UI mode and keybind belong in the menu is still NotRexed's call, so I am leaving the decision with them.
@ -0,0 +19,4 @@
RUN_ALL_ARGS = ["--cxx", "{cxx}"]
ROOT = Path(__file__).resolve().parents[1]
hckrman101 (Migrated from github.com) commented 2026-11-11 19:51:56 +00:00

Add RUN_ALL_ARGS = ["--cxx", "{cxx}"] here (as test_menu_keys.py does) so run_all passes the CI's compiler instead of the default g++.

Add `RUN_ALL_ARGS = ["--cxx", "{cxx}"]` here (as test_menu_keys.py does) so run_all passes the CI's compiler instead of the default `g++`.
hckrman101 commented 2026-11-11 19:56:08 +00:00 (Migrated from github.com)

Fixing this myself^

Fixing this myself^
hckrman101 (Migrated from github.com) approved these changes 2026-11-11 20:01:37 +00:00
hckrman101 (Migrated from github.com) left a comment

I pushed one small commit on top: tests/test_menu_tune.py now declares RUN_ALL_ARGS = ["--cxx", "{cxx}"], so run_all passes the platform compiler instead of falling back to g++ (it passes locally). That was the last open point; everything from the earlier rounds is fixed. Looks good to me, thanks for iterating on this.

CI still needs a maintainer to approve the workflow runs before it goes in.

I pushed one small commit on top: `tests/test_menu_tune.py` now declares `RUN_ALL_ARGS = ["--cxx", "{cxx}"]`, so run_all passes the platform compiler instead of falling back to g++ (it passes locally). That was the last open point; everything from the earlier rounds is fixed. Looks good to me, thanks for iterating on this. CI still needs a maintainer to approve the workflow runs before it goes in.
Commenting is not possible because the repository is archived.
No description provided.