193 lines
5.3 KiB
JavaScript
193 lines
5.3 KiB
JavaScript
import { COLORS, EMPTY_VALUE } from "../constants.js";
|
||
import { fitString } from "../utils.js";
|
||
|
||
// --- Constants ---
|
||
|
||
const ROW_HEIGHT = 17;
|
||
const ROW_MARGIN = 10;
|
||
const INNER_MARGIN = 4;
|
||
const BTN_WIDTH = 36;
|
||
|
||
const EMPTY_PRESET = "";
|
||
|
||
// --- PresetRowWidget ---
|
||
|
||
export class PresetRowWidget {
|
||
constructor(name, onNew, onSave, onDelete, onSelect) {
|
||
this.name = name;
|
||
this.type = "custom";
|
||
this.value = EMPTY_PRESET;
|
||
this.presets = [];
|
||
this.last_y = 0;
|
||
|
||
this.onNew = onNew;
|
||
this.onSave = onSave;
|
||
this.onDelete = onDelete;
|
||
this.onSelect = onSelect;
|
||
|
||
// Hit areas [x, width]
|
||
this._hitDropdown = [0, 0];
|
||
this._hitNew = [0, 0];
|
||
this._hitSave = [0, 0];
|
||
this._hitDelete = [0, 0];
|
||
}
|
||
|
||
computeSize() {
|
||
return [0, ROW_HEIGHT];
|
||
}
|
||
|
||
// --- Public API ---
|
||
|
||
setPresets(presets) {
|
||
this.presets = presets;
|
||
if (!presets.length) {
|
||
this.value = EMPTY_PRESET;
|
||
} else if (!presets.includes(this.value)) {
|
||
this.value = presets[0];
|
||
}
|
||
}
|
||
|
||
isEmpty() {
|
||
return !this.presets.length;
|
||
}
|
||
|
||
flashSave(node, label = "✅", duration = 1500) {
|
||
if (this._flashTimer) clearTimeout(this._flashTimer);
|
||
this._saveFlash = label;
|
||
if (node.graph) node.graph.setDirtyCanvas(true, true);
|
||
this._flashTimer = setTimeout(() => {
|
||
this._saveFlash = null;
|
||
this._flashTimer = null;
|
||
if (node.graph) node.graph.setDirtyCanvas(true, true);
|
||
}, duration);
|
||
}
|
||
|
||
// --- Draw ---
|
||
|
||
draw(ctx, node, width, posY, height) {
|
||
this.last_y = posY;
|
||
const h = height ?? ROW_HEIGHT;
|
||
const midY = posY + h * 0.5;
|
||
const margin = ROW_MARGIN;
|
||
|
||
ctx.save();
|
||
ctx.font = "11px monospace";
|
||
ctx.textBaseline = "middle";
|
||
|
||
const totalWidth = width - margin * 2;
|
||
const btnsWidth = BTN_WIDTH * 3 + INNER_MARGIN * 2;
|
||
const dropdownWidth = totalWidth - btnsWidth - INNER_MARGIN;
|
||
|
||
const dropdownX = margin;
|
||
const newX = margin + dropdownWidth + INNER_MARGIN;
|
||
const saveX = newX + BTN_WIDTH + INNER_MARGIN;
|
||
const deleteX = saveX + BTN_WIDTH + INNER_MARGIN;
|
||
|
||
// Dropdown
|
||
ctx.fillStyle = "#222233";
|
||
ctx.beginPath();
|
||
ctx.roundRect(dropdownX, posY + 4, dropdownWidth, h - 8, 4);
|
||
ctx.fill();
|
||
ctx.fillStyle = this.isEmpty() ? COLORS.inactive : COLORS.text;
|
||
ctx.textAlign = "left";
|
||
ctx.fillText(
|
||
this.isEmpty()
|
||
? EMPTY_VALUE
|
||
: fitString(ctx, this.value, dropdownWidth - 16),
|
||
dropdownX + 8,
|
||
midY,
|
||
);
|
||
// Dropdown arrow
|
||
ctx.fillStyle = COLORS.inactive;
|
||
ctx.textAlign = "center";
|
||
ctx.fillText("▾", dropdownX + dropdownWidth - 10, midY);
|
||
this._hitDropdown = [dropdownX, dropdownWidth];
|
||
|
||
// New button
|
||
ctx.fillStyle = "#223322";
|
||
ctx.beginPath();
|
||
ctx.roundRect(newX, posY + 4, BTN_WIDTH, h - 8, 4);
|
||
ctx.fill();
|
||
ctx.fillStyle = COLORS.text;
|
||
ctx.textAlign = "center";
|
||
ctx.fillText("➕", newX + BTN_WIDTH / 2, midY);
|
||
this._hitNew = [newX, BTN_WIDTH];
|
||
|
||
// Save button (disabled if empty)
|
||
ctx.fillStyle = this.isEmpty() ? "#1a1a1a" : "#222233";
|
||
ctx.beginPath();
|
||
ctx.roundRect(saveX, posY + 4, BTN_WIDTH, h - 8, 4);
|
||
ctx.fill();
|
||
ctx.fillStyle = this.isEmpty() ? COLORS.inactive : COLORS.text;
|
||
ctx.fillText(this._saveFlash ?? "💾", saveX + BTN_WIDTH / 2, midY);
|
||
this._hitSave = [saveX, BTN_WIDTH];
|
||
|
||
// Delete button (disabled if empty)
|
||
ctx.fillStyle = this.isEmpty() ? "#1a1a1a" : "#4a1a1a";
|
||
ctx.beginPath();
|
||
ctx.roundRect(deleteX, posY + 4, BTN_WIDTH, h - 8, 4);
|
||
ctx.fill();
|
||
ctx.fillStyle = this.isEmpty() ? COLORS.inactive : "#ff6666";
|
||
ctx.fillText("🗑️", deleteX + BTN_WIDTH / 2, midY);
|
||
this._hitDelete = [deleteX, BTN_WIDTH];
|
||
|
||
ctx.restore();
|
||
}
|
||
|
||
// --- Mouse ---
|
||
|
||
mouse(event, pos, node) {
|
||
const x = pos[0];
|
||
const type = event.type;
|
||
if (type !== "pointerdown" && type !== "mousedown") return false;
|
||
|
||
if (this._inHit(x, this._hitNew)) {
|
||
this.onNew?.();
|
||
return true;
|
||
}
|
||
|
||
if (this._inHit(x, this._hitSave)) {
|
||
if (!this.isEmpty()) this.onSave?.();
|
||
return true;
|
||
}
|
||
|
||
if (this._inHit(x, this._hitDelete)) {
|
||
if (!this.isEmpty()) this.onDelete?.();
|
||
return true;
|
||
}
|
||
|
||
if (this._inHit(x, this._hitDropdown)) {
|
||
this._showDropdown(event, node);
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
// --- Utils ---
|
||
|
||
_inHit(x, hit) {
|
||
return x >= hit[0] && x <= hit[0] + hit[1];
|
||
}
|
||
|
||
// --- Dropdown ---
|
||
|
||
_showDropdown(event, node) {
|
||
if (!this.presets.length) return;
|
||
|
||
const items = this.presets.map((name) => ({
|
||
content: name,
|
||
callback: () => {
|
||
this.value = name;
|
||
this.onSelect?.(name);
|
||
node.setDirtyCanvas(true, true);
|
||
},
|
||
}));
|
||
|
||
new LiteGraph.ContextMenu(items, {
|
||
event,
|
||
title: "Select Preset",
|
||
});
|
||
}
|
||
}
|