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.
1. Decision
Section titled “1. Decision”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.
2. Why egui (and why not the others)
Section titled “2. Why egui (and why not the others)”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, doc12§5) every frame — that is egui’s model. The UI thread reads thetriple_buffersnapshot 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
13screens, 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:
- 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.
Caveats (set expectations honestly)
Section titled “Caveats (set expectations honestly)”- 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.
3.1 Channel strip anatomy (authoritative)
Section titled “3.1 Channel strip anatomy (authoritative)”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:
M/Skeys — a two-key row.M(mute) lights amber-orange when engaged;S(solo) lights yellow. Both areauvra-ui::widgets::togglecaps. State is UI-owned and mirrored to the engine viaSetStripMute/SetStripSolo.- Pan knob — a centred rotary (
widgets::knob); a 0..1 knob position maps to −1..1 pan, emitted asSetStripPan. - 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. - 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.
4. Architecture fit
Section titled “4. Architecture fit”auvra-ui(new crate) depends onegui+auvra-core; it holds the theme and widgets and renders from engine state/telemetry.auvra-corestays platform-free (it exposesMeterSnapshotand, later, patch/param views).- The UI runs on the UI thread, reading the wait-free
triple_buffersnapshot and sendingCommands over the ring — it never blocks or shares data with the RT audio thread (12§5). - The M1/M2
Paintertrait (clear/rect) was a scaffold; egui supersedes it. It can stay for the boot splash or be retired. - Verification:
egui_kittestrenders UIs to PNG headlessly (wgpu), giving deterministic visual snapshots that double as regression tests — no window needed.
5. Open questions [verify]
Section titled “5. Open questions [verify]”- The complete egui/GL path on the N100 X11 kiosk.
- 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). - Validate palette contrast, fonts and control sizing on the real panel under stage lighting.
- Whether a composited shader background layer is worth it for the aesthetic, or pure-egui vector drawing suffices.
Footnotes
Section titled “Footnotes”-
Slint licensing: the royalty-free licence covers desktop/mobile/web but excludes embedded systems; embedded use requires a commercial licence. https://slint.dev/pricing ↩