Cookbook¶
10 practical recipes for common Pulp UI patterns. Each recipe is self-contained — copy the JS into your ui/main.js and adapt.
1. Form Layout¶
A labeled parameter section with knobs in a row.
const form = createCol("form", "root");
setFlex("form", "gap", 16);
setFlex("form", "padding_top", 20);
setFlex("form", "padding_left", 20);
setFlex("form", "padding_right", 20);
// Section header
createLabel("hdr", "EQ Controls", "form");
setFontSize("hdr", 16);
setFontWeight("hdr", 700);
setTextColor("hdr", "#e0e0e0");
// Row of knobs
const row = createRow("knobs", "form");
setFlex("knobs", "gap", 24);
setFlex("knobs", "justify_content", "center");
["Low", "Mid", "High"].forEach((name) => {
const id = name.toLowerCase();
const col = createCol(id + "-col", "knobs");
setFlex(id + "-col", "align_items", "center");
setFlex(id + "-col", "gap", 4);
createKnob(id, id + "-col");
setFlex(id, "width", 60);
setFlex(id, "height", 60);
setValue(id, getParam(name));
on(id, "change", (v) => setParam(name, v));
createLabel(id + "-lbl", name, id + "-col");
setFontSize(id + "-lbl", 12);
setTextColor(id + "-lbl", "#888888");
setTextAlign(id + "-lbl", "center");
});
2. Modal Dialog¶
A centered overlay with OK/Cancel buttons.
// Backdrop
const backdrop = createPanel("backdrop", "root");
setFlex("backdrop", "width", 600);
setFlex("backdrop", "height", 400);
setBackground("backdrop", "rgba(0,0,0,0.6)");
setFlex("backdrop", "justify_content", "center");
setFlex("backdrop", "align_items", "center");
// Dialog box
const dialog = createCol("dialog", "backdrop");
setFlex("dialog", "width", 300);
setFlex("dialog", "padding_top", 24);
setFlex("dialog", "padding_left", 24);
setFlex("dialog", "padding_right", 24);
setFlex("dialog", "padding_bottom", 24);
setFlex("dialog", "gap", 16);
setBackground("dialog", "#1e1e2e");
setBorder("dialog", "#333333", 1, 12);
setBoxShadow("dialog", 0, 8, 24, 0, "rgba(0,0,0,0.5)");
createLabel("dialog-title", "Save Preset?", "dialog");
setFontSize("dialog-title", 18);
setFontWeight("dialog-title", 700);
setTextColor("dialog-title", "#e0e0e0");
createLabel("dialog-msg", "This will overwrite the existing preset.", "dialog");
setFontSize("dialog-msg", 14);
setTextColor("dialog-msg", "#999999");
// Button row
const btns = createRow("btns", "dialog");
setFlex("btns", "gap", 12);
setFlex("btns", "justify_content", "flex_end");
const cancel = createToggleButton("cancel", "btns");
setLabel("cancel", "Cancel");
registerClick("cancel");
on("cancel", "click", () => setVisible("backdrop", false));
const ok = createToggleButton("ok", "btns");
setLabel("ok", "Save");
registerClick("ok");
on("ok", "click", () => {
// Save logic here
setVisible("backdrop", false);
});
3. Scrolling List¶
A scrollable preset list with click selection.
const scroll = createScrollView("list", "root");
setFlex("list", "width", 200);
setFlex("list", "height", 300);
setBorder("list", "#333333", 1, 4);
const content = createCol("list-content", "list");
setFlex("list-content", "gap", 1);
const presets = ["Init", "Warm Pad", "Bright Lead", "Deep Bass",
"Pluck", "Strings", "Brass", "Choir", "Sweep", "Noise",
"Arp", "Bell", "Organ", "Keys", "Sub"];
let selectedId = null;
presets.forEach((name, i) => {
const id = "preset-" + i;
createLabel(id, name, "list-content");
setFlex(id, "padding_top", 8);
setFlex(id, "padding_left", 12);
setFlex(id, "padding_right", 12);
setFlex(id, "padding_bottom", 8);
setFontSize(id, 13);
setTextColor(id, "#cccccc");
setBackground(id, i % 2 === 0 ? "#1a1a2e" : "#1e1e32");
registerClick(id);
registerHover(id);
on(id, "mouseenter", () => setBackground(id, "#2a2a4a"));
on(id, "mouseleave", () => {
setBackground(id, id === selectedId ? "#3a3a5a" : (i % 2 === 0 ? "#1a1a2e" : "#1e1e32"));
});
on(id, "click", () => {
if (selectedId) setBackground(selectedId, "#1a1a2e");
selectedId = id;
setBackground(id, "#3a3a5a");
});
});
setScrollContentSize("list", 200, presets.length * 35);
4. Tab Panel¶
Switchable content panels with tab buttons.
const tabs = createCol("tabs", "root");
setFlex("tabs", "width", 400);
setFlex("tabs", "height", 300);
// Tab bar
const bar = createRow("tab-bar", "tabs");
setFlex("tab-bar", "gap", 0);
setBackground("tab-bar", "#111122");
const pages = ["Oscillator", "Filter", "Envelope"];
let activeTab = 0;
pages.forEach((name, i) => {
const id = "tab-" + i;
createLabel(id, name, "tab-bar");
setFlex(id, "flex_grow", 1);
setFlex(id, "padding_top", 10);
setFlex(id, "padding_bottom", 10);
setTextAlign(id, "center");
setFontSize(id, 13);
setTextColor(id, i === 0 ? "#ffffff" : "#666666");
setBackground(id, i === 0 ? "#1a1a2e" : "transparent");
setCursor(id, "pointer");
registerClick(id);
on(id, "click", () => {
// Deactivate old tab
setTextColor("tab-" + activeTab, "#666666");
setBackground("tab-" + activeTab, "transparent");
setVisible("page-" + activeTab, false);
// Activate new tab
activeTab = i;
setTextColor(id, "#ffffff");
setBackground(id, "#1a1a2e");
setVisible("page-" + i, true);
});
});
// Content pages
pages.forEach((name, i) => {
const id = "page-" + i;
const page = createCol(id, "tabs");
setFlex(id, "flex_grow", 1);
setFlex(id, "padding_top", 16);
setFlex(id, "padding_left", 16);
setBackground(id, "#1a1a2e");
setVisible(id, i === 0);
createLabel(id + "-title", name + " Settings", id);
setFontSize(id + "-title", 14);
setTextColor(id + "-title", "#cccccc");
});
5. Custom Meter¶
A stereo level meter drawn on a CanvasWidget.
const meter = createCanvas("meter", "root");
setFlex("meter", "width", 40);
setFlex("meter", "height", 200);
function drawMeter(peakL, peakR) {
canvasClear("meter");
const w = 40, h = 200;
const barW = 16, gap = 8;
// Background
canvasRect("meter", 0, 0, w, h, "#111111");
// Left channel
const lH = peakL * h;
const lColor = peakL > 0.9 ? "#ff4444" : peakL > 0.7 ? "#ffaa00" : "#44ff44";
canvasRect("meter", 0, h - lH, barW, lH, lColor);
// Right channel
const rH = peakR * h;
const rColor = peakR > 0.9 ? "#ff4444" : peakR > 0.7 ? "#ffaa00" : "#44ff44";
canvasRect("meter", barW + gap, h - rH, barW, rH, rColor);
// Grid lines at -6, -12, -24 dB
[0.5, 0.25, 0.063].forEach((level) => {
const y = h - level * h;
canvasStrokeLine("meter", 0, y, w, y, "#333333", 1);
});
}
// Poll audio bridge meter data (called by the view system)
// In practice, use Meter widget for automatic polling — this
// shows how to build a custom visualization.
drawMeter(0.6, 0.4);
6. Theme Switching¶
Toggle between built-in themes with animated transitions.
const root = createCol("root");
setFlex("root", "padding_top", 20);
setFlex("root", "padding_left", 20);
setFlex("root", "gap", 12);
createLabel("theme-label", "Theme", "root");
setFontSize("theme-label", 14);
setTextColor("theme-label", "#cccccc");
const combo = createCombo("theme-picker", "root");
setItems("theme-picker", ["Dark", "Light", "Pro Audio"]);
on("theme-picker", "change", (index) => {
const themes = ["dark", "light", "pro_audio"];
const oldTheme = getThemeJson();
setTheme(themes[index]);
const newTheme = getThemeJson();
applyTokenDiff(newTheme); // Smooth transition
});
7. Hover Effects¶
Glow and scale on mouse hover with animation.
function makeHoverButton(id, label, parent) {
const btn = createPanel(id, parent);
setFlex(id, "padding_top", 12);
setFlex(id, "padding_left", 24);
setFlex(id, "padding_right", 24);
setFlex(id, "padding_bottom", 12);
setBackground(id, "#2a2a4a");
setBorder(id, "#444466", 1, 8);
setTransitionDuration(id, 0.15);
const lbl = createLabel(id + "-lbl", label, id);
setFontSize(id + "-lbl", 14);
setTextColor(id + "-lbl", "#cccccc");
setTextAlign(id + "-lbl", "center");
registerHover(id);
on(id, "mouseenter", () => {
setBackground(id, "#3a3a6a");
setBorder(id, "#6666aa", 1, 8);
setBoxShadow(id, 0, 0, 12, 2, "rgba(100,100,200,0.3)");
animate(id, "scale", 1.05, 150, "ease_out_cubic");
});
on(id, "mouseleave", () => {
setBackground(id, "#2a2a4a");
setBorder(id, "#444466", 1, 8);
setBoxShadow(id, 0, 0, 0, 0, "transparent");
animate(id, "scale", 1.0, 150, "ease_out_cubic");
});
}
const row = createRow("btns", "root");
setFlex("btns", "gap", 12);
makeHoverButton("btn-a", "Preset A", "btns");
makeHoverButton("btn-b", "Preset B", "btns");
makeHoverButton("btn-c", "Preset C", "btns");
8. Keyboard Shortcuts¶
Listen for key events on the root to implement shortcuts.
// The root widget receives key events when the plugin window has focus.
on("root", "keydown", (event) => {
const key = event.key;
const cmd = event.metaKey || event.ctrlKey;
if (cmd && key === "s") {
// Save preset
showSaveDialog();
} else if (cmd && key === "z") {
// Undo — handled by DAW, but you can respond to it
} else if (key === "Escape") {
// Close any open modal
setVisible("backdrop", false);
} else if (key === "Tab") {
// Cycle focus between knobs
cycleFocus();
} else if (key === " ") {
// Toggle bypass
const bypass = getParam("Bypass");
setParam("Bypass", bypass >= 0.5 ? 0.0 : 1.0);
}
});
function cycleFocus() {
// Move focus to next interactive widget
// The view system handles Tab focus traversal automatically,
// but you can override for custom behavior.
}
function showSaveDialog() {
setVisible("backdrop", true);
}
9. Drag Interaction¶
An XY pad with custom drag behavior and value readout.
const container = createCol("xy-container", "root");
setFlex("xy-container", "align_items", "center");
setFlex("xy-container", "gap", 8);
// XY Pad
const pad = createXYPad("xy", "xy-container");
setFlex("xy", "width", 200);
setFlex("xy", "height", 200);
setXY("xy", getParam("Cutoff"), getParam("Resonance"));
// Readout labels
const readout = createRow("xy-readout", "xy-container");
setFlex("xy-readout", "gap", 16);
createLabel("xy-x-lbl", "Cutoff: 50%", "xy-readout");
setFontSize("xy-x-lbl", 12);
setTextColor("xy-x-lbl", "#888888");
createLabel("xy-y-lbl", "Resonance: 50%", "xy-readout");
setFontSize("xy-y-lbl", 12);
setTextColor("xy-y-lbl", "#888888");
on("xy", "change", (x, y) => {
setParam("Cutoff", x);
setParam("Resonance", y);
setText("xy-x-lbl", "Cutoff: " + Math.round(x * 100) + "%");
setText("xy-y-lbl", "Resonance: " + Math.round(y * 100) + "%");
});
10. Preset Browser¶
A searchable, categorized preset browser with text filtering.
const browser = createCol("browser", "root");
setFlex("browser", "width", 250);
setFlex("browser", "height", 350);
setBackground("browser", "#141422");
setBorder("browser", "#333333", 1, 8);
// Search bar
const search = createTextEditor("search", "browser");
setFlex("search", "height", 32);
setFlex("search", "margin_top", 8);
setFlex("search", "margin_left", 8);
setFlex("search", "margin_right", 8);
setPlaceholder("search", "Search presets...");
// Category filter
const cats = createRow("cats", "browser");
setFlex("cats", "gap", 4);
setFlex("cats", "padding_left", 8);
setFlex("cats", "padding_top", 8);
const categories = ["All", "Bass", "Lead", "Pad", "FX"];
categories.forEach((cat, i) => {
const id = "cat-" + i;
createLabel(id, cat, "cats");
setFontSize(id, 11);
setTextColor(id, i === 0 ? "#ffffff" : "#666666");
setFlex(id, "padding_top", 4);
setFlex(id, "padding_left", 8);
setFlex(id, "padding_right", 8);
setFlex(id, "padding_bottom", 4);
setBackground(id, i === 0 ? "#333355" : "transparent");
setBorder(id, "transparent", 0, 4);
setCursor(id, "pointer");
registerClick(id);
on(id, "click", () => filterByCategory(cat));
});
// Preset list
const list = createScrollView("preset-list", "browser");
setFlex("preset-list", "flex_grow", 1);
setFlex("preset-list", "margin_top", 8);
const presets = [
{ name: "Deep Sub", cat: "Bass" },
{ name: "Warm Bass", cat: "Bass" },
{ name: "Screamer", cat: "Lead" },
{ name: "Bright Lead", cat: "Lead" },
{ name: "Lush Pad", cat: "Pad" },
{ name: "Dark Pad", cat: "Pad" },
{ name: "Riser", cat: "FX" },
{ name: "Wobble", cat: "FX" },
];
function renderPresets(filter, category) {
// Remove old items
presets.forEach((_, i) => removeWidget("p-" + i));
presets
.filter((p) => !category || category === "All" || p.cat === category)
.filter((p) => !filter || p.name.toLowerCase().includes(filter.toLowerCase()))
.forEach((p, i) => {
const id = "p-" + i;
createLabel(id, p.name, "preset-list");
setFlex(id, "padding_top", 6);
setFlex(id, "padding_left", 12);
setFlex(id, "padding_bottom", 6);
setFontSize(id, 13);
setTextColor(id, "#cccccc");
setCursor(id, "pointer");
registerClick(id);
registerHover(id);
on(id, "mouseenter", () => setBackground(id, "#2a2a4a"));
on(id, "mouseleave", () => setBackground(id, "transparent"));
on(id, "click", () => loadPreset(p.name));
});
}
let currentCategory = "All";
function filterByCategory(cat) {
currentCategory = cat;
renderPresets(getText("search"), cat);
}
on("search", "change", () => {
renderPresets(getText("search"), currentCategory);
});
function loadPreset(name) {
// Load preset logic — set parameters, update UI
}
renderPresets("", "All");