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 <noreply@anthropic.com>
This commit is contained in:
Anachronaut
2026-07-07 22:32:46 -04:00
parent ff59c7c66a
commit e40645f280
4 changed files with 71 additions and 96 deletions

View File

@@ -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.

View File

@@ -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];

View File

@@ -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
}

View File

@@ -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;
&& 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) {
curFi = fi;
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];
}
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;
}
}
}