From 82294844ddbd5eee18f689ef1bc98af6fb1f14e3 Mon Sep 17 00:00:00 2001 From: Tyler Date: Wed, 12 Aug 2026 14:17:05 -0700 Subject: [PATCH] feat: hide control annotations by default, demote auto-crop notice to a toast MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Control events are zero-duration log markers about the run rather than signals on the air. A busy capture carries thousands, and they clutter the overlay without saying anything about what was transmitted, so the GUI now starts with that kind hidden; the per-kind checkbox brings it back. The headless --render path still enables every kind — an export should render what was asked for, not a GUI preference. The auto-crop notice was a full modal: it dimmed the window, took focus, and demanded a click before the user could look at the file they had just opened. Auto-crop is a helpful default, not a decision worth blocking on. It is now a bottom-right toast offering Undo / Dismiss, with a progress strip showing the remaining time so it doesn't just vanish mid-read, and it is out of UiModalOpen() so it no longer swallows keys or blocks the spectrogram underneath. The countdown only advances while the window is focused, so a crop applied during a background load is still readable when the user returns. The per-frame step is clamped: auto-crop fires on the frame right after the blocking STFT compute, and GetFrameTime() there reports the entire compute, which drained the whole 5 s budget at once and made the toast flash by in an instant. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01V8ZWfr5XZyyDttvkhJUgHN --- src/spectrogram.c | 9 ++++ src/spectrogram_types.h | 8 +++- src/ui.c | 91 +++++++++++++++++++++++++---------------- 3 files changed, 72 insertions(+), 36 deletions(-) diff --git a/src/spectrogram.c b/src/spectrogram.c index 6a40e92..54e3d25 100644 --- a/src/spectrogram.c +++ b/src/spectrogram.c @@ -147,6 +147,7 @@ static bool IsAppActive(void) if (app.sel.isDragging || app.sel.isTimeSelecting || app.sel.isFreqSelecting) return true; if (app.marker.dragging) return true; if (app.exportMessageTimer > 0.0f) return true; // notification countdown + if (app.autocropNoticeActive) return true; // toast countdown + progress strip return false; } @@ -526,6 +527,7 @@ void ApplyAutoCrop(void) timePart, srcPart); app.autocropNoticeActive = true; + app.autocropNoticeTimer = AUTOCROP_NOTICE_SECONDS; TraceLog(LOG_INFO, "Auto-crop: %s", app.autocropNoticeMsg); } @@ -968,6 +970,13 @@ int main(int argc, char* argv[]) app.hoverStackCount = 0; app.currentCollision = -1; for (int i = 0; i < MLNL_KIND_MAX; i++) app.annotationKindEnabled[i] = true; + // Control events are zero-duration log markers about the run, not signals on + // the air. On a busy capture there are thousands of them and they clutter + // the overlay without saying anything about what was transmitted, so they + // start hidden; the per-kind checkbox turns them back on. (Headless + // --render keeps every kind enabled — an export should show what was asked + // for, not a GUI default.) + app.annotationKindEnabled[MLNL_KIND_CONTROL] = false; app.showScope = true; app.dividerY = 0.6f; // Start with 60% spectro, 40% scope app.isDividing = false; diff --git a/src/spectrogram_types.h b/src/spectrogram_types.h index a83e657..9986245 100644 --- a/src/spectrogram_types.h +++ b/src/spectrogram_types.h @@ -56,6 +56,9 @@ typedef struct { // still reports the total collision count, it just stops adding bands. #define MAX_COLLISION_REGIONS 4096 +// How long the auto-crop toast stays up, in seconds of *focused* time. +#define AUTOCROP_NOTICE_SECONDS 5.0f + // How many overlapping annotation boxes the cursor-hit stack retains. Deeper // piles than this are counted but not listed individually (the tooltip says // "+N more"), which keeps a dense pile-up from covering the spectrogram. @@ -306,6 +309,7 @@ typedef struct { // user dismisses with "OK" (keep crop) or "Uncrop" (restore full view). bool autocropNoticeActive; char autocropNoticeMsg[256]; + float autocropNoticeTimer; // seconds left; counts down only while focused // Optional mLnL annotations parsed from the loaded WAV (empty if the file // doesn't carry the chunk). The annotations overlay has two surfaces: @@ -381,7 +385,9 @@ void ApplyAutoCrop(void); // is gated off while this is the case. Add new overlays here in one place. static inline bool UiModalOpen(void) { - return app.showFileBrowser || app.showAbout || app.autocropNoticeActive; + // The auto-crop notice is a toast, not a modal — it must not swallow keys + // or block interaction with the spectrogram underneath it. + return app.showFileBrowser || app.showAbout; } // Reset the box selection to the full signal (the "no selection" state). diff --git a/src/ui.c b/src/ui.c index 8bad4e4..aa6b9ae 100644 --- a/src/ui.c +++ b/src/ui.c @@ -826,6 +826,13 @@ void DrawAboutDialog(void) // [OK] — dismiss; keep the cropped view // Esc closes (= OK). Clicks outside the panel do nothing (avoids losing the // crop by missing a button by a few px). +// Auto-crop notice, as a bottom-right toast rather than a modal. +// +// Auto-crop is a helpful default, not a decision worth blocking on: a modal +// stole focus and demanded a click before the user could look at the file they +// had just opened. The toast states what happened, offers Undo, and expires on +// its own — but only counts down while the window is focused, so a crop applied +// during a background load is still there to read when the user comes back. void DrawAutocropNotice(void) { if (!app.autocropNoticeActive) return; @@ -833,52 +840,52 @@ void DrawAutocropNotice(void) int sw = GetScreenWidth(); int sh = GetScreenHeight(); - DrawRectangle(0, 0, sw, sh, Fade(BLACK, 0.55f)); + const char* title = "Auto-crop applied"; + float titleW = MeasureTextScaled(title, 13); + float msgW = MeasureTextScaled(app.autocropNoticeMsg, 11); + float btnW = 76 * scale, btnH = 24 * scale; - float pw = 460 * scale; - float ph = 170 * scale; - Rectangle panel = { (sw - pw) * 0.5f, (sh - ph) * 0.5f, pw, ph }; - DrawRectangleRec(panel, (Color){ 30, 30, 40, 255 }); - DrawRectangleLinesEx(panel, 2, (Color){ 160, 130, 200, 255 }); + float pw = fmaxf(fmaxf(titleW, msgW) + 28 * scale, btnW * 2 + 44 * scale); + float ph = 92 * scale; + float margin = 16 * scale; + Rectangle panel = { sw - pw - margin, sh - ph - margin, pw, ph }; - DrawTextScaled("Auto-crop applied", - panel.x + 20 * scale, panel.y + 16 * scale, 16, + DrawRectangleRec(panel, (Color){ 28, 28, 36, 240 }); + DrawRectangleLinesEx(panel, 1, (Color){ 150, 120, 190, 255 }); + + DrawTextScaled(title, panel.x + 12 * scale, panel.y + 10 * scale, 13, (Color){ 200, 170, 240, 255 }); + DrawTextScaled(app.autocropNoticeMsg, panel.x + 12 * scale, panel.y + 30 * scale, + 11, LIGHTGRAY); - // Body: word-wrap not needed for the short message ApplyAutoCrop builds. - DrawTextScaled(app.autocropNoticeMsg, - panel.x + 20 * scale, panel.y + 50 * scale, 13, LIGHTGRAY); + float btnY = panel.y + ph - btnH - 10 * scale; + Rectangle undoBtn = { panel.x + 12 * scale, btnY, btnW, btnH }; + Rectangle okBtn = { panel.x + pw - btnW - 12 * scale, btnY, btnW, btnH }; - float btnW = 110 * scale, btnH = 32 * scale; - float btnY = panel.y + ph - btnH - 16 * scale; - Rectangle uncropBtn = { panel.x + 20 * scale, btnY, btnW, btnH }; - Rectangle okBtn = { panel.x + pw - btnW - 20*scale, btnY, btnW, btnH }; + bool undoHover = CheckCollisionPointRec(GetMousePosition(), undoBtn); + bool okHover = CheckCollisionPointRec(GetMousePosition(), okBtn); - bool uncropHover = CheckCollisionPointRec(GetMousePosition(), uncropBtn); - bool okHover = CheckCollisionPointRec(GetMousePosition(), okBtn); - - DrawPanelBox(uncropBtn, - uncropHover ? (Color){ 100, 60, 60, 255 } : (Color){ 70, 40, 40, 255 }, + DrawPanelBox(undoBtn, + undoHover ? (Color){ 100, 60, 60, 255 } : (Color){ 70, 40, 40, 255 }, (Color){ 230, 160, 160, 255 }); - DrawTextScaled("Uncrop", - uncropBtn.x + btnW * 0.5f - MeasureTextScaled("Uncrop", 14) * 0.5f, - uncropBtn.y + 8 * scale, 14, WHITE); + DrawTextScaled("Undo", undoBtn.x + btnW * 0.5f - MeasureTextScaled("Undo", 12) * 0.5f, + undoBtn.y + 5 * scale, 12, WHITE); DrawPanelBox(okBtn, - okHover ? (Color){ 60, 90, 60, 255 } : (Color){ 40, 70, 40, 255 }, - (Color){ 160, 220, 160, 255 }); - DrawTextScaled("OK", - okBtn.x + btnW * 0.5f - MeasureTextScaled("OK", 14) * 0.5f, - okBtn.y + 8 * scale, 14, WHITE); + okHover ? (Color){ 60, 90, 60, 255 } : (Color){ 45, 60, 45, 255 }, + (Color){ 160, 200, 160, 255 }); + DrawTextScaled("Dismiss", okBtn.x + btnW * 0.5f - MeasureTextScaled("Dismiss", 12) * 0.5f, + okBtn.y + 5 * scale, 12, WHITE); - DrawTextScaled("Esc or Enter = OK", panel.x + pw - 156 * scale, - panel.y + ph - 14 * scale, 10, GRAY); + // Remaining-time strip along the bottom edge, so the auto-expiry is visible + // rather than the toast just vanishing mid-read. + float frac = app.autocropNoticeTimer / AUTOCROP_NOTICE_SECONDS; + if (frac < 0.0f) frac = 0.0f; + if (frac > 1.0f) frac = 1.0f; + DrawRectangle((int)panel.x, (int)(panel.y + ph - 2 * scale), + (int)(pw * frac), (int)(2 * scale), (Color){ 150, 120, 190, 200 }); - // Hit handling. Esc/Enter dismiss (keep crop). Uncrop restores full view. - if (IsKeyPressed(KEY_ESCAPE) || IsKeyPressed(KEY_ENTER)) { - app.autocropNoticeActive = false; - } - if (uncropHover && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { + if (undoHover && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { app.displayMaxFreqHz = 0.0f; app.view.start = 0.0f; app.view.end = 1.0f; app.view.freqStart = 0.0f; app.view.freqEnd = 1.0f; @@ -887,4 +894,18 @@ void DrawAutocropNotice(void) } else if (okHover && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { app.autocropNoticeActive = false; } + + // Count down only while focused: a toast that expired behind another window + // would be gone before it was ever seen. + // + // The step is clamped because auto-crop fires on the frame right after the + // blocking STFT compute, and GetFrameTime() on that frame reports the whole + // compute — seconds on a long capture. Unclamped, that single frame drained + // the entire 5 s budget and the toast flashed by in an instant. + if (IsWindowFocused()) { + float dt = GetFrameTime(); + if (dt > 0.1f) dt = 0.1f; // ignore load hitches / debugger stalls + app.autocropNoticeTimer -= dt; + if (app.autocropNoticeTimer <= 0.0f) app.autocropNoticeActive = false; + } }