The frontend
src/store.tsis a tiny external store (getState,setState,useStore(selector)onuseSyncExternalStore). It holds the latestSnapshot(snap), plus UI state: which panes are shown, zoom (pxPerBeat), track heights, snap mode, selection (selectedRegions,selectedTracks,focus), the clipboard, the open dialog, theme, UI scale, the automation and take-folder view state, and toasts.- The snapshot is replaced wholesale on every edit (
act) and on theprojectevent. Treat it as read-only; never mutate it in place. - Components select narrowly (
useStore((s) => s.snap?.project.tracks)) so they re-render only when what they show changes.Apponly subscribes to layout-level flags. A selector must return the same value while nothing changed: one that builds a new array or object on each call re-renders forever and blanks the window (tests/storeSelectors.test.tscatches it). Use a shared empty constant, or select the parts separately.
Ticks and the frame loop
Section titled “Ticks and the frame loop”- The
tickevent (~30 Hz) is kept outside React state:pushTickstores it, rebuilds a meter map (track id, -1 for Stereo Out, -100-n for input n) and callsonTicklisteners. The clip indicators’ holds (peakHold) and gain reductions (gainReduction) use the same keys. Recording previews go intoliveTakes. src/frame.tsruns onerequestAnimationFrameloop for everything that animates (playhead, meters, LCD).framePosextrapolates the playhead from the last tick by the time since it arrived (capped at 100 ms) and wraps at the cycle end, so motion is smooth at display rate.coalesced(f)schedules a redraw once per frame however often it is requested.
Canvas views
Section titled “Canvas views”The tracks area (Arrange.tsx, drawing in components/tracksCanvas/draw.ts), ruler, Piano Roll,
Audio Track Editor and meters are canvas drawings. The common pattern:
- Draw functions read the store with
getState()and the canvas theme fromsrc/theme.ts(a mutable object thatapplyThemeoverwrites), and subscribe tothemeIdso they redraw on a theme switch. - Backing stores are sized with
pixelRatio()(uiScale.ts), which accounts for both the display density and the interface zoom. - Redraws are triggered by store changes, ticks and viewport changes, coalesced to one per frame.
- Waveforms (
waveform.ts) are fetched once per clip as binary (clip_waveformreturns raw bytes: a signed min/max pair per 32 frames) and cached.
Layout and viewport
Section titled “Layout and viewport”src/layout.ts:trackLayoutcomputes every track’s top, height (0 for tracks hidden or inside a collapsed stack, plus open take-folder rows) and stack depth.indexAtmaps a y coordinate to a track,stepVisiblesteps over hidden ones. Heights are stored per project inlocalStorage.src/view.tsshares the tracks area’s scroll position (viewport,onViewport) between the ruler, the headers and the canvas without React state, and holds layout constants (RULER_H,HEADER_W, the track colour palette).src/time.tsconverts between samples and bars/beats (1-based, like Logic’s LCD).
Themes
Section titled “Themes”src/themes.tsdefines each theme as a map of CSS custom properties (applied to:root) plus aCanvasThemefor canvas drawing. Flat, Midnight, Light and High Contrast spread the previous theme’s maps and override what differs, so a new variable only needs a value in Studio to exist everywhere.src/styles/uses only the variables, never literal theme colours.src/styles/index.cssimports one stylesheet per area, and its order is the cascade:tokens.cssfirst, the rest in the order they were written, so an area can come in more than one file (see the comment there).
Key commands
Section titled “Key commands”src/keymap.tslists every rebindable command (KEY_COMMANDS: id, label, group, default keys) and, read-only, the native menu’s accelerators (MENU_SHORTCUTS).- A key string is modifiers then key:
Modis ⌘ on macOS and Ctrl elsewhere,Ctrlis the Control key on macOS (folded intoModelsewhere bynormalizeKey), thenAlt,Shift.eventKeynames the physical letter or punctuation for Alt/⌘ chords, since macOS Option changes the character. App.tsxinstalls onekeydownlistener (onKey). It ignores events in inputs, while a dialog is open and while an open sheet holds the command back (sheetBlocks, see Sheets and key commands), looks the key up withcommandForEvent(which honours the overrides insettings.json›ui.keymap) and runs the matching entry inKEY_ACTIONS.- Editors with their own keys (Piano Roll, Event List, Step Sequencer, Audio Track Editor,
Musical Typing, Step Input, automation selection) handle them in their own listeners when
they have focus. Edit › Select’s key commands act on notes and events while the Piano Roll or
the Event List has focus (
src/noteSelect.ts). tests/commands.test.tsreadskeymap.ts,App.tsxandmenu.rsand fails when a key command or menu id has no handler, or two commands share a default key.
Menus and dialogs
Section titled “Menus and dialogs”- The native menu bar is built in
src-tauri/src/menu.rs. Each item emits amenuevent with its id;handleMenuinApp.tsxdispatches it. Editors get the first look: the Audio Track Editor (editorMenuCommand) and the Piano Roll (pianoRollMenu) take Split, Cut, Copy, Delete and similar when they have focus or a selection. Menu commands are ignored while a render is running (isBouncing). - In-window menus (right-click menus, local menus, slot menus) are rendered by
components/Menu.tsx(MenuHost);src/ops.tsbuilds the track, region and lane menus. - Dialogs are chosen by
state.dialog(new-tracks,settings,chooser,bounce,bounce-in-place,project-settings,recovery,alternatives), plus separately flagged sheets (group settings, Marker List, Channel EQ, Musical Typing, Controller Assignments, Strip Silence). askName(components/NameSheet.tsx) asks for a name or a value in a small sheet above everything else, the Project Chooser included, and resolves with it or null. Never usewindow.prompt,confirmoralert: macOS’s web view doesn’t implement them (prompt returns null), andtests/browserDialogs.test.tsfails if they appear insrc/. Detached windows show the name sheet too.- Native file dialogs and confirmations use
@tauri-apps/plugin-dialog(src/files.ts).
Detached windows
Section titled “Detached windows”A detachable panel (the Mixer, the Editors, the Event List, the Step Sequencer, a Channel EQ, a
built-in effect panel) can be shown in an OS window of its own, labelled panel-<key> (keys:
mixer, editor, list, steps, eq:<track>, fx:<uid>, the last two shared with
fxDock.ts). Every window loads the same frontend;
main.tsx renders DetachedWindow instead of App when the label is a panel’s.
- Authority. The main window decides what is detached (
state.detached), remembers it with each kind’s window geometry (localStoragemarbble.detachedPanels), and opens, focuses and closes the windows (windows.rs:open_panel_window,focus_panel_window,close_panel_window). Panel windows ask it withpanel-msgevents (hello,attach,detach,close,key,geometry), and key commands pressed in them run in the main window’sKEY_ACTIONS. - Kinds.
src/panelKinds.tsxregisters each kind withsrc/detach.ts: its title, what its window renders, whether a window’s state shows it, and the state changes that hide and show it (bottomfor the Mixer, Editors, Event List and Step Sequencer,fxDockfor effect panels). Each window hides a detached panel its own state tries to show (X, a slot click) and brings that panel’s window to the front instead, so a panel is never shown twice. The main window closes the windows of panels whose track or effect has gone. - Sync. Each window keeps its own store. The fields in
detachModel.tsSYNCED_FIELDS(the region, track and note selection, focus, the region, channel strip, plug-in and Channel EQ clipboards, snap, Catch, hide mode, open take folders, the automation lane shown, theme, interface size, waveform style, the plug-in lists,detached) go to the other windows asui-syncpatches (syncPatchcompares by identity), settings asui-settings. Snapshots don’t travel through the frontend: after a window’sact()changes itssnap, it callsbroadcast_snapshot(throttled to one every 30 ms), and Rust sends the session’s snapshot to every other window asproject-sync. The engine’stickandprojectevents reach every window anyway. Applying another window’s change is marked remote, so it isn’t sent back. With nothing detached, the main window sends nothing. - Event targets. Tauri delivers an event to every listener registered for any target,
whatever target it was sent to, and a plain
listen()registers for any target. So the events meant for one window (menu,panel-msg,ui-sync,ui-settings,project-sync) are listened for withgetCurrentWebviewWindow().listen. - Menu bar.
windows.rsroute_menusends a native menu item to the focused panel window, else to the main window. On macOS the menu bar is the app’s, so this is what decides which window an item acts on. A panel window handles the editor’s Edit items itself and passes the rest to the main window. - Geometry. Kept per kind, as the outer position and inner size, in physical pixels; on
macOS in points, since the screens share one coordinate space in points there and a physical
position would be converted with the scale of the screen the window is on now. It is fitted
to the monitors’ work areas when a window opens (
fitToMonitors). - Closing. A panel window’s close button attaches the panel. When the main window is
destroyed,
windows.rsdestroys the panel windows without asking them, so the detached Mixer and Editors reopen at the next launch.