feat: menubar + icon rail, minimap, Blender-style navigation

A pass over the whole interface, driven by using it on a 5.7-hour capture.

**Layout.** The 320px sidebar of labelled widgets is now a 46px icon rail —
one column of square buttons, sized so it costs the spectrogram as little
width as possible — plus a menubar for one-shot actions. Menu items are
defined by naming a keymap entry, so an item reuses that binding's action,
gate and shortcut label and the two can't drift; items grey out under
exactly the conditions that make the shortcut a no-op. Settings that need
more than an on/off (FFT size, colours/levels, annotation kinds) open as
popouts beside the rail rather than widening it. Every toggle has exactly
one home: nothing is reachable from both the rail and a menu.

Icons are drawn from raylib primitives rather than an atlas or font
glyphs — the bundled font has no symbol coverage, and vector shapes stay
crisp at any UI scale with no assets to ship.

**Navigation.** Left-drag now pans and Ctrl+drag box-selects, with Tab
swapping which is bare (Ctrl always means "the other one", so either mode
does both). Previously a bare left-drag did three different things
depending on invisible state, with no cursor feedback; the cursor now
reports the active gesture. Wheeling a scrollbar pans that axis, or zooms
it with Shift.

**Minimap.** Whole-file thumbnail in the top-right with the current view
drawn on it; click or drag to scrub, corner handle switches between two
sizes. Rendered once per size into a cached texture and rebuilt only when
its content changes — panning and zooming just move the rectangle drawn on
top. The reduction is strided (each thumbnail cell samples at most 8x8),
because reducing every segment x bin meant ~1 G reads per rebuild and a
visible hitch on every overlay toggle.

**Fixes found along the way:**
- The timeline lane mapped events across the whole file while the
  spectrogram above it showed a zoomed window, so the two only lined up at
  full zoom-out and an event's tick sat nowhere near its burst. It is also
  properly toggleable now: the old flag only grew an always-present lane.
- Clicks preferred the spectrogram over the minimap. Input handling runs
  ~900 lines before the draw pass that computed the minimap's rect, so a
  press there started a pan AND a scrub — two handlers writing app.view in
  one frame. Geometry queries that input depends on now live outside the
  draw pass.
- Repainting during the background fill re-ran the full synchrosqueeze
  every 0.5 s. Zoomed out that is ~0.5 G bin-visits with four trig calls
  each, twice a second, for minutes — while showing almost nothing new,
  since folded segments land in columns already drawn. The interval now
  scales with how much work a repaint actually costs.
- IsUserInteracting() sweeps 512 key codes and was called three times a
  frame; memoized per frame.
- Frequency labels were drawn at a fixed offset wider than their gutter and
  overflowed into the rail. They are measured and right-aligned now, with
  one format chosen per axis so the column doesn't mix "3k" with "2.3k".
- The horizontal scrollbar is pinned to the window bottom; it used to sit
  mid-layout competing with the scope's divider, and the scope covered it
  outright at larger window sizes.
- The scope starts hidden — the spectrogram is the primary view.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8ZWfr5XZyyDttvkhJUgHN
This commit is contained in:
2026-08-12 15:28:44 -07:00
parent 82294844dd
commit cce96659b1
7 changed files with 1499 additions and 401 deletions
+793 -344
View File
File diff suppressed because it is too large Load Diff