Skip to content

14 — UI Framework

Status · reconciled 2026-08-30. egui/SDL3, the Auvra theme and custom mixer widgets are implemented on macOS. N100/X11/touch validation belongs to M6.

The interaction design is in 13-mvp-and-ux.md; this document fixes how it is rendered. It is the technology decision behind the screens.

Auvra’s UI is built with egui (immediate-mode, pure Rust), rendered through our existing SDL3 window — Cocoa on the macOS dev host and SDL3’s x11 backend in the N100 kiosk. X11 is required because native plugin editors and the host must share a display server; 16 is canonical for that decision. The signature “instrument” look comes from a two-layer strategy (§3): themed built-in widgets for chrome, plus a small library of custom-drawn Auvra widgets for the performance surfaces.

egui fits Auvra’s specific constraints better than the alternatives:

  • It rides the SDL3 backend we already have. SDL3 gives Cocoa on macOS and X11 on the N100 kiosk (07, 16); egui is a mesh generator on top, so one UI stack covers both ends. On the target the production path is egui → egui_glow → the SDL3-created GL context (we own it).
  • Immediate-mode matches the engine. The live HUD/meters redraw from the wait-free MeterSnapshot (auvra-core::telemetry, doc 12 §5) every frame — that is egui’s model. The UI thread reads the triple_buffer snapshot and never touches the RT audio thread.
  • Permissive licence (MIT/Apache-2.0). Zero friction for a commercial product and the plugin store.
  • Fastest path to the 13 screens, and proven audio-UI precedent to lean on (custom knob/fader widget crates).
Alternative Verdict
Slint Purpose-built for embedded HMI + desktop, with a native linuxkms backend (renders bare on DRM/GBM/EGL — could drop SDL on the target) and a designer-friendly declarative language. But its royalty-free licence excludes embedded systems, and Auvra-in-an-S90-shell is embedded → a paid commercial licence is required regardless of disclosure.1 Its linuxkms backend is also still experimental. Re-evaluate only if the premium aesthetic later justifies the licence + a declarative-UI rewrite.
iced Elm-architecture, pure Rust, but winit-based — no first-class bare-KMS/DRM story for the kiosk target.
embedded-graphics / LVGL The no_std / MCU tier — wrong for a GPU x86 Linux box, and LVGL is C, not Rust-native. (Auvra’s only true MCUs are the RP2350 nodes, which have no display.)
Xilem / GPUI / Makepad / Dioxus Too young to ship an instrument on, or the wrong rendering target / ecosystem.

Tier note. Auvra’s UI runs on an x86 N100 with an Intel GPU under Linux — an embedded appliance, not a microcontroller. The framework must be desktop-GPU-class and work in a minimal kiosk without bringing in a desktop shell.

3. The Auvra look — skeuomorphic hardware console

Section titled “3. The Auvra look — skeuomorphic hardware console”

Styling direction (the rule of thumb): a physical mixing desk, not a flat modern UI. Charcoal channel strips with subtle vertical gradients and recessed dark seams between them; cyan lit-display readouts; gradient-green meters with a held-peak marker and a dB scale; beveled dark control faces; and — the signature element — realistic metallic fader caps. Reference target: a hardware-style DAW channel/master strip; the in-repo visual is the spike render at crates/auvra-ui/tests/snapshots/auvra_perform_panel.png.

Faders must render exactly as on a real console: a brushed-metal cap with horizontal grip ridges, a top highlight, a recessed centre band with a bright position line, a drop shadow, sitting in a recessed groove beside a dB scale. This is a hard requirement, implemented in auvra-ui::widgets::fader.

A native look comes from two layers:

  1. Global identity via egui::Style / Visuals / Spacing / FontDefinitions. The charcoal palette, per-interaction-state widget colours, corner radius, strokes, spacing, and the type system. This carries the “chrome” — BROWSE lists, EDIT fields, menus, beveled buttons.

Type system. Inter for UI text, JetBrains Mono for all numeric readouts (meters, dB scales, counters, param values) — monospace gives tabular figures so digits don’t jitter width as values change, an instrument-UX must. Both are SIL OFL 1.1 (embed + ship commercially, OK) and are embedded whole in the binary via include_bytes! (crates/auvra-ui/assets/fonts/) so the immutable appliance image needs no fontconfig and renders deterministically. No subsetting — the target is a full x86 Linux box with GB of storage, and egui rasterizes only the glyphs actually drawn, so a subset saves ~1 MB of disk for zero runtime gain while risking wrong-font fallbacks (that’s an MCU/web optimization, wrong tier here). The wordmark is the stacked a·uvr·a lockup — two blue lowercase as frame a smaller, top-aligned white uvr over a blue baseline rule. It drives the UI header directly — assets/auvra_logo.svg is embedded via include_image! (auvra-ui::logo::logo_image) and rasterized at runtime by the egui_extras SVG loader, so editing the SVG updates the UI on the next build. The SVG is clean, font-outlined SVG — assets/auvra_logo.svg (white uvr, for the dark brand) and auvra_logo_light.svg (dark uvr, for light backgrounds). 2. Bespoke instrument controls as custom widgets drawn with egui’s Painter (mesh gradients, layered strokes, vector shapes): the metallic fader, the gradient meter + peak-hold + dB scale, rotary knobs, value tiles, the PERFORM slot tiles. This is where the console identity lives.

