K

Design a custom look

Lay out your device's controls your way.

By default Msh lays your controls out in a row. Want more say? Add a ui.ts that describes the look. Editing it never touches the sound, so you can redesign while the device plays.

ui.ts
export function createUI(_node: NodeContext): UI {
  return ui(
    {
      size: { graph: { w: Theme.sizing(80), h: Theme.sizing(48) } },
      palette: { accent: Theme.colors.signal.audio },
    },
    Box({ fill: true }, [
      Spectrum({ input: channel("spectrum"), color: Theme.colors.accent }),
      Stack({ dir: "row", justify: "between", pad: 2 }, [
        Knob({ bind: param("cutoff"), label: "Cutoff" }),
        Knob({ bind: param("q"), label: "Q" }),
      ]),
    ]),
  );
}

A tree: a container with controls and displays inside. Each control binds to a param from your describe, so modulation, automation, and undo keep working. Full set in the widget reference.

Buttons that do things

A Button carries an action, a plain data object (no closures). Use it to set, toggle, or reset params:

Button({ label: "Reset", action: { kind: "resetParams", prefix: "pt_" } });

The kinds:

  • { kind: "setParam", id, value } set one param to a value.
  • { kind: "toggleSetting", id } / { kind: "cycleSetting", id } flip or advance a setting.
  • { kind: "setParams", prefix, value } set every param whose id starts with prefix.
  • { kind: "resetParams", prefix } reset that whole bank to defaults.

One button can reset a 16-point curve's pt_* params instead of editing each.

A full editor window

For a bigger view than the tile, export createEditorUI alongside createUI. The tile header's "open editor" icon opens it in a floating window. It reuses the same bound params, so edits round-trip.

export function createEditorUI(node: NodeContext): UI {
  return ui(
    {
      size: { graph: { w: Theme.sizing(120), h: Theme.sizing(80) } },
      palette: { accent: Theme.colors.signal.audio },
    },
    /* a larger root */
  );
}

Besides the header icon, an { kind: "openEditor" } Button opens it too. Set editorTrigger: "doubleClick" on the graph ui() config to also open it by double-clicking the tile body (default "none", since double-click collides with the reset gesture on knobs and pads).

Two rules

  • No pixel sizes. Use Theme.sizing(...), fill, or grow, so it looks right at any size.
  • Prefer theme colours. Pick from Theme.colors (Theme.colors.accent, Theme.colors.grey[600]) so the device matches the user's theme. For a deliberately fixed colour, pass Theme.raw("#ff3300") or any CSS colour string; it stays constant across themes.

You can give the device its own accent, background, text colour, and a background image from assets. When the widgets aren't enough, draw your own.