// AUDIO_HAXOR — WALKTHROUGH

Tutorial index Docs hub
Progress
11 / 19

11.Tray popover & menu bar

A separate WebView living in the macOS menu bar / Windows system tray / Linux panel. Track info, mini-waveform with playhead + sample-loop region overlay, scrubber drag, shift+drag paint-a-loop, volume, speed, transport, and full theme + color scheme sync from the main window.

Tray popover showing now-playing title, scrubber, transport, volume, speed
assets/tray.png · Tray popover

Open the popover

Click the AUDIO_HAXOR icon in your menu bar (macOS) or system tray (Windows/Linux). On macOS this is a dedicated NSStatusItem managed by Tauri. The popover is a standalone WebView loaded from frontend/tray-popover.html and driven by frontend/js/tray-popover.js, completely separate from the main window process.

Right-clicking the tray icon opens the native OS menu with seven actions: Show AUDIO_HAXOR, Scan All, Stop All, Previous Track, Play / Pause, Next Track, and Quit (IDs tray_show, tray_scan_all, tray_stop_all, tray_prev, tray_play_pause, tray_next, tray_quit).

HUD layout

From top to bottom (IDs defined in frontend/tray-popover.html):

  • Title#trayPopoverTitle. Two-line clamp with ellipsis.
  • Subtitle#subtitle. Three-line clamp. Cleared when redundant with the title (basename-only path matching the displayed name).
  • Idle hint#idleHint. Shows when no track is playing (falls back to the tray.popover_idle i18n string).
  • Scrubber bar#trackBar hosting the mini-waveform canvas (#trayWaveformCanvas), a translucent progress fill #fill, a vertical playhead #trackThumb spanning the full 28 px canvas height, and an optional sample-loop region overlay with yellow brace markers matching the main-window meta waveform.
  • Elapsed / total#elapsed / #total. Orbitron font, M:SS format.
  • Volume row — label #trayVolLabel, slider #trayVol (0–100), percentage #trayVolPct.
  • Speed row — label #traySpeedLabel, select #traySpeed (0.25, 0.5, 0.75, 1, 1.25, 1.5, 2).
  • Transport#btnPrev ◀, #btnPlay ▶/⏸, #btnNext ▶▶.
  • Build info#trayBuildMeta, version string populated from get_build_info.
  • CRT wash.crt-wash — the signature scanline overlay gets its own copy here so the popover looks consistent with the main window.

State sync from the main window

Whenever playback changes, the main window calls update_tray_now_playing with a TrayNowPlayingPayload (defined in src-tauri/src/tray_menu.rs). The payload carries:

  • title_bar, tooltip, idle.
  • popover_title, popover_subtitle, popover_idle_label.
  • elapsed_sec, total_sec, popover_playing.
  • playback_speed, volume_pct.
  • waveform_peaks — flat [max0, min0, max1, min1, …] array sent once per track load; cached in _trayWaveformPeaks and only re-rendered when the signature actually changes.
  • loop_region_enabled, loop_region_start_sec, loop_region_end_sec — sample-loop region overlay state.
  • ui_theme"light" or "dark".
  • appearance — a map of CSS scheme variables (every key in SCHEME_VAR_KEYS) so the popover recolors to match your active color scheme instantly.

Rust relays the payload via emit_to("tray-popover", "tray-popover-state", emit); the JS side's applyState() normalizes snake/camel keys, updates every element, and re-bases the animation model. Theme changes emit tray-popover-ui-theme separately. SMB / network-share syscalls run in a separate task so they never block tray_popover_action; the Rust-side now-playing cache is mutated synchronously inside the action handler so the next state push reflects the latest seek/volume immediately.

Scrubber drag & shift-paint sample loop

The scrubber is a pointer-driven seek control:

  1. pointerdown on #trackBar captures the pointer and sets _dragging = true.
  2. pointermove updates _dragFrac from the mouse X coordinate.
  3. pointerup/pointercancel sends tray_popover_action { action: "seek:" } (4 decimal places, e.g. seek:0.3425) and re-bases the animation model.

Shift+drag on the trackbar paints a new sample-loop region — release fires tray_popover_action { action: "loop_region_paint::" } with 4-decimal-place seconds, and the yellow brace markers appear over the canvas immediately. Shift+click a single point with no drag sends loop_region_disable to clear the region. The same loop region honored by the main-window player drives the tray overlay, so painting from the tray loops the running track without raising the main window.

The animation loop in animationTick runs at 60 fps while not idle and linearly interpolates between host state pushes using performance.now(), so the playhead moves smoothly even though the host only pushes state every ~500 ms.

Volume & speed changes

Both controls emit the same tray_popover_action IPC but with different action strings:

  • Volume slider inputaction: "volume:" (integer 0–100). Updates #trayVolPct text immediately, then fires the IPC.
  • Speed select changeaction: "speed:" (float, 0.25–2.0).

Transport & menu IDs

The popover's transport buttons emit the same menu-action IDs as the native menu bar and the tray context menu:

  • #btnPrevsend('prev_track') → main window prevTrack({ respectAutoplaySource: true })
  • #btnPlaysend('play_pause') → main window toggleAudioPlayback()
  • #btnNextsend('next_track') → main window nextTrack({ respectAutoplaySource: true })

The respectAutoplaySource flag means prev/next from the tray follow whichever autoplay source you chose in Settings (samples table vs player history) — unlike the floating-player buttons which always walk the history list.

Idle vs playing state

When idle === true, a .idle class is added to #shell. CSS then:

  • Reduces opacity on progress / transport / volume / speed.
  • Shows the idle hint text.
  • Disables interaction on the transport buttons.

Theme & color scheme sync

When you toggle theme or change color scheme in the main window, applyTrayDocumentTheme(theme) sets document.documentElement.data-theme on the popover, which triggers the full cascade of light/dark CSS rules. The appearance map in the payload pushes per-variable overrides so custom color schemes also carry over — your tray popover always matches the main window, including mid-session scheme swaps.

Window sizing

syncWindowSize() measures the rendered #shell content plus padding and calls tray_popover_resize to update the popover's window dimensions. The body is allowed to shrink-wrap to its actual rendered shell — no min-height padding, no excess gutters — so the macOS NSPanel stays tight against the content. Bounds are clamped 240 – 520 px wide and 280 – 800 px tall on the Rust side. A ResizeObserver re-syncs on font load, text changes, locale swaps, and waveform-peaks delivery.

macOS Escape-key handling

The macOS NSPanel rarely becomes first responder, so a JS keydown for Escape never fires inside the popover. src-tauri/src/tray_popover_escape_macos.rs installs a local NSEvent monitor on app launch so pressing Escape anywhere while the popover is open closes it. On Linux / Windows, the standard keydown path closes the popover directly.

TipThe popover is the fastest way to scrub through a long loop without raising the main window. Grab the thumb, drag, release — the main window's engine or Web Audio playback jumps to the new position.