import { app } from "../../scripts/app.js"; app.registerExtension({ name: "phazei.MULTI_BUTTON", getCustomWidgets() { return { MULTI_BUTTON: (node, name, data) => { // normalize options const opts = data?.options || (Array.isArray(data) ? (data[1] || {}) : (data && typeof data === "object" ? data : {})); const btns = Array.isArray(opts.buttons) && opts.buttons.length ? opts.buttons : [{ label: "Action", callback: () => { } }]; const H = Math.max(22, opts.height ?? 28); const pad = Math.max(0, opts.pad ?? 6); const gap = Math.max(0, opts.gap ?? 6); const alternate = opts.alternate ?? true; const selectMode = opts.selectMode ?? null; // "single" | "multi" | null const selectedInit = Array.isArray(opts.selected) ? new Set(opts.selected) : new Set(); // theme keys available in your build const T = (LiteGraph?.Themes?.current || LiteGraph); const colors = { bg: T.WIDGET_BGCOLOR || "#2b2b2b", fg: T.WIDGET_TEXT_COLOR || "#DDD", border: T.WIDGET_OUTLINE_COLOR || "#666", hi: T.WIDGET_ADVANCED_OUTLINE_COLOR || T.WIDGET_OUTLINE_COLOR || "#888", fontSize: T.NODE_TEXT_SIZE || 12, }; // tiny shade for alternating const hexToRgb = (hex) => { const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); if (!m) return { r: 34, g: 34, b: 34 }; return { r: parseInt(m[1], 16), g: parseInt(m[2], 16), b: parseInt(m[3], 16) }; }; const rgbToHex = (r, g, b) => `#${[r, g, b].map(v => Math.max(0, Math.min(255, v | 0)).toString(16).padStart(2, "0")).join("")}`; const lighten = (hex, p = 0.06) => { const { r, g, b } = hexToRgb(hex); const L = c => c + (255 - c) * p; return rgbToHex(L(r), L(g), L(b)); }; const darken = (hex, p = 0.06) => { const { r, g, b } = hexToRgb(hex); const D = c => c * (1 - p); return rgbToHex(D(r), D(g), D(b)); }; const { r: _r, g: _g, b: _b } = hexToRgb(colors.bg); const lum = (0.2126 * _r + 0.7152 * _g + 0.0722 * _b) / 255; const bgAlt = lum < 0.5 ? lighten(colors.bg, 0.06) : darken(colors.bg, 0.06); const state = { regions: [], pressed: -1, selected: selectedInit }; // Redraw helper: setDirtyCanvas for LiteGraph, triggerDraw for Vue (Nodes 2.0). // WidgetLegacy.vue sets widget.triggerDraw when binding; it's undefined in LG mode. const redraw = () => { node.setDirtyCanvas(true, true); widget?.triggerDraw?.(); }; const rr = (ctx, x, y, w, h, r = 6) => { ctx.beginPath(); ctx.moveTo(x + r, y); ctx.lineTo(x + w - r, y); ctx.quadraticCurveTo(x + w, y, x + w, y + r); ctx.lineTo(x + w, y + h - r); ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h); ctx.lineTo(x + r, y + h); ctx.quadraticCurveTo(x, y + h, x, y + h - r); ctx.lineTo(x, y + r); ctx.quadraticCurveTo(x, y, x + r, y); ctx.closePath(); }; // expose a few controls const api = { setDisabled(idx, val) { if (btns[idx]) { btns[idx].disabled = !!val; redraw(); } }, setSelected(idxOrArr, val) { if (Array.isArray(idxOrArr)) { state.selected = new Set(idxOrArr); } else { if (val) state.selected.add(idxOrArr); else state.selected.delete(idxOrArr); } redraw(); opts.onSelect?.(Array.from(state.selected)); }, getSelected() { return Array.from(state.selected); }, setButtons(newBtns) { if (!Array.isArray(newBtns) || !newBtns.length) return; // mutate in place so external refs remain valid btns.length = 0; for (const b of newBtns) btns.push(b); // clamp selections state.selected.forEach(i => { if (i < 0 || i >= btns.length) state.selected.delete(i); }); redraw(); }, update() { redraw(); }, buttons: btns, // convenience reference }; const widget = { name, type: "custom", serialize: false, // single row; height fixed computeSize: (w) => [w, H], draw(ctx, node, width, y) { ctx.save(); const n = Math.max(1, btns.length); const inner = Math.max(0, width - pad * 2); const totalGaps = Math.max(0, gap * (n - 1)); const bw = (inner > totalGaps ? (inner - totalGaps) : inner) / n; const bh = H - 6; const y0 = y + 3; state.regions = []; ctx.font = `${colors.fontSize}px sans-serif`; ctx.textAlign = "center"; ctx.textBaseline = "middle"; for (let i = 0; i < n; i++) { const x = pad + i * (bw + gap); state.regions.push({ x, y: y0, w: bw, h: bh }); const disabled = !!btns[i]?.disabled; const isSel = state.selected.has(i); // fill rr(ctx, x, y0, bw, bh, 6); ctx.save(); ctx.fillStyle = alternate ? (i % 2 ? bgAlt : colors.bg) : colors.bg; if (disabled) ctx.globalAlpha = 0.5; ctx.fill(); ctx.restore(); // selected overlay (subtle) if (isSel) { ctx.save(); ctx.globalAlpha = 0.18; ctx.fillStyle = colors.hi; rr(ctx, x, y0, bw, bh, 6); ctx.fill(); ctx.restore(); } // normal border ctx.save(); ctx.lineWidth = 1; ctx.strokeStyle = colors.border; rr(ctx, x, y0, bw, bh, 6); ctx.stroke(); ctx.restore(); // pressed border if (state.pressed === i) { ctx.save(); ctx.lineWidth = 2; ctx.strokeStyle = colors.hi; rr(ctx, x, y0, bw, bh, 6); ctx.stroke(); ctx.restore(); } // label ctx.save(); ctx.fillStyle = colors.fg; if (disabled) ctx.globalAlpha = 0.6; ctx.beginPath(); ctx.rect(x + 4, y0, Math.max(0, bw - 8), bh); ctx.clip(); ctx.fillText(btns[i]?.label ?? `B${i + 1}`, x + bw / 2, y0 + bh / 2); ctx.restore(); } ctx.restore(); }, // supports (event, x, y) and (event, [x,y]) mouse(event, a, b) { const lx = Array.isArray(a) ? a[0] : a; const ly = Array.isArray(a) ? a[1] : b; const hit = state.regions.findIndex(r => lx >= r.x && lx <= r.x + r.w && ly >= r.y && ly <= r.y + r.h); if (event.type === "pointerdown") { if (hit !== -1 && !btns[hit]?.disabled) { state.pressed = hit; redraw(); return true; } } if (event.type === "pointerup") { const i = state.pressed; state.pressed = -1; // In the Vue renderer (Nodes 2.0), WidgetLegacy wraps this widget // in a mini-canvas. The pointerup coordinates arrive in node-local // space (offset by title bar + widgets above), while state.regions // are in mini-canvas-local space. This makes the hit-test fail. // Fix: if we recorded a valid press on pointerdown (where coords // ARE correct), use that index directly instead of re-hit-testing. // Fall back to hit-test when available (works in LiteGraph mode). const target = (hit !== -1) ? hit : i; if (i !== -1 && target === i && !btns[i]?.disabled) { // confirm (optional) -> action -> selection const b = btns[i]; let go = true; if (b?.confirm && typeof window !== "undefined" && window.confirm) { const msg = typeof b.confirm === "string" ? b.confirm : "Are you sure?"; go = window.confirm(msg); } if (go) { // action b?.callback?.(node, i); // selection behavior if (selectMode === "single") { state.selected.clear(); state.selected.add(i); opts.onSelect?.(Array.from(state.selected)); } else if (selectMode === "multi") { if (state.selected.has(i)) state.selected.delete(i); else state.selected.add(i); opts.onSelect?.(Array.from(state.selected)); } } redraw(); return true; } redraw(); } if (event.type === "pointerleave" || event.type === "pointercancel") { state.pressed = -1; redraw(); } return false; }, // expose control surface setDisabled: (...a) => api.setDisabled(...a), setSelected: (...a) => api.setSelected(...a), getSelected: () => api.getSelected(), setButtons: (...a) => api.setButtons(...a), update: () => api.update(), buttons: api.buttons, }; return widget; }, }; }, });