refactor: table-driven keymap dispatch + auto-derived help list

Replace the global key handling scattered through the main loop with a single
KEYMAP table (key, gate, action, help text) and a DispatchKeymap() pass. The
About/Help overlay now renders its key list straight from the table, so the
on-screen shortcuts can never drift from the actual bindings.

- Order-sensitive keys (Space, Esc) stay inline where their frame ordering
  matters; they carry action==NULL and appear in the table for the help list.
- Adds the F11 fullscreen binding the sidebar button already advertised but
  that was never actually wired up.
- Fix About title em-dash rendering as '?' (font is ASCII-only); use a hyphen.

Launch render verified pixel-identical (AE=0); keys verified via injected input.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 09:05:37 -07:00
parent 53f7fa6047
commit 99c7d43637
3 changed files with 105 additions and 43 deletions
+63 -37
View File
@@ -129,6 +129,66 @@ void ResetForNewSignal(void)
app.visibleTextureValid = false;
}
// ============================================================================
// Keymap — handlers + table + dispatcher. See spectrogram_types.h for the
// KeyBinding contract. Adding a global key = add one row here (and, if it needs
// to run at a specific point in the frame, leave action NULL and wire it inline).
// ============================================================================
static void ActionOpenBrowser(void) { app.showFileBrowser = true; ScanDirectory(GetWorkingDirectory()); }
static void ActionToggleScope(void) { app.showScope = !app.showScope; }
static void ActionToggleAbout(void) { app.showAbout = !app.showAbout; }
static void ActionToggleFullscreen(void){ ToggleFullscreen(); }
static void ActionExport(void) { ExportPNG(&app, app.exportDir); }
static void ActionResetView(void)
{
app.viewStart = 0.0f; app.viewEnd = 1.0f;
app.freqViewStart = 0.0f; app.freqViewEnd = 1.0f;
app.visibleTextureValid = false;
}
static void ActionZoomToStart(void)
{
app.viewStart = 0.0f;
app.viewEnd = 0.1f;
app.visibleTextureValid = false;
}
static const KeyBinding KEYMAP[] = {
{ KEY_O, KEYGATE_MODAL, ActionOpenBrowser, "O", "open file browser" },
{ KEY_P, KEYGATE_NONE, ActionToggleScope, "P", "show / hide waveform scope" },
{ KEY_F1, KEYGATE_NONE, ActionToggleAbout, "F1", "about / help" },
{ KEY_F11, KEYGATE_NONE, ActionToggleFullscreen,"F11", "toggle fullscreen" },
{ KEY_HOME, KEYGATE_MODAL | KEYGATE_LOADED,ActionResetView, "Home", "reset view (fit all)" },
{ KEY_END, KEYGATE_MODAL | KEYGATE_LOADED,ActionZoomToStart, "End", "zoom to start" },
{ KEY_E, KEYGATE_MODAL | KEYGATE_STFT, ActionExport, "E", "export PNG" },
// Order-sensitive: handled inline (see main loop), listed here for the overlay.
{ KEY_SPACE, KEYGATE_NONE, NULL, "Space", "play / stop selection" },
{ KEY_ESCAPE,KEYGATE_NONE, NULL, "Esc", "clear selection / close dialog" },
};
const KeyBinding* GetKeymap(int* count)
{
*count = (int)(sizeof(KEYMAP) / sizeof(KEYMAP[0]));
return KEYMAP;
}
// Run every gated, dispatchable binding whose key was pressed this frame.
static void DispatchKeymap(void)
{
int n;
const KeyBinding* km = GetKeymap(&n);
for (int i = 0; i < n; i++) {
const KeyBinding* b = &km[i];
if (!b->action) continue;
if ((b->gate & KEYGATE_MODAL) && UiModalOpen()) continue;
if ((b->gate & KEYGATE_LOADED) && !app.loaded) continue;
if ((b->gate & KEYGATE_STFT) && !app.stftComputed) continue;
if (IsKeyPressed(b->key)) b->action();
}
}
// ============================================================================
// Main Application
// ============================================================================
@@ -253,26 +313,9 @@ int main(int argc, char* argv[])
UnloadDroppedFiles(dropped);
}
// File browser toggle
if (IsKeyPressed(KEY_O) && !UiModalOpen()) {
app.showFileBrowser = true;
ScanDirectory(GetWorkingDirectory());
}
// Scope view toggle (P key)
if (IsKeyPressed(KEY_P)) {
app.showScope = !app.showScope;
}
// About / help dialog (F1)
if (IsKeyPressed(KEY_F1)) {
app.showAbout = !app.showAbout;
}
// File browser update
if (app.showFileBrowser) {
// Browser handles its own input
}
// Global key bindings (table-driven; see KEYMAP/GetKeymap). The
// order-sensitive keys (Space, Esc) are handled inline further below.
DispatchKeymap();
// Check if playback finished naturally
if (app.isPlaying && AudioPlaybackSound.frameCount > 0) {
@@ -455,18 +498,6 @@ int main(int argc, char* argv[])
}
}
// Home/End keys
if (IsKeyPressed(KEY_HOME)) {
app.viewStart = 0.0f; app.viewEnd = 1.0f;
app.freqViewStart = 0.0f; app.freqViewEnd = 1.0f;
app.visibleTextureValid = false;
}
if (IsKeyPressed(KEY_END)) {
float newWidth = 0.1f;
app.viewStart = 0.0f;
app.viewEnd = newWidth;
app.visibleTextureValid = false;
}
}
// Keyboard shortcuts (SPACE for play/stop toggle, ESC for clear)
@@ -490,11 +521,6 @@ int main(int argc, char* argv[])
}
}
// Export PNG
if (IsKeyPressed(KEY_E) && !UiModalOpen() && app.stftComputed) {
ExportPNG(&app, app.exportDir);
}
if (IsKeyPressed(KEY_ESCAPE)) {
if (app.showAbout) {
app.showAbout = false;