From e40645f280ceddea1ee4a1e537f0b17b9c5d5c85 Mon Sep 17 00:00:00 2001 From: Anachronaut Date: Tue, 7 Jul 2026 22:32:46 -0400 Subject: [PATCH] Fix patch browser keyboard navigation to cover directory rows Arrow keys now navigate the full virtual row list (../, subdirs, patches) rather than only patch rows. Landing on a patch auto-loads it as before; landing on .. or a subdir highlights it and Enter confirms navigation. Added patchVCursor to UIState to track keyboard focus independently of the loaded patch. Mouse-click on a patch now also syncs the cursor so subsequent arrow-key movement starts from the right position. Co-Authored-By: Claude Sonnet 4.6 --- Readme.md | 16 ------ include/ui.h | 1 + patches/Keys/Vibraphone.json | 50 ------------------ source/ui.c | 100 ++++++++++++++++++++++++----------- 4 files changed, 71 insertions(+), 96 deletions(-) delete mode 100644 Readme.md delete mode 100644 patches/Keys/Vibraphone.json diff --git a/Readme.md b/Readme.md deleted file mode 100644 index e25603b..0000000 --- a/Readme.md +++ /dev/null @@ -1,16 +0,0 @@ -# soundThing - - A basic demonstration of generating sound samples and feeding an audio buffer, along with opening a MIDI port with ALSA, and playing back tones from the sound generator using MIDI events. - -## Building: - - Depends on Raylib and probably libasound2-dev, so install those if you don't have them already. - - 1) Clone the repository. - 2) run make from inside the repository directory. - -### Running and use: - - 1) Run make run from inside the repository directory. - 2) Specify the client and port for the MIDI device you want to use to control soundThing. - 3) You should now be able to play back notes by sending soundThing MIDI data. Use the up and down arrows to change waveforms. diff --git a/include/ui.h b/include/ui.h index b0fb17a..cf462b0 100644 --- a/include/ui.h +++ b/include/ui.h @@ -72,6 +72,7 @@ typedef struct { char patchCurrentDir[256]; char patchSubDirs[PATCH_MAX_DIRS][PATCH_NAME_LEN]; int patchSubDirCount; + int patchVCursor; // Control registry — built lazily by uiKnob/uiSlider for CC mapping persistence UIControlEntry controlRegistry[128]; diff --git a/patches/Keys/Vibraphone.json b/patches/Keys/Vibraphone.json deleted file mode 100644 index 15ecab9..0000000 --- a/patches/Keys/Vibraphone.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "osc0_waveform": 0, - "osc0_dutyCycle": 0.500000, - "osc0_detune": 0.000000, - "osc0_gain": 4.000000, - "osc0_active": 1, - "osc0_octave": 0, - "osc0_modRouting0": 0, - "osc0_modRouting1": 3, - "osc0_modRouting2": 0, - "osc0_modDepth0": 0.000000, - "osc0_modDepth1": 10.000000, - "osc0_modDepth2": 0.000000, - "osc1_waveform": 0, - "osc1_dutyCycle": 0.500000, - "osc1_detune": 2.000000, - "osc1_gain": 0.300000, - "osc1_active": 1, - "osc1_octave": 1, - "osc1_modRouting0": 0, - "osc1_modRouting1": 0, - "osc1_modRouting2": 2, - "osc1_modDepth0": 0.000000, - "osc1_modDepth1": 0.000000, - "osc1_modDepth2": 1.500000, - "ampEnv_attack": 0.001000, - "ampEnv_decay": 2.000000, - "ampEnv_sustain": 0.000000, - "ampEnv_release": 0.600000, - "modEnv_attack": 0.001000, - "modEnv_decay": 0.080000, - "modEnv_sustain": 0.000000, - "modEnv_release": 0.040000, - "lfo0_rate": 4.500000, - "lfo0_waveform": 0, - "lfo0_active": 1, - "lfo1_rate": 1.000000, - "lfo1_waveform": 0, - "lfo1_active": 0, - "filter_cutoff": 10000.000000, - "filter_resonance": 0.100000, - "filter_type": 0, - "filter_active": 1, - "filter_modRouting": 0, - "filter_modDepth": 0.000000, - "filter_resModRouting": 0, - "filter_resModDepth": 0.000000, - "master_volume": 0.800000, - "master_pitchBendRange": 2.000000 -} diff --git a/source/ui.c b/source/ui.c index c96b2bf..d10ed6a 100644 --- a/source/ui.c +++ b/source/ui.c @@ -137,6 +137,7 @@ void uiInit(UIState *ui, MidiState *midi, Camera2D *camera) ui->patchFavCount = patchLoadFavs(ui->patchFavs, PATCH_MAX_FILES); ui->patchCurrentDir[0] = '\0'; ui->patchSubDirCount = 0; + ui->patchVCursor = -1; ui->controlRegistryCount = 0; ui->pendingCount = 0; } @@ -827,6 +828,7 @@ static void patchRescan(UIState *ui) ui->patchFiles, PATCH_MAX_FILES, ui->patchSubDirs, PATCH_MAX_DIRS, &ui->patchSubDirCount); ui->patchScrollOffset = 0; + ui->patchVCursor = -1; } void uiPatchMenu(UIState *ui, float x, float y, float width, Synth *s) @@ -1042,32 +1044,69 @@ void uiPatchMenu(UIState *ui, float x, float y, float width, Synth *s) ui->patchScrollOffset -= (int)GetMouseWheelMove(); } - // Arrow key navigation: move selection through patches only + // Arrow key navigation over all virtual rows: [..], subdirs, patches if (ui->focusedTextInput == -1 && !ui->patchConfirmOverwrite && !ui->patchConfirmClear - && filteredCount > 0) { - int curFi = -1; - for (int fi = 0; fi < filteredCount; fi++) { - if (strcmp(ui->patchFiles[filteredIdx[fi]], ui->patchCurrentName) == 0) { - curFi = fi; - break; + && totalVirtual > 0) { + // Sync cursor from currently loaded patch if unset + if (ui->patchVCursor < 0 && ui->patchCurrentName[0]) { + for (int fi = 0; fi < filteredCount; fi++) { + if (strcmp(ui->patchFiles[filteredIdx[fi]], ui->patchCurrentName) == 0) { + ui->patchVCursor = fi + backRows + ui->patchSubDirCount; + break; + } } } - int nextFi = curFi; - if (IsKeyPressed(KEY_DOWN) && curFi < filteredCount - 1) nextFi = curFi + 1; - if (IsKeyPressed(KEY_UP) && curFi > 0) nextFi = curFi - 1; - if (nextFi != curFi) { - int i = filteredIdx[nextFi]; - char path[512]; - buildPatchPath(path, sizeof(path), ui->patchCurrentDir, ui->patchFiles[i]); - if (patchLoad(s, path)) { - snprintf(ui->patchCurrentName, PATCH_NAME_LEN, "%s", ui->patchFiles[i]); - snprintf(ui->patchSaveName, PATCH_NAME_LEN, "%s", ui->patchFiles[i]); + int next = ui->patchVCursor; + if (IsKeyPressed(KEY_DOWN)) + next = (next < 0) ? 0 : (next < totalVirtual - 1 ? next + 1 : next); + if (IsKeyPressed(KEY_UP)) + next = (next < 0) ? totalVirtual - 1 : (next > 0 ? next - 1 : next); + if (next != ui->patchVCursor) { + ui->patchVCursor = next; + // Auto-load when landing on a patch row + if (next >= backRows + ui->patchSubDirCount && filteredCount > 0) { + int fi = next - backRows - ui->patchSubDirCount; + int i = filteredIdx[fi]; + char path[512]; + buildPatchPath(path, sizeof(path), ui->patchCurrentDir, ui->patchFiles[i]); + if (patchLoad(s, path)) { + snprintf(ui->patchCurrentName, PATCH_NAME_LEN, "%s", ui->patchFiles[i]); + snprintf(ui->patchSaveName, PATCH_NAME_LEN, "%s", ui->patchFiles[i]); + } } - int vRow = nextFi + backRows + ui->patchSubDirCount; - if (vRow < ui->patchScrollOffset) - ui->patchScrollOffset = vRow; - if (vRow >= ui->patchScrollOffset + MAX_VIS) - ui->patchScrollOffset = vRow - MAX_VIS + 1; + } + // Enter activates navigation rows (.. and subdirs) + if (IsKeyPressed(KEY_ENTER) && ui->patchVCursor >= 0) { + int vc = ui->patchVCursor; + if (!isRoot && vc == 0) { + char *lastSlash = strrchr(ui->patchCurrentDir, '/'); + if (lastSlash) *lastSlash = '\0'; + else ui->patchCurrentDir[0] = '\0'; + ui->patchCurrentName[0] = '\0'; + patchRescan(ui); + return; + } else if (vc >= backRows && vc < backRows + ui->patchSubDirCount) { + int d = vc - backRows; + if (ui->patchCurrentDir[0]) { + size_t curLen = strlen(ui->patchCurrentDir); + snprintf(ui->patchCurrentDir + curLen, + sizeof(ui->patchCurrentDir) - curLen, + "/%s", ui->patchSubDirs[d]); + } else { + snprintf(ui->patchCurrentDir, sizeof(ui->patchCurrentDir), + "%s", ui->patchSubDirs[d]); + } + ui->patchCurrentName[0] = '\0'; + patchRescan(ui); + return; + } + } + // Scroll to keep cursor visible + if (ui->patchVCursor >= 0) { + if (ui->patchVCursor < ui->patchScrollOffset) + ui->patchScrollOffset = ui->patchVCursor; + if (ui->patchVCursor >= ui->patchScrollOffset + MAX_VIS) + ui->patchScrollOffset = ui->patchVCursor - MAX_VIS + 1; } } @@ -1099,10 +1138,10 @@ void uiPatchMenu(UIState *ui, float x, float y, float width, Synth *s) if (!isRoot && r == 0) { // ".." — navigate to parent - DrawRectangle((int)(x + padding), (int)contentY, (int)btnW, (int)(rowH - 2), - (Color){20, 30, 50, 255}); - DrawRectangleLines((int)(x + padding), (int)contentY, (int)btnW, (int)(rowH - 2), - (Color){80, 120, 180, 255}); + Color dotBg = (ui->patchVCursor == r) ? (Color){40, 60, 100, 255} : (Color){20, 30, 50, 255}; + Color dotEdge = (ui->patchVCursor == r) ? SKYBLUE : (Color){80, 120, 180, 255}; + DrawRectangle((int)(x + padding), (int)contentY, (int)btnW, (int)(rowH - 2), dotBg); + DrawRectangleLines((int)(x + padding), (int)contentY, (int)btnW, (int)(rowH - 2), dotEdge); DRAW_TEXT("..", (int)(x + padding + 4), (int)(contentY + 2), fontSize, SKYBLUE); if (!ui->patchConfirmClear && IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && mouse.x >= x + padding && mouse.x <= x + padding + btnW && @@ -1119,10 +1158,10 @@ void uiPatchMenu(UIState *ui, float x, float y, float width, Synth *s) int d = r - backRows; char label[PATCH_NAME_LEN + 4]; snprintf(label, sizeof(label), "> %s", ui->patchSubDirs[d]); - DrawRectangle((int)(x + padding), (int)contentY, (int)btnW, (int)(rowH - 2), - (Color){20, 30, 50, 255}); - DrawRectangleLines((int)(x + padding), (int)contentY, (int)btnW, (int)(rowH - 2), - (Color){80, 120, 180, 255}); + Color dirBg = (ui->patchVCursor == r) ? (Color){40, 60, 100, 255} : (Color){20, 30, 50, 255}; + Color dirEdge = (ui->patchVCursor == r) ? SKYBLUE : (Color){80, 120, 180, 255}; + DrawRectangle((int)(x + padding), (int)contentY, (int)btnW, (int)(rowH - 2), dirBg); + DrawRectangleLines((int)(x + padding), (int)contentY, (int)btnW, (int)(rowH - 2), dirEdge); DRAW_TEXT(label, (int)(x + padding + 4), (int)(contentY + 2), fontSize, SKYBLUE); if (!ui->patchConfirmClear && IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && mouse.x >= x + padding && mouse.x <= x + padding + btnW && @@ -1190,6 +1229,7 @@ void uiPatchMenu(UIState *ui, float x, float y, float width, Synth *s) if (patchLoad(s, path)) { snprintf(ui->patchCurrentName, PATCH_NAME_LEN, "%s", ui->patchFiles[i]); snprintf(ui->patchSaveName, PATCH_NAME_LEN, "%s", ui->patchFiles[i]); + ui->patchVCursor = r; } } }