Both are centralised in one auvra-ui crate (theme + widgets + panel) so the look stays coherent as the screens grow. The spike under crates/auvra-ui renders a channel strip + master (metallic fader, gradient meters, value tiles, coloured footers) from a live MeterSnapshot; its snapshot PNG is the visual reference.

  • Styling is code, not a visual designer (unlike Slint). Design specs → Rust by hand.
  • Stock widget shapes aren’t deeply reskinnable — restyle colours/rounding/ borders; draw anything that must look truly custom yourself (idiomatic in egui).
  • Rich effects (blur, soft shadows, elaborate gradients) exceed egui’s basics — but we own the SDL3/OpenGL context, so a custom shader/background layer can be composited under egui as an escape hatch.

The authoritative layout reference for the mixer view is the Logic Pro for iPad mixer (channel strips + master, coloured track labels, M/S keys). Auvra adopts its channel-strip grammar; it does not adopt Logic-specific chrome (record-enable, automation modes, the FX-section selector column, the timeline transport) — Auvra is an instrument host, not a DAW. Implemented in auvra-ui::panel (MainPanel), the strip stacks top → bottom:

  1. M / S keys — a two-key row. M (mute) lights amber-orange when engaged; S (solo) lights yellow. Both are auvra-ui::widgets::toggle caps. State is UI-owned and mirrored to the engine via SetStripMute / SetStripSolo.
  2. Pan knob — a centred rotary (widgets::knob); a 0..1 knob position maps to −1..1 pan, emitted as SetStripPan.
  3. Fader + peak meter — the signature metallic fader beside a gradient-green peak meter with a dB scale (SetStripGain). The fader follows a proper dB law (auvra-ui::widgets::fader_pos_to_gain): the cap position is 0..1 but the engine is fed the linear gain, with unity (0 dB) a little below the top and a +6 dB headroom band above it, then a taper compressing to the −60 dB floor — the scale ticks (fader_db_to_pos) sit at their true positions, so the labels match the audio.
  4. Coloured title — a full-width footer filled with the slot’s authored colour, showing the patch/slot name over its zone summary. Titles use vivid, distinct per-slot hues (the reference’s saturated gradient); the glyph colour is luminance-adaptive (dark text on bright hues, light on dark) so every colour stays legible.

Master strip mirrors the reference: an M / Dim key row (Dim ≈ −18 dB, both folded into the master gain UI-side so no extra command is needed), the master fader + stereo meters + dB scale, and a plain (uncoloured) Master footer showing live limiter gain-reduction.

One fader row. Every strip’s fader — slot, FX bus, master, and empty placeholder — shares a single top edge, so the console reads as one continuous fader row however different the controls above each strip are (slots carry a pan knob + plugin/insert selectors + send knobs; the master carries an insert picker; buses carry just a label). Alignment is structural, not hand-tuned per strip: each strip pads its controls down to a fixed prelude height (panel::PRELUDE_H) before drawing its fader, and that same constant reserves the fader’s height (fader = strip − footer − prelude), so the two can’t disagree. MIN_STRIP_H is derived so the fader never has to clamp — hence the footer can never paint over the fader’s lower end. Two headless geometry tests lock both invariants (all_faders_share_one_top, fader_never_overlaps_footer_at_any_height).

Live value + detents. While a fader or knob is being dragged, a lit readout shows the current value — a cyan dB bubble above the fader (0.0, +3.2, -12.4, -∞) and the pan position (C / L50 / R50) in place of the PAN label. Both controls have a magnetic detent: the fader snaps to unity (0 dB) and the pan knob to centre when dragged within a small zone, so the musically-important positions are easy to hit exactly.

Solo semantics. Solo is a real engine feature, not a UI trick: when any strip is soloed, the mixer silences every un-soloed slot via a smoothed per-strip solo gate (Strip::apply_solo, decided once per block from the global solo state); mute is an independent gate, so a soloed-but-muted strip is still silent. The UI matches this by dimming the titles of un-soloed strips while a solo is active.

  • auvra-ui (new crate) depends on egui + auvra-core; it holds the theme and widgets and renders from engine state/telemetry. auvra-core stays platform-free (it exposes MeterSnapshot and, later, patch/param views).
  • The UI runs on the UI thread, reading the wait-free triple_buffer snapshot and sending Commands over the ring — it never blocks or shares data with the RT audio thread (12 §5).
  • The M1/M2 Painter trait (clear/rect) was a scaffold; egui supersedes it. It can stay for the boot splash or be retired.
  • Verification: egui_kittest renders UIs to PNG headlessly (wgpu), giving deterministic visual snapshots that double as regression tests — no window needed.
  1. The complete egui/GL path on the N100 X11 kiosk.
  2. Multi-touch gestures on the capacitive panel (fader drag, two-finger) — egui’s touch support is basic; validate on the real N100 panel (P3, 07).
  3. Validate palette contrast, fonts and control sizing on the real panel under stage lighting.
  4. Whether a composited shader background layer is worth it for the aesthetic, or pure-egui vector drawing suffices.
  1. Slint licensing: the royalty-free licence covers desktop/mobile/web but excludes embedded systems; embedded use requires a commercial licence. https://slint.dev/pricing ↩