# Six Sines UI

> Reference for the Six Sines synthesizer UI (src/ui/). Covers the editor and its patchMain binding, the PatchContinuous/PatchDiscrete data model, the createComponent factory, the panel/sub-panel navigation model, the DAHDSR/LFO/Modulation component templates, clipboard, presets and theming.

- Skill: `baconpaul/six-sines-ui` (Agent Skill)
- Install (CLI): `npx skillmds@latest add baconpaul/six-sines-ui`
- Raw SKILL.md: https://api.skillmd.com/api/skills/baconpaul/six-sines-ui/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: baconpaul (https://skillmd.com/u/baconpaul)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/baconpaul/six-sines-ui

---


# Six Sines UI

**Repo:** `baconpaul/six-sines`. Paths are relative to the repo root.
**Framework:** JUCE, through `sst-jucegui`.

Companion skill: `six-sines-dsp` — read its §3 (the node model) and §9 (patch/patchMain)
before working here, because the UI mirrors both.

```bash
cmake --build $B --target six-sines_standalone --parallel
```

Build the standalone, not `six-sines_all` — the latter overwrites installed plugin bundles.

---

## 1. The editor binds patchMain, it does not own a copy

```cpp
struct SixSinesEditor : jcmp::WindowPanel, ScreenHolder<SixSinesEditor>
{
    Patch &patchMainRef;    // bound to Synth::patchMain
    SixSinesEditor(Patch &patchMain, Synth::audioToMainQueue_t &, ...);
```

This is the whole model. **`patchMainRef` is the UI's data — there is no separate UI copy
to keep in sync.** A widget reads a `Param` straight out of `patchMainRef.paramMap`, and
writing one writes `patchMain` directly and *then* tells the audio thread.

That is the opposite of a "dumb terminal" design and it is deliberate: the main thread
owns `patchMain`, the audio thread owns `patch`, and the CLAP adapter reads only
`patchMain`. Nothing has to wait for an echo before displaying.

Editor size is fixed: `edWidth` / `edHeight` in `six-sines-editor.h`. Read them; the layout
has been reproportioned before.

### Idle

An `idleTimer` drives `SixSinesEditor::idle()`, which does two things:

1. **Checks `uiForceRebuild`.** An out-of-band write to `patchMain` — host `stateLoad`,
   preset load, an inactive `paramsFlush` — bumps that atomic. `patchMainRef` already holds
   the new values, so the editor calls `rebuildFromPatchMain()` to refresh every widget.
2. **Drains `audioToMain`.** `UPDATE_PARAM` is the host-automation echo and goes through
   `setAndSendParamValue(id, value, /*notifyAudio*/ false)` — writing the model and
   refreshing the widget without dirtying the patch or bouncing the value back. The rest is
   telemetry: VU, voice count, CPU, sample rate, MTS pointer.

`editorActive` gates the audio thread's telemetry pushes, so a closed editor costs nothing.

## 2. Data bindings

`patch-data-bindings.h`. Two adapters implement the `sst-jucegui` data interfaces over a
parameter id:

```cpp
struct PatchContinuous : jdat::Continuous { SixSinesEditor &editor; uint32_t pid; Param *p; ... };
struct PatchDiscrete   : jdat::Discrete   { ... };
```

Both resolve `pid` through `patchMainRef.paramMap` in their constructor and **assert and
terminate if the id is not there** — a parameter you forgot to push in `Patch::params()`
fails loudly at startup rather than silently doing nothing.

`setValueFromGUI` does four things in order: write `p->value`, mark the patch dirty, push
`SET_PARAM` onto `mainToAudio`, request a params flush so the host sees it.

Display strings come from `p->meta` (`ParamMetaData` — see the `sst-param-metadata` skill).
`PatchContinuous::setTemposyncPowerPartner` lets a rate control render as a beat fraction
when its temposync toggle is on; the partner is another `jdat::Discrete`.

`onPullFromMin` / `onPullFromDef` fire when a control leaves its minimum or default — used
for "turning this up should also switch that on" behaviour.

## 3. createComponent

Never wire a control by hand. `createComponent` builds the widget and its binding together
and attaches everything:

```cpp
createComponent(editor, *this, patch.someNode.someParam, myWidget, myWidgetData, args...);
```

It sets up:

- the binding (`PatchContinuous` or `PatchDiscrete`) and `setSource`
- `onBeginEdit` / `onEndEdit` pushing `BEGIN_EDIT` / `END_EDIT` to the audio thread, which
  is what gives the host proper automation gestures
- tooltip show/update/hide on edit, idle hover and popup
- the right-click popup menu for continuous controls
- `setClapParamId`, so the host can map the widget to its parameter
- registration in `editor.componentByID[id]`, used by `rebuildFromPatchMain`
- `panelSelectGestureFor`, so touching the control also selects its panel

`createRescaledComponent` is the same thing with a rescaler in between — e.g.
`PatchContinuous::cubic_t`, a cubic throw for controls that need fine resolution near zero.

To suppress tooltips for a widget type, specialize by widget only:

```cpp
template <> constexpr bool suppressTooltipByWidget<MyWidget>() { return true; }
```

That covers every binding of that widget type. `JogUpDownButton` is already specialized —
it shows its value in the label.

## 4. The component templates mirror the DSP mixins

The DSP side composes nodes from `EnvelopeSupport`, `LFOSupport` and `ModulationSupport`.
The UI composes panels from the matching three:

```cpp
template <typename Comp, typename PatchPart> struct DAHDSRComponents;      // dahdsr-components.h
template <typename Comp, typename Patch>     struct LFOComponents;         // lfo-components.h
template <typename Comp, typename Patch>     struct ModulationComponents;  // modulation-components.h
```

So a sub-panel is:

```cpp
struct SourceSubPanel : juce::Component, HasEditor,
                        DAHDSRComponents<SourceSubPanel, Patch::SourceNode>,
                        ModulationComponents<SourceSubPanel, Patch::SourceNode>,
                        LFOComponents<SourceSubPanel, Patch::SourceNode>,
                        SupportsClipboard
```

**A node that gains a mixin on the DSP side should gain the matching template here.** You
get the whole envelope, LFO or mod-slot control set for free, including the step-sequencer
editor (`LFOComponents::StepEditor`).

## 5. Panels and sub-panels

Two tiers. The always-visible panels across the body — `SourcePanel`, `MatrixPanel`,
`MainPanel`, `MixerPanel`, `MacroPanel` — show the headline control for each node. A single
`singlePanel` at the bottom hosts exactly one sub-panel at a time: `SourceSubPanel`,
`MatrixSubPanel`, `SelfSubPanel`, `MixerSubPanel`, `MainSubPanel`, `MainPanSubPanel`,
`FineTuneSubPanel`, `PlayModeSubPanel`, `MacroSubPanel`, `SettingsPanel`.

Clicking a control in a top panel selects the corresponding sub-panel — that is what
`panelSelectGestureFor` and each panel's `beginEdit(args...)` do. Sub-panels carry an
`index` and a `setSelectedIndex(size_t)`, since one sub-panel serves all six operators or
all fifteen matrix cells. `hideAllSubPanels()` and `doSinglePanelHamburger()` handle
switching.

`ls src/ui/` is the index. Also there and easy to miss: `segmented-ratio-editor` (the ratio
control's own widget), `waveform-display`, `spectrum-analyzer`, `knob-highlight`,
`ui-constants.h` / `ui-defaults.h`, `six-sines-skin.h`.

Layout is `SixSinesEditor::resized()` driven by `ui-constants.h`. Read it rather than a
diagram.

## 6. Clipboard

`clipboard.h`. Panels implement `SupportsClipboard` and get copy/paste/reset for an
envelope, an LFO, a modulation block, or a whole node, typed by `ClipboardType`. The
templates take any node type, so a new node gets this by declaring the interface —
`copyEnvelopeTo`, `pasteEnvelopeFrom` and friends.

Paste goes through the editor so it produces proper parameter edits, not raw writes.

## 7. Presets and theming

`src/presets/` holds `PresetManager` and `UIThemeManager`; the editor owns one of each plus
a `PresetDataBinding` behind the preset jog button.

Preset identity is subtler than it looks — a user preset can share a name with a factory
one, and jogging must stay in the right list and survive a session save. That behaviour is
pinned by `tests/preset_jog.cpp`; read it before touching preset selection.

Theming: `SixSinesSkin`, `applyTheme`, `setThemeFromPreference`, plus a live colour editor
in a `DocumentWindow` (`openColorEditor`, `refreshColorEditorFromSkin`,
`commitSessionColorMap`). `factoryThemeSentinel` marks a built-in theme in the stored
preference.

## 8. DAW extra state

State that belongs to the plugin instance rather than the patch — zoom factor, theme
choice, MPE settings, smoothing times — lives in DAW extra state.
`scheduleDawExtraStatePush`, `applyDawExtraState` and `dawExtraStateRefreshListeners`
manage it, and `pushAudioDawState` sends the audio-relevant subset
(`SET_AUDIO_DAW_STATE`) to the engine.

Register a `dawExtraStateRefreshListener` if a widget needs to re-read after a session
load.

## 9. Adding a control

1. Add the parameter in `src/synth/patch.h` with a permanent id and a version tag — see
   `six-sines-dsp` §4.
2. Declare a `std::unique_ptr<Widget>` and a matching `std::unique_ptr<PatchContinuous>` (or
   `PatchDiscrete`) as members of the panel.
3. `createComponent(editor, *this, patch.node.param, widget, widgetData, args...)` in the
   constructor.
4. Position it in `resized()`.
5. If the parameter affects modulation routing, add its id to `modRoutingParamIds` so
   `recomputeMacroUsage()` runs on automation echo.

Everything else — tooltips, begin/end edit, clap id, rebuild registration — comes from
`createComponent`.

## 10. Sharp edges

- **A missing parameter id terminates at construction.** If the editor dies on launch after
  you added a control, you did not push the parameter in `Patch::params()`.
- **`UPDATE_PARAM` must not dirty the patch.** It is the host's own value coming back;
  `setAndSendParamValue(..., notifyAudio=false)` exists for exactly this.
- **Do not read `Synth::patch` from the UI.** Only `patchMainRef`.
- **`rebuildFromPatchMain` only reaches widgets in `componentByID`** — hand-built controls
  that skipped `createComponent` will not refresh after a preset load.
- Sub-panels are shared across indices; anything cached in one must be reset in
  `setSelectedIndex`.

