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);
|
||||
float t0 = hasSel ? app.sel.timeStart : app.view.start;
|
||||
float t1 = hasSel ? app.sel.timeEnd : app.view.end;
|
||||
float f0 = hasSel ? app.sel.freqStart : app.view.freqStart;
|
||||
float f1 = hasSel ? app.sel.freqEnd : app.view.freqEnd;
|
||||
if (f1 < f0) { float t = f0; f0 = f1; f1 = t; }
|
||||
// The spectrum slice always spans the complete frequency range. Honoring a
|
||||
// partial frequency selection only crops the displayed bins and skews the
|
||||
// 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;
|
||||
float* power = (float*)malloc(maxBins * sizeof(float));
|
||||
|
||||
+64
-29
@@ -142,7 +142,7 @@ static Layout ComputeLayout(void)
|
||||
L.scale = GetUIScale();
|
||||
L.sidebarWidth = 320 * L.scale;
|
||||
L.labelHeight = 15 * L.scale;
|
||||
L.scrollbarHeight = 18 * L.scale;
|
||||
L.scrollbarHeight = 22 * L.scale;
|
||||
L.freqLabelWidth = 65 * L.scale;
|
||||
L.vScrollbarWidth = 18 * 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.showAnnotations = true;
|
||||
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
|
||||
// Optional CLI override of the resting overlay alpha (e.g. for a brighter
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Draw scrollbars
|
||||
// Draw scrollbars. Thumb geometry uses proper "track travel" math
|
||||
// (thumb travels over width-thumb, not the whole track) so a min-size
|
||||
// thumb stays inside the track and dragging maps 1:1 to the cursor at
|
||||
// any zoom. minThumb keeps the thumb grabbable when zoomed way in.
|
||||
Vector2 mouse = GetMousePosition();
|
||||
float minThumb = 28.0f * renderScale;
|
||||
Color trackColor = (Color){ 40, 40, 46, 255 };
|
||||
Color trackBorder = (Color){ 72, 72, 82, 255 };
|
||||
Color thumbColor = (Color){ 110, 114, 126, 255 };
|
||||
Color thumbHot = (Color){ 158, 164, 180, 255 };
|
||||
|
||||
// Horizontal scrollbar (time)
|
||||
DrawRectangleRec(hScrollbar, DARKGRAY);
|
||||
float hThumbWidth = (app.view.end - app.view.start) * hScrollbar.width;
|
||||
float hThumbX = hScrollbar.x + app.view.start * hScrollbar.width;
|
||||
if (hThumbWidth < 10) hThumbWidth = 10;
|
||||
DrawRectangle(hThumbX, hScrollbar.y, hThumbWidth, hScrollbar.height, GRAY);
|
||||
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)
|
||||
DrawRectangleRec(vScrollbar, DARKGRAY);
|
||||
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);
|
||||
// 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 dragging
|
||||
// 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 Vector2 dragStartPos;
|
||||
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;
|
||||
dragStartPos = GetMousePosition();
|
||||
dragStartPos = mouse;
|
||||
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;
|
||||
dragStartPos = GetMousePosition();
|
||||
dragStartPos = mouse;
|
||||
dragStartFreqViewStart = app.view.freqStart;
|
||||
}
|
||||
if (draggingH && IsMouseButtonDown(MOUSE_LEFT_BUTTON)) {
|
||||
float dx = (GetMousePosition().x - dragStartPos.x) / hScrollbar.width;
|
||||
float viewWidth = app.view.end - app.view.start;
|
||||
float dx = (hTravel > 0.0f) ? ((mouse.x - dragStartPos.x) / hTravel) * hDenom : 0.0f;
|
||||
app.view.start = dragStartViewStart + dx;
|
||||
app.view.end = app.view.start + viewWidth;
|
||||
if (app.view.start < 0) { app.view.start = 0; app.view.end = viewWidth; }
|
||||
if (app.view.end > 1) { app.view.end = 1; app.view.start = 1 - viewWidth; }
|
||||
app.view.end = app.view.start + hViewW;
|
||||
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 - hViewW; }
|
||||
app.visibleTextureValid = false;
|
||||
}
|
||||
if (draggingV && IsMouseButtonDown(MOUSE_LEFT_BUTTON)) {
|
||||
float dy = (GetMousePosition().y - dragStartPos.y) / vScrollbar.height;
|
||||
float freqWidth = app.view.freqEnd - app.view.freqStart;
|
||||
float dy = (vTravel > 0.0f) ? ((mouse.y - dragStartPos.y) / vTravel) * vDenom : 0.0f;
|
||||
app.view.freqStart = dragStartFreqViewStart - dy;
|
||||
app.view.freqEnd = app.view.freqStart + freqWidth;
|
||||
app.view.freqEnd = app.view.freqStart + vViewH;
|
||||
if (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) {
|
||||
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.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; }
|
||||
|
||||
// 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));
|
||||
// Timeline lane sits above the spectrogram. Drawn before the
|
||||
// overlays so its hover/selection state is set for the same frame.
|
||||
|
||||
Reference in New Issue
Block a user