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:
+63
-37
@@ -129,6 +129,66 @@ void ResetForNewSignal(void)
|
|||||||
app.visibleTextureValid = false;
|
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
|
// Main Application
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -253,26 +313,9 @@ int main(int argc, char* argv[])
|
|||||||
UnloadDroppedFiles(dropped);
|
UnloadDroppedFiles(dropped);
|
||||||
}
|
}
|
||||||
|
|
||||||
// File browser toggle
|
// Global key bindings (table-driven; see KEYMAP/GetKeymap). The
|
||||||
if (IsKeyPressed(KEY_O) && !UiModalOpen()) {
|
// order-sensitive keys (Space, Esc) are handled inline further below.
|
||||||
app.showFileBrowser = true;
|
DispatchKeymap();
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if playback finished naturally
|
// Check if playback finished naturally
|
||||||
if (app.isPlaying && AudioPlaybackSound.frameCount > 0) {
|
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)
|
// 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 (IsKeyPressed(KEY_ESCAPE)) {
|
||||||
if (app.showAbout) {
|
if (app.showAbout) {
|
||||||
app.showAbout = false;
|
app.showAbout = false;
|
||||||
|
|||||||
@@ -233,6 +233,32 @@ static inline bool UiModalOpen(void)
|
|||||||
return app.showFileBrowser || app.showAbout;
|
return app.showFileBrowser || app.showAbout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Keymap — single source of truth for global key bindings.
|
||||||
|
// The dispatcher (DispatchKeymap in spectrogram.c) runs every entry whose
|
||||||
|
// `action` is non-NULL and whose gate passes; the About/Help overlay renders
|
||||||
|
// the whole table so the on-screen key list can never drift from the bindings.
|
||||||
|
// Order-sensitive keys (Space, Esc) carry action==NULL and are handled inline
|
||||||
|
// where their frame ordering matters; they appear here for documentation only.
|
||||||
|
// ============================================================================
|
||||||
|
typedef void (*KeyActionFn)(void);
|
||||||
|
|
||||||
|
#define KEYGATE_NONE 0u
|
||||||
|
#define KEYGATE_MODAL 1u // skip while a modal overlay is open (!UiModalOpen())
|
||||||
|
#define KEYGATE_LOADED 2u // require a loaded signal
|
||||||
|
#define KEYGATE_STFT 4u // require a computed STFT
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int key; // raylib key code
|
||||||
|
unsigned gate; // KEYGATE_* bitmask
|
||||||
|
KeyActionFn action; // NULL = handled inline / documentation-only
|
||||||
|
const char* label; // shown in the help overlay, e.g. "O", "Home"
|
||||||
|
const char* help; // description for the help overlay
|
||||||
|
} KeyBinding;
|
||||||
|
|
||||||
|
// Returns the keymap table and its entry count (defined in spectrogram.c).
|
||||||
|
const KeyBinding* GetKeymap(int* count);
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Small math helpers (header-inline so every module can use them)
|
// Small math helpers (header-inline so every module can use them)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -578,7 +578,7 @@ void DrawAboutDialog(void)
|
|||||||
DrawRectangle(0, 0, sw, sh, Fade(BLACK, 0.7f));
|
DrawRectangle(0, 0, sw, sh, Fade(BLACK, 0.7f));
|
||||||
|
|
||||||
float pw = 560 * scale;
|
float pw = 560 * scale;
|
||||||
float ph = 470 * scale;
|
float ph = 540 * scale;
|
||||||
Rectangle panel = { (sw - pw) / 2, (sh - ph) / 2, pw, ph };
|
Rectangle panel = { (sw - pw) / 2, (sh - ph) / 2, pw, ph };
|
||||||
DrawRectangleRec(panel, (Color){ 30, 30, 38, 255 });
|
DrawRectangleRec(panel, (Color){ 30, 30, 38, 255 });
|
||||||
DrawRectangleLinesEx(panel, 2, (Color){ 90, 90, 110, 255 });
|
DrawRectangleLinesEx(panel, 2, (Color){ 90, 90, 110, 255 });
|
||||||
@@ -586,7 +586,7 @@ void DrawAboutDialog(void)
|
|||||||
float px = panel.x + 24 * scale;
|
float px = panel.x + 24 * scale;
|
||||||
float py = panel.y + 20 * scale;
|
float py = panel.y + 20 * scale;
|
||||||
|
|
||||||
DrawTextScaled("rspektrum — synchrosqueezed spectrogram viewer", px, py, 18, WHITE);
|
DrawTextScaled("rspektrum - synchrosqueezed spectrogram viewer", px, py, 18, WHITE);
|
||||||
py += 34 * scale;
|
py += 34 * scale;
|
||||||
|
|
||||||
// Body lines. NULL = blank spacer; headings start with no leading spaces.
|
// Body lines. NULL = blank spacer; headings start with no leading spaces.
|
||||||
@@ -606,10 +606,6 @@ void DrawAboutDialog(void)
|
|||||||
" spectral magnitudes.",
|
" spectral magnitudes.",
|
||||||
" - Pixels show synchrosqueezed (reassigned) energy, not raw FFT bins.",
|
" - Pixels show synchrosqueezed (reassigned) energy, not raw FFT bins.",
|
||||||
" Good to ~a dB for visualization; not lab-grade metrology.",
|
" Good to ~a dB for visualization; not lab-grade metrology.",
|
||||||
"",
|
|
||||||
"Keys",
|
|
||||||
" O open file P toggle scope Home/End reset / fit view",
|
|
||||||
" F1 / Esc close this dialog",
|
|
||||||
};
|
};
|
||||||
int n = sizeof(lines) / sizeof(lines[0]);
|
int n = sizeof(lines) / sizeof(lines[0]);
|
||||||
for (int i = 0; i < n; i++) {
|
for (int i = 0; i < n; i++) {
|
||||||
@@ -619,6 +615,20 @@ void DrawAboutDialog(void)
|
|||||||
py += (s[0] == '\0' ? 8 : 18) * scale;
|
py += (s[0] == '\0' ? 8 : 18) * scale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keys — rendered straight from the keymap table so this list can never
|
||||||
|
// drift from the actual bindings.
|
||||||
|
py += 10 * scale;
|
||||||
|
DrawTextScaled("Keys", px, py, 14, (Color){ 150, 200, 255, 255 });
|
||||||
|
py += 20 * scale;
|
||||||
|
int kn;
|
||||||
|
const KeyBinding* km = GetKeymap(&kn);
|
||||||
|
for (int i = 0; i < kn; i++) {
|
||||||
|
DrawTextScaled(TextFormat(" %-5s %s", km[i].label, km[i].help), px, py, 13, LIGHTGRAY);
|
||||||
|
py += 16 * scale;
|
||||||
|
}
|
||||||
|
DrawTextScaled(" Mouse wheel = zoom, Alt+drag = pan, drag = select box",
|
||||||
|
px, py, 13, LIGHTGRAY);
|
||||||
|
|
||||||
DrawTextScaled("F1 / Esc / click to close", panel.x + pw - 196 * scale,
|
DrawTextScaled("F1 / Esc / click to close", panel.x + pw - 196 * scale,
|
||||||
panel.y + ph - 26 * scale, 12, GRAY);
|
panel.y + ph - 26 * scale, 12, GRAY);
|
||||||
// Note: opening/closing is handled in the main input loop (not here) so the
|
// Note: opening/closing is handled in the main input loop (not here) so the
|
||||||
|
|||||||
Reference in New Issue
Block a user