refactor: group selection state into a Selection sub-struct + ClearSelection()

The box-selection state was 11 fields spread across three comment blocks
(time sel, freq sel, drag state). Consolidate them into one Selection
sub-struct (app.sel.*) and extract the 4-line 'clear to full range' block —
duplicated in 5 places — into a ClearSelection() helper.

Pure mechanical rename (anchored to app./spa-> so the ScopeView fields of the
same name are untouched) + behavior-identical dedup. Fully-settled render
verified pixel-identical (AE=0).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 09:15:50 -07:00
parent ab7a6bc71c
commit 8e4250ae63
5 changed files with 103 additions and 107 deletions
+4 -4
View File
@@ -182,8 +182,8 @@ void PlaySelectedRegion(void)
{
if (!app.loaded || !app.stftComputed) return;
int startSample = (int)(app.timeSelectionStart * app.signal.numSamples);
int endSample = (int)(app.timeSelectionEnd * app.signal.numSamples);
int startSample = (int)(app.sel.timeStart * app.signal.numSamples);
int endSample = (int)(app.sel.timeEnd * app.signal.numSamples);
int numSamples = endSample - startSample;
if (numSamples <= 0 || startSample < 0 || endSample > app.signal.numSamples) return;
@@ -191,8 +191,8 @@ void PlaySelectedRegion(void)
memcpy(regionSamples, app.signal.samples + startSample, numSamples * sizeof(float));
float maxFreq = (float)app.signal.sampleRate / 2.0f;
float freqLow = app.freqSelectionStart * maxFreq;
float freqHigh = app.freqSelectionEnd * maxFreq;
float freqLow = app.sel.freqStart * maxFreq;
float freqHigh = app.sel.freqEnd * maxFreq;
if (freqLow > 10.0f || freqHigh < maxFreq - 10.0f) {
TraceLog(LOG_INFO, "Applying bandpass filter: %.0f - %.0f Hz", freqLow, freqHigh);
ApplyBandpassFilter(regionSamples, numSamples, app.signal.sampleRate, freqLow, freqHigh);
+18 -18
View File
@@ -341,9 +341,9 @@ void DrawLabels(Rectangle bounds)
void DrawSelection(Rectangle bounds)
{
// Only draw if selection is not full range AND not currently dragging
bool hasSelection = (app.timeSelectionStart > 0.001f || app.timeSelectionEnd < 0.999f ||
app.freqSelectionStart > 0.001f || app.freqSelectionEnd < 0.999f);
if (!hasSelection || app.isTimeSelecting) return; // Don't draw overlay while dragging
bool hasSelection = (app.sel.timeStart > 0.001f || app.sel.timeEnd < 0.999f ||
app.sel.freqStart > 0.001f || app.sel.freqEnd < 0.999f);
if (!hasSelection || app.sel.isTimeSelecting) return; // Don't draw overlay while dragging
Color overlayColor = Fade(BLACK, 0.25f); // Lighter overlay
@@ -351,10 +351,10 @@ void DrawSelection(Rectangle bounds)
float viewWidth = app.viewEnd - app.viewStart;
float freqWidth = app.freqViewEnd - app.freqViewStart;
float selStartX = bounds.x + ((app.timeSelectionStart - app.viewStart) / viewWidth) * bounds.width;
float selEndX = bounds.x + ((app.timeSelectionEnd - app.viewStart) / viewWidth) * bounds.width;
float selStartY = bounds.y + bounds.height - ((app.freqSelectionEnd - app.freqViewStart) / freqWidth) * bounds.height;
float selEndY = bounds.y + bounds.height - ((app.freqSelectionStart - app.freqViewStart) / freqWidth) * bounds.height;
float selStartX = bounds.x + ((app.sel.timeStart - app.viewStart) / viewWidth) * bounds.width;
float selEndX = bounds.x + ((app.sel.timeEnd - app.viewStart) / viewWidth) * bounds.width;
float selStartY = bounds.y + bounds.height - ((app.sel.freqEnd - app.freqViewStart) / freqWidth) * bounds.height;
float selEndY = bounds.y + bounds.height - ((app.sel.freqStart - app.freqViewStart) / freqWidth) * bounds.height;
// Clamp to viewport bounds
selStartX = fmaxf(bounds.x, fminf(bounds.x + bounds.width, selStartX));
@@ -373,8 +373,8 @@ void DrawSelection(Rectangle bounds)
// Display selection stats inside viewport, clamped to fit
{
int startSample = (int)(app.timeSelectionStart * app.signal.numSamples);
int endSample = (int)(app.timeSelectionEnd * app.signal.numSamples);
int startSample = (int)(app.sel.timeStart * app.signal.numSamples);
int endSample = (int)(app.sel.timeEnd * app.signal.numSamples);
SignalStats stats = ComputeSignalStats(&app.signal, startSample, endSample);
if (stats.durationSec > 0.0f && app.signal.samples != NULL) {
@@ -439,17 +439,17 @@ void DrawSelection(Rectangle bounds)
void DrawSelectionDrag(Rectangle bounds)
{
// Draw bounding box while dragging (no overlay)
if ((!app.isTimeSelecting && !app.isFreqSelecting && !app.isDraggingSelection) ||
(app.isDraggingSelection && !IsMouseButtonDown(MOUSE_LEFT_BUTTON))) return;
if ((!app.sel.isTimeSelecting && !app.sel.isFreqSelecting && !app.sel.isDragging) ||
(app.sel.isDragging && !IsMouseButtonDown(MOUSE_LEFT_BUTTON))) return;
// Convert signal coordinates to viewport coordinates
float viewWidth = app.viewEnd - app.viewStart;
float freqWidth = app.freqViewEnd - app.freqViewStart;
float selStartX = bounds.x + ((app.timeSelectionStart - app.viewStart) / viewWidth) * bounds.width;
float selEndX = bounds.x + ((app.timeSelectionEnd - app.viewStart) / viewWidth) * bounds.width;
float selStartY = bounds.y + bounds.height - ((app.freqSelectionEnd - app.freqViewStart) / freqWidth) * bounds.height;
float selEndY = bounds.y + bounds.height - ((app.freqSelectionStart - app.freqViewStart) / freqWidth) * bounds.height;
float selStartX = bounds.x + ((app.sel.timeStart - app.viewStart) / viewWidth) * bounds.width;
float selEndX = bounds.x + ((app.sel.timeEnd - app.viewStart) / viewWidth) * bounds.width;
float selStartY = bounds.y + bounds.height - ((app.sel.freqEnd - app.freqViewStart) / freqWidth) * bounds.height;
float selEndY = bounds.y + bounds.height - ((app.sel.freqStart - app.freqViewStart) / freqWidth) * bounds.height;
// Clamp to viewport bounds
selStartX = fmaxf(bounds.x, fminf(bounds.x + bounds.width, selStartX));
@@ -467,8 +467,8 @@ void DrawSelectionDrag(Rectangle bounds)
// Display live stats while dragging (inside viewport, clamped to fit)
{
int startSample = (int)(app.timeSelectionStart * app.signal.numSamples);
int endSample = (int)(app.timeSelectionEnd * app.signal.numSamples);
int startSample = (int)(app.sel.timeStart * app.signal.numSamples);
int endSample = (int)(app.sel.timeEnd * app.signal.numSamples);
SignalStats stats = ComputeSignalStats(&app.signal, startSample, endSample);
if (stats.durationSec > 0.0f && app.signal.samples != NULL) {
@@ -538,7 +538,7 @@ void DrawPlayhead(Rectangle bounds)
{
if (!app.isPlaying || app.playheadT < 0.0f || app.playheadT > 1.0f) return;
float timePos = app.timeSelectionStart + app.playheadT * (app.timeSelectionEnd - app.timeSelectionStart);
float timePos = app.sel.timeStart + app.playheadT * (app.sel.timeEnd - app.sel.timeStart);
float viewWidth = app.viewEnd - app.viewStart;
float t = (timePos - app.viewStart) / viewWidth;
float x = bounds.x + t * bounds.width;
+52 -62
View File
@@ -120,8 +120,7 @@ void ResetForNewSignal(void)
// Reset view + selection to full range.
app.viewStart = 0.0f; app.viewEnd = 1.0f;
app.timeSelectionStart = 0.0f; app.timeSelectionEnd = 1.0f;
app.freqSelectionStart = 0.0f; app.freqSelectionEnd = 1.0f;
ClearSelection();
// Invalidate the cached visible texture.
if (app.visibleTexture.id != 0) UnloadTexture(app.visibleTexture);
@@ -223,8 +222,8 @@ int main(int argc, char* argv[])
TraceLog(LOG_WARNING, "Failed to load TTF font, using default bitmap font");
}
app.timeSelectionStart = 0.0f; app.timeSelectionEnd = 1.0f;
app.freqSelectionStart = 0.0f; app.freqSelectionEnd = 1.0f;
app.sel.timeStart = 0.0f; app.sel.timeEnd = 1.0f;
app.sel.freqStart = 0.0f; app.sel.freqEnd = 1.0f;
app.viewStart = 0.0f; app.viewEnd = 1.0f;
app.freqViewStart = 0.0f; app.freqViewEnd = 1.0f;
app.showGrid = true;
@@ -326,7 +325,7 @@ int main(int argc, char* argv[])
}
// Track playhead position manually
app.playheadElapsed += GetFrameTime();
float selectionDuration = (app.timeSelectionEnd - app.timeSelectionStart) * app.signal.duration;
float selectionDuration = (app.sel.timeEnd - app.sel.timeStart) * app.signal.duration;
if (selectionDuration > 0) {
app.playheadT = app.playheadElapsed / selectionDuration;
}
@@ -528,10 +527,7 @@ int main(int argc, char* argv[])
app.showFileBrowser = false;
} else {
// Clear selections instead of exiting
app.timeSelectionStart = 0.0f;
app.timeSelectionEnd = 1.0f;
app.freqSelectionStart = 0.0f;
app.freqSelectionEnd = 1.0f;
ClearSelection();
}
}
@@ -556,15 +552,12 @@ int main(int argc, char* argv[])
// Right-click clears selection
if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT) && CheckCollisionPointRec(mousePos, selBounds)) {
app.timeSelectionStart = 0.0f;
app.timeSelectionEnd = 1.0f;
app.freqSelectionStart = 0.0f;
app.freqSelectionEnd = 1.0f;
ClearSelection();
}
// Check if click is inside existing selection (for dragging)
bool hasSelection = (app.timeSelectionStart > 0.001f || app.timeSelectionEnd < 0.999f ||
app.freqSelectionStart > 0.001f || app.freqSelectionEnd < 0.999f);
bool hasSelection = (app.sel.timeStart > 0.001f || app.sel.timeEnd < 0.999f ||
app.sel.freqStart > 0.001f || app.sel.freqEnd < 0.999f);
bool clickInsideSelection = false;
bool hoverInsideSelection = false;
if (hasSelection && CheckCollisionPointRec(mousePos, selBounds)) {
@@ -574,8 +567,8 @@ int main(int argc, char* argv[])
float mouseTime = app.viewStart + ((mousePos.x - selBounds.x) / selBounds.width) * viewWidth;
float mouseFreq = app.freqViewStart + (1.0f - (mousePos.y - selBounds.y) / selBounds.height) * freqWidth;
if (mouseTime >= app.timeSelectionStart && mouseTime <= app.timeSelectionEnd &&
mouseFreq >= app.freqSelectionStart && mouseFreq <= app.freqSelectionEnd) {
if (mouseTime >= app.sel.timeStart && mouseTime <= app.sel.timeEnd &&
mouseFreq >= app.sel.freqStart && mouseFreq <= app.sel.freqEnd) {
hoverInsideSelection = true;
if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
clickInsideSelection = true;
@@ -584,7 +577,7 @@ int main(int argc, char* argv[])
}
// Set cursor based on context
if (app.isDraggingSelection) {
if (app.sel.isDragging) {
SetMouseCursor(MOUSE_CURSOR_RESIZE_ALL); // 4-way arrow while dragging
} else if (hoverInsideSelection) {
SetMouseCursor(MOUSE_CURSOR_POINTING_HAND); // Pointing hand on hover
@@ -597,7 +590,7 @@ int main(int argc, char* argv[])
// Set cursor to resize all when near divider
if (mouseNearDivider && !app.isDividing) {
SetMouseCursor(MOUSE_CURSOR_RESIZE_ALL);
} else if (app.isDraggingSelection) {
} else if (app.sel.isDragging) {
SetMouseCursor(MOUSE_CURSOR_RESIZE_ALL); // 4-way arrow while dragging
} else if (hoverInsideSelection) {
SetMouseCursor(MOUSE_CURSOR_POINTING_HAND); // Pointing hand on hover
@@ -607,85 +600,82 @@ int main(int argc, char* argv[])
if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
if (clickInsideSelection) {
// Start dragging existing selection
app.isDraggingSelection = true;
app.dragSelectionStartPos = mousePos;
app.dragSelectionTimeStart = app.timeSelectionStart;
app.dragSelectionFreqStart = app.freqSelectionStart;
app.sel.isDragging = true;
app.sel.dragStartPos = mousePos;
app.sel.dragTimeStart = app.sel.timeStart;
app.sel.dragFreqStart = app.sel.freqStart;
} else {
// Start new box selection
app.isTimeSelecting = true;
app.isFreqSelecting = true;
app.selectStartPos = mousePos;
app.sel.isTimeSelecting = true;
app.sel.isFreqSelecting = true;
app.sel.selectStartPos = mousePos;
// Convert screen position to signal coordinates (accounting for zoom)
float viewportT = (mousePos.x - selBounds.x) / selBounds.width;
float viewportF = 1.0f - (mousePos.y - selBounds.y) / selBounds.height;
app.timeSelectionStart = Clamp(app.viewStart + viewportT * (app.viewEnd - app.viewStart), 0.0f, 1.0f);
app.timeSelectionEnd = app.timeSelectionStart;
app.freqSelectionStart = Clamp(app.freqViewStart + viewportF * (app.freqViewEnd - app.freqViewStart), 0.0f, 1.0f);
app.freqSelectionEnd = app.freqSelectionStart;
app.sel.timeStart = Clamp(app.viewStart + viewportT * (app.viewEnd - app.viewStart), 0.0f, 1.0f);
app.sel.timeEnd = app.sel.timeStart;
app.sel.freqStart = Clamp(app.freqViewStart + viewportF * (app.freqViewEnd - app.freqViewStart), 0.0f, 1.0f);
app.sel.freqEnd = app.sel.freqStart;
}
}
// Dragging existing selection
if (app.isDraggingSelection && IsMouseButtonDown(MOUSE_LEFT_BUTTON)) {
if (app.sel.isDragging && IsMouseButtonDown(MOUSE_LEFT_BUTTON)) {
float viewWidth = app.viewEnd - app.viewStart;
float freqWidth = app.freqViewEnd - app.freqViewStart;
float dx = (mousePos.x - app.dragSelectionStartPos.x) / selBounds.width;
float dy = (mousePos.y - app.dragSelectionStartPos.y) / selBounds.height;
float dx = (mousePos.x - app.sel.dragStartPos.x) / selBounds.width;
float dy = (mousePos.y - app.sel.dragStartPos.y) / selBounds.height;
float timeShift = dx * viewWidth;
float freqShift = -dy * freqWidth; // Y is inverted
float timeWidth = app.timeSelectionEnd - app.timeSelectionStart;
float freqHeight = app.freqSelectionEnd - app.freqSelectionStart;
float timeWidth = app.sel.timeEnd - app.sel.timeStart;
float freqHeight = app.sel.freqEnd - app.sel.freqStart;
app.timeSelectionStart = Clamp(app.dragSelectionTimeStart + timeShift, 0.0f, 1.0f - timeWidth);
app.timeSelectionEnd = app.timeSelectionStart + timeWidth;
app.freqSelectionStart = Clamp(app.dragSelectionFreqStart + freqShift, 0.0f, 1.0f - freqHeight);
app.freqSelectionEnd = app.freqSelectionStart + freqHeight;
app.sel.timeStart = Clamp(app.sel.dragTimeStart + timeShift, 0.0f, 1.0f - timeWidth);
app.sel.timeEnd = app.sel.timeStart + timeWidth;
app.sel.freqStart = Clamp(app.sel.dragFreqStart + freqShift, 0.0f, 1.0f - freqHeight);
app.sel.freqEnd = app.sel.freqStart + freqHeight;
}
// Creating new box selection
if ((app.isTimeSelecting || app.isFreqSelecting) && IsMouseButtonDown(MOUSE_LEFT_BUTTON)) {
if ((app.sel.isTimeSelecting || app.sel.isFreqSelecting) && IsMouseButtonDown(MOUSE_LEFT_BUTTON)) {
float viewportT = (mousePos.x - selBounds.x) / selBounds.width;
float viewportF = 1.0f - (mousePos.y - selBounds.y) / selBounds.height;
app.timeSelectionEnd = Clamp(app.viewStart + viewportT * (app.viewEnd - app.viewStart), 0.0f, 1.0f);
app.freqSelectionEnd = Clamp(app.freqViewStart + viewportF * (app.freqViewEnd - app.freqViewStart), 0.0f, 1.0f);
app.sel.timeEnd = Clamp(app.viewStart + viewportT * (app.viewEnd - app.viewStart), 0.0f, 1.0f);
app.sel.freqEnd = Clamp(app.freqViewStart + viewportF * (app.freqViewEnd - app.freqViewStart), 0.0f, 1.0f);
}
if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) {
if (app.isDraggingSelection) {
app.isDraggingSelection = false;
} else if (app.isTimeSelecting || app.isFreqSelecting) {
if (app.sel.isDragging) {
app.sel.isDragging = false;
} else if (app.sel.isTimeSelecting || app.sel.isFreqSelecting) {
// Check if drag was large enough (minimum 5 pixels)
float dx = mousePos.x - app.selectStartPos.x;
float dy = mousePos.y - app.selectStartPos.y;
float dx = mousePos.x - app.sel.selectStartPos.x;
float dy = mousePos.y - app.sel.selectStartPos.y;
float dragDist = sqrtf(dx * dx + dy * dy);
if (dragDist > 5.0f) {
// Normalize so start < end
if (app.timeSelectionEnd < app.timeSelectionStart) {
float tmp = app.timeSelectionStart;
app.timeSelectionStart = app.timeSelectionEnd;
app.timeSelectionEnd = tmp;
if (app.sel.timeEnd < app.sel.timeStart) {
float tmp = app.sel.timeStart;
app.sel.timeStart = app.sel.timeEnd;
app.sel.timeEnd = tmp;
}
if (app.freqSelectionEnd < app.freqSelectionStart) {
float tmp = app.freqSelectionStart;
app.freqSelectionStart = app.freqSelectionEnd;
app.freqSelectionEnd = tmp;
if (app.sel.freqEnd < app.sel.freqStart) {
float tmp = app.sel.freqStart;
app.sel.freqStart = app.sel.freqEnd;
app.sel.freqEnd = tmp;
}
} else {
// Drag too small - revert to full range
app.timeSelectionStart = 0.0f;
app.timeSelectionEnd = 1.0f;
app.freqSelectionStart = 0.0f;
app.freqSelectionEnd = 1.0f;
ClearSelection();
}
app.isTimeSelecting = false;
app.isFreqSelecting = false;
app.sel.isTimeSelecting = false;
app.sel.isFreqSelecting = false;
}
}
}
@@ -982,7 +972,7 @@ int main(int argc, char* argv[])
app.scopeView.data.sampleRate = app.signal.sampleRate;
// Show playhead if playing
if (app.isPlaying) {
DrawScopeView(&app.scopeView, app.timeSelectionStart + app.playheadT * (app.timeSelectionEnd - app.timeSelectionStart));
DrawScopeView(&app.scopeView, app.sel.timeStart + app.playheadT * (app.sel.timeEnd - app.sel.timeStart));
} else {
DrawScopeView(&app.scopeView, -1.0f);
}
+24 -15
View File
@@ -91,6 +91,21 @@ typedef struct {
int nextOrder;
} FFTSizeCache;
// The time+frequency box selection and its drag/move interaction state.
// All coordinates are 0-1 normalized. A "box-select" drags out a new box;
// a "move" drags an existing box around.
typedef struct {
float timeStart, timeEnd; // selected time span
float freqStart, freqEnd; // selected frequency span
bool isTimeSelecting; // dragging out a new time span
bool isFreqSelecting; // dragging out a new frequency span
Vector2 selectStartPos; // mouse pos when a box-select began (min-drag check)
bool isDragging; // moving an existing selection box
Vector2 dragStartPos; // mouse pos when the move began
float dragTimeStart; // selection start time when the move began
float dragFreqStart; // selection freq start when the move began
} Selection;
typedef struct {
AudioSignal signal;
StftResult stft;
@@ -103,27 +118,14 @@ typedef struct {
float playheadT; // 0-1 normalized position in selection
float playheadElapsed; // Elapsed seconds since play started
// Time selection (0-1 normalized)
float timeSelectionStart;
float timeSelectionEnd;
bool isTimeSelecting;
// Frequency selection (0-1 normalized)
float freqSelectionStart;
float freqSelectionEnd;
bool isFreqSelecting;
// Time + frequency box selection and its drag/move interaction state.
Selection sel;
// Export settings
float exportScale;
char exportDir[4096];
char exportMessage[256];
Vector2 selectStartPos; // For minimum drag distance check
bool isDraggingSelection; // Dragging existing selection box
Vector2 dragSelectionStartPos; // Mouse position when started dragging selection
float dragSelectionTimeStart; // Selection start time when dragging
float dragSelectionFreqStart; // Selection freq start when dragging
// Viewport/zoom controls
float viewStart; // 0-1, start of visible time region
float viewEnd; // 0-1, end of visible time region
@@ -233,6 +235,13 @@ static inline bool UiModalOpen(void)
return app.showFileBrowser || app.showAbout;
}
// Reset the box selection to the full signal (the "no selection" state).
static inline void ClearSelection(void)
{
app.sel.timeStart = 0.0f; app.sel.timeEnd = 1.0f;
app.sel.freqStart = 0.0f; app.sel.freqEnd = 1.0f;
}
// ============================================================================
// Keymap — single source of truth for global key bindings.
// The dispatcher (DispatchKeymap in spectrogram.c) runs every entry whose
+5 -8
View File
@@ -266,10 +266,10 @@ void ExportPNG(const SpectrogramApp* spa, const char* dirPath)
int imgH = spa->spectrogramImage.height;
// Selection region in image-pixel coordinates
int selX0 = (int)(spa->timeSelectionStart * imgW);
int selX1 = (int)(spa->timeSelectionEnd * imgW);
int selY0 = (int)((1.0f - spa->freqSelectionEnd) * imgH);
int selY1 = (int)((1.0f - spa->freqSelectionStart) * imgH);
int selX0 = (int)(spa->sel.timeStart * imgW);
int selX1 = (int)(spa->sel.timeEnd * imgW);
int selY0 = (int)((1.0f - spa->sel.freqEnd) * imgH);
int selY1 = (int)((1.0f - spa->sel.freqStart) * imgH);
// Clamp to image bounds
selX0 = Clamp(selX0, 0, imgW);
@@ -468,10 +468,7 @@ void DrawSidebar(void)
// Clear selection (identical to the Esc key)
Rectangle clearButton = { x, y, sidebarWidth - 10 * scale, 25 * scale };
if (Clicked(clearButton)) {
app.timeSelectionStart = 0.0f;
app.timeSelectionEnd = 1.0f;
app.freqSelectionStart = 0.0f;
app.freqSelectionEnd = 1.0f;
ClearSelection();
}
DrawPanelBox(clearButton, (Color){ 80, 50, 50, 255 }, RED);
DrawTextScaled("Clear Selection (ESC)", clearButton.x + 10 * scale, clearButton.y + 6 * scale, 14, WHITE);