feat: scrollbar thumb travel math + UI tweaks
- Scrollbars: proper track-travel geometry so a min-size thumb stays inside the track and dragging maps 1:1 to the cursor at any zoom; clicking the empty track jumps the view to re-center the thumb under the cursor. Rounded thumbs with hover/active highlight; taller track. - Spectrum slice (PSD) always spans the full frequency range; ignoring the selection's freq bounds avoids cropping bins and skewing the auto-ranged dB axis and peak pick. Selection rect still drawn normally. - Default annotation overlay opacity 0.06 -> 0.24 (quiet but legible). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+7
-3
@@ -714,9 +714,13 @@ void DrawSpectrumPanel(Rectangle bounds)
|
|||||||
app.sel.freqStart > 0.001f || app.sel.freqEnd < 0.999f);
|
app.sel.freqStart > 0.001f || app.sel.freqEnd < 0.999f);
|
||||||
float t0 = hasSel ? app.sel.timeStart : app.view.start;
|
float t0 = hasSel ? app.sel.timeStart : app.view.start;
|
||||||
float t1 = hasSel ? app.sel.timeEnd : app.view.end;
|
float t1 = hasSel ? app.sel.timeEnd : app.view.end;
|
||||||
float f0 = hasSel ? app.sel.freqStart : app.view.freqStart;
|
// The spectrum slice always spans the complete frequency range. Honoring a
|
||||||
float f1 = hasSel ? app.sel.freqEnd : app.view.freqEnd;
|
// partial frequency selection only crops the displayed bins and skews the
|
||||||
if (f1 < f0) { float t = f0; f0 = f1; f1 = t; }
|
// auto-ranged dB axis + peak pick (inaccurate bins), so we ignore the
|
||||||
|
// selection's freq bounds here. The selection rectangle is still drawn
|
||||||
|
// normally by DrawSelection(); we just don't filter the slice by frequency.
|
||||||
|
float f0 = 0.0f;
|
||||||
|
float f1 = 1.0f;
|
||||||
|
|
||||||
int maxBins = FFT_SIZE_MAX / 2 + 1;
|
int maxBins = FFT_SIZE_MAX / 2 + 1;
|
||||||
float* power = (float*)malloc(maxBins * sizeof(float));
|
float* power = (float*)malloc(maxBins * sizeof(float));
|
||||||
|
|||||||
+67
-32
@@ -142,7 +142,7 @@ static Layout ComputeLayout(void)
|
|||||||
L.scale = GetUIScale();
|
L.scale = GetUIScale();
|
||||||
L.sidebarWidth = 320 * L.scale;
|
L.sidebarWidth = 320 * L.scale;
|
||||||
L.labelHeight = 15 * L.scale;
|
L.labelHeight = 15 * L.scale;
|
||||||
L.scrollbarHeight = 18 * L.scale;
|
L.scrollbarHeight = 22 * L.scale;
|
||||||
L.freqLabelWidth = 65 * L.scale;
|
L.freqLabelWidth = 65 * L.scale;
|
||||||
L.vScrollbarWidth = 18 * L.scale;
|
L.vScrollbarWidth = 18 * L.scale;
|
||||||
L.topMargin = 50 * L.scale;
|
L.topMargin = 50 * L.scale;
|
||||||
@@ -827,7 +827,7 @@ int main(int argc, char* argv[])
|
|||||||
app.displayMaxFreqHz = 0.0f; // 0 = no crop; user sets via sidebar slider
|
app.displayMaxFreqHz = 0.0f; // 0 = no crop; user sets via sidebar slider
|
||||||
app.showAnnotations = true;
|
app.showAnnotations = true;
|
||||||
app.annotationsExpanded = false;
|
app.annotationsExpanded = false;
|
||||||
app.annotationOpacityBase = 0.06f; // whisper-faint by default — signal wins
|
app.annotationOpacityBase = 0.24f; // quiet but legible by default — signal still wins
|
||||||
app.annotationOpacityHover = 0.65f; // pop on hover / selection
|
app.annotationOpacityHover = 0.65f; // pop on hover / selection
|
||||||
// Optional CLI override of the resting overlay alpha (e.g. for a brighter
|
// Optional CLI override of the resting overlay alpha (e.g. for a brighter
|
||||||
// GUI default). The headless render path sets its own default separately.
|
// GUI default). The headless render path sets its own default separately.
|
||||||
@@ -1584,57 +1584,80 @@ int main(int argc, char* argv[])
|
|||||||
viewBounds, (Vector2){ 0, 0 }, 0.0f, WHITE);
|
viewBounds, (Vector2){ 0, 0 }, 0.0f, WHITE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw scrollbars
|
// Draw scrollbars. Thumb geometry uses proper "track travel" math
|
||||||
// Horizontal scrollbar (time)
|
// (thumb travels over width-thumb, not the whole track) so a min-size
|
||||||
DrawRectangleRec(hScrollbar, DARKGRAY);
|
// thumb stays inside the track and dragging maps 1:1 to the cursor at
|
||||||
float hThumbWidth = (app.view.end - app.view.start) * hScrollbar.width;
|
// any zoom. minThumb keeps the thumb grabbable when zoomed way in.
|
||||||
float hThumbX = hScrollbar.x + app.view.start * hScrollbar.width;
|
Vector2 mouse = GetMousePosition();
|
||||||
if (hThumbWidth < 10) hThumbWidth = 10;
|
float minThumb = 28.0f * renderScale;
|
||||||
DrawRectangle(hThumbX, hScrollbar.y, hThumbWidth, hScrollbar.height, GRAY);
|
Color trackColor = (Color){ 40, 40, 46, 255 };
|
||||||
|
Color trackBorder = (Color){ 72, 72, 82, 255 };
|
||||||
// Vertical scrollbar (frequency)
|
Color thumbColor = (Color){ 110, 114, 126, 255 };
|
||||||
DrawRectangleRec(vScrollbar, DARKGRAY);
|
Color thumbHot = (Color){ 158, 164, 180, 255 };
|
||||||
float vThumbHeight = (app.view.freqEnd - app.view.freqStart) * vScrollbar.height;
|
|
||||||
float vThumbY = vScrollbar.y + (1.0f - app.view.freqEnd) * vScrollbar.height;
|
|
||||||
if (vThumbHeight < 10) vThumbHeight = 10;
|
|
||||||
DrawRectangle(vScrollbar.x, vThumbY, vScrollbar.width, vThumbHeight, GRAY);
|
|
||||||
|
|
||||||
// Handle scrollbar dragging
|
// Horizontal scrollbar (time)
|
||||||
|
float hViewW = app.view.end - app.view.start;
|
||||||
|
float hThumbW = fmaxf(hViewW * hScrollbar.width, minThumb);
|
||||||
|
float hTravel = hScrollbar.width - hThumbW;
|
||||||
|
float hDenom = 1.0f - hViewW;
|
||||||
|
float hThumbX = hScrollbar.x + (hDenom > 1e-6f ? (app.view.start / hDenom) * hTravel : 0.0f);
|
||||||
|
Rectangle hThumb = { hThumbX, hScrollbar.y, hThumbW, hScrollbar.height };
|
||||||
|
|
||||||
|
// Vertical scrollbar (frequency; axis flipped so freqEnd=1 is at top)
|
||||||
|
float vViewH = app.view.freqEnd - app.view.freqStart;
|
||||||
|
float vThumbH = fmaxf(vViewH * vScrollbar.height, minThumb);
|
||||||
|
float vTravel = vScrollbar.height - vThumbH;
|
||||||
|
float vDenom = 1.0f - vViewH;
|
||||||
|
float vThumbY = vScrollbar.y + (vDenom > 1e-6f ? ((1.0f - app.view.freqEnd) / vDenom) * vTravel : 0.0f);
|
||||||
|
Rectangle vThumb = { vScrollbar.x, vThumbY, vScrollbar.width, vThumbH };
|
||||||
|
|
||||||
|
// Handle scrollbar interaction. A press on the empty track jumps the
|
||||||
|
// view so the thumb re-centers under the cursor, then drags from there.
|
||||||
static bool draggingH = false, draggingV = false;
|
static bool draggingH = false, draggingV = false;
|
||||||
static Vector2 dragStartPos;
|
static Vector2 dragStartPos;
|
||||||
static float dragStartViewStart, dragStartFreqViewStart;
|
static float dragStartViewStart, dragStartFreqViewStart;
|
||||||
|
|
||||||
if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && CheckCollisionPointRec(GetMousePosition(), hScrollbar)) {
|
if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && CheckCollisionPointRec(mouse, hScrollbar)) {
|
||||||
|
if (!CheckCollisionPointRec(mouse, hThumb) && hTravel > 0.0f) {
|
||||||
|
float frac = Clamp((mouse.x - hScrollbar.x - hThumbW * 0.5f) / hTravel, 0.0f, 1.0f);
|
||||||
|
app.view.start = frac * hDenom;
|
||||||
|
app.view.end = app.view.start + hViewW;
|
||||||
|
app.visibleTextureValid = false;
|
||||||
|
}
|
||||||
draggingH = true;
|
draggingH = true;
|
||||||
dragStartPos = GetMousePosition();
|
dragStartPos = mouse;
|
||||||
dragStartViewStart = app.view.start;
|
dragStartViewStart = app.view.start;
|
||||||
}
|
}
|
||||||
if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && CheckCollisionPointRec(GetMousePosition(), vScrollbar)) {
|
if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && CheckCollisionPointRec(mouse, vScrollbar)) {
|
||||||
|
if (!CheckCollisionPointRec(mouse, vThumb) && vTravel > 0.0f) {
|
||||||
|
float frac = Clamp((mouse.y - vScrollbar.y - vThumbH * 0.5f) / vTravel, 0.0f, 1.0f);
|
||||||
|
app.view.freqEnd = 1.0f - frac * vDenom;
|
||||||
|
app.view.freqStart = app.view.freqEnd - vViewH;
|
||||||
|
app.visibleTextureValid = false;
|
||||||
|
}
|
||||||
draggingV = true;
|
draggingV = true;
|
||||||
dragStartPos = GetMousePosition();
|
dragStartPos = mouse;
|
||||||
dragStartFreqViewStart = app.view.freqStart;
|
dragStartFreqViewStart = app.view.freqStart;
|
||||||
}
|
}
|
||||||
if (draggingH && IsMouseButtonDown(MOUSE_LEFT_BUTTON)) {
|
if (draggingH && IsMouseButtonDown(MOUSE_LEFT_BUTTON)) {
|
||||||
float dx = (GetMousePosition().x - dragStartPos.x) / hScrollbar.width;
|
float dx = (hTravel > 0.0f) ? ((mouse.x - dragStartPos.x) / hTravel) * hDenom : 0.0f;
|
||||||
float viewWidth = app.view.end - app.view.start;
|
|
||||||
app.view.start = dragStartViewStart + dx;
|
app.view.start = dragStartViewStart + dx;
|
||||||
app.view.end = app.view.start + viewWidth;
|
app.view.end = app.view.start + hViewW;
|
||||||
if (app.view.start < 0) { app.view.start = 0; app.view.end = viewWidth; }
|
if (app.view.start < 0) { app.view.start = 0; app.view.end = hViewW; }
|
||||||
if (app.view.end > 1) { app.view.end = 1; app.view.start = 1 - viewWidth; }
|
if (app.view.end > 1) { app.view.end = 1; app.view.start = 1 - hViewW; }
|
||||||
app.visibleTextureValid = false;
|
app.visibleTextureValid = false;
|
||||||
}
|
}
|
||||||
if (draggingV && IsMouseButtonDown(MOUSE_LEFT_BUTTON)) {
|
if (draggingV && IsMouseButtonDown(MOUSE_LEFT_BUTTON)) {
|
||||||
float dy = (GetMousePosition().y - dragStartPos.y) / vScrollbar.height;
|
float dy = (vTravel > 0.0f) ? ((mouse.y - dragStartPos.y) / vTravel) * vDenom : 0.0f;
|
||||||
float freqWidth = app.view.freqEnd - app.view.freqStart;
|
|
||||||
app.view.freqStart = dragStartFreqViewStart - dy;
|
app.view.freqStart = dragStartFreqViewStart - dy;
|
||||||
app.view.freqEnd = app.view.freqStart + freqWidth;
|
app.view.freqEnd = app.view.freqStart + vViewH;
|
||||||
if (app.view.freqStart < 0) {
|
if (app.view.freqStart < 0) {
|
||||||
app.view.freqStart = 0;
|
app.view.freqStart = 0;
|
||||||
app.view.freqEnd = fminf(freqWidth, 1.0f);
|
app.view.freqEnd = fminf(vViewH, 1.0f);
|
||||||
}
|
}
|
||||||
if (app.view.freqEnd > 1) {
|
if (app.view.freqEnd > 1) {
|
||||||
app.view.freqEnd = 1;
|
app.view.freqEnd = 1;
|
||||||
app.view.freqStart = fmaxf(1.0f - freqWidth, 0.0f);
|
app.view.freqStart = fmaxf(1.0f - vViewH, 0.0f);
|
||||||
}
|
}
|
||||||
if (app.view.freqStart < 0) app.view.freqStart = 0;
|
if (app.view.freqStart < 0) app.view.freqStart = 0;
|
||||||
if (app.view.freqEnd > 1) app.view.freqEnd = 1;
|
if (app.view.freqEnd > 1) app.view.freqEnd = 1;
|
||||||
@@ -1642,6 +1665,18 @@ int main(int argc, char* argv[])
|
|||||||
}
|
}
|
||||||
if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { draggingH = false; draggingV = false; }
|
if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { draggingH = false; draggingV = false; }
|
||||||
|
|
||||||
|
// Draw tracks + thumbs after interaction so hover/active state and the
|
||||||
|
// jumped position are reflected this frame.
|
||||||
|
DrawRectangleRec(hScrollbar, trackColor);
|
||||||
|
DrawRectangleLinesEx(hScrollbar, 1.0f, trackBorder);
|
||||||
|
DrawRectangleRounded(hThumb, 0.5f, 4,
|
||||||
|
(draggingH || CheckCollisionPointRec(mouse, hThumb)) ? thumbHot : thumbColor);
|
||||||
|
|
||||||
|
DrawRectangleRec(vScrollbar, trackColor);
|
||||||
|
DrawRectangleLinesEx(vScrollbar, 1.0f, trackBorder);
|
||||||
|
DrawRectangleRounded(vThumb, 0.5f, 4,
|
||||||
|
(draggingV || CheckCollisionPointRec(mouse, vThumb)) ? thumbHot : thumbColor);
|
||||||
|
|
||||||
if (app.showGrid) DrawSpectrogramGrid(viewBounds, 10, 8, Fade(GRAY, 0.3f));
|
if (app.showGrid) DrawSpectrogramGrid(viewBounds, 10, 8, Fade(GRAY, 0.3f));
|
||||||
// Timeline lane sits above the spectrogram. Drawn before the
|
// Timeline lane sits above the spectrogram. Drawn before the
|
||||||
// overlays so its hover/selection state is set for the same frame.
|
// overlays so its hover/selection state is set for the same frame.
|
||||||
|
|||||||
Reference in New Issue
Block a user