Files
olivv-cs-ComfyUI-FunPack/app/shell/menubar.js
T
digital-garbage 33eeff08e3 app: the dials on the main window, and a way to have two projects
Steps, Sampler and Scheduler are on the main window -- the three v4 buried
several clicks deep in Engine Settings. No new UI: the sampler slot declares
roles the way the prompt and the size already do.

What made that nearly free is that the controls now come from the NODE's own
description, through the settingFor/rendererFor pair the pipeline window has
always used. So a combo is a dropdown of the real choices and a number obeys
the real bounds, and prompt.js lost both of its hand-rolled branches: it no
longer decides what a control looks like, it asks.

File: New project, and every project listed with the open one ticked. The
store has been able to do this since the day it arrived and there was no way
in -- 20 lines of menu.

Three faults found on the way, all in the shared input helper:

* ENTER COULD NOT BE TYPED IN A TEXTAREA. `wire()` bound Enter to commit for
  every text control, textareas included, so a multi-line value was
  impossible -- the prompt above all, which is the longest multi-line thing
  in the app. It has been that way in the pipeline window since it was
  written; nobody had typed a second line into one.
* A control committed on blur alone, so autofill, a password manager and any
  script setting a value went unheard. It listens to `change` as well now,
  and commit() only acts on a real difference so the pair costs nothing.
* A control commits on blur, which means text still being typed is not in
  the project yet -- and a tab closing is exactly when that is lost. boot
  blurs before it saves.
2026-09-06 12:18:33 +03:00

111 lines
4.1 KiB
JavaScript

// The menu bar: who this is, what is not a zone, and whether the server is
// there.
//
// v4's arrangement, and the reason an app with only zones reads as unfinished:
// a zone head holds what acts on THAT zone, so anything that acts on the app --
// a window, a preference, the state of the connection -- has nowhere to live
// unless there is a bar that belongs to the app itself.
//
// Only menus with something real in them exist. An empty File menu is a promise
// the app does not keep, and this project's rule for an inapplicable feature is
// that it is absent rather than greyed.
import { composer } from "../composer/composer.js";
import { LIVE, LOST } from "./run.js";
import { services } from "./services.js";
const THEMES = [
{ id: "dark", label: "Dark" },
{ id: "light", label: "Light" },
{ id: "auto", label: "Auto", hint: "Follows the system" },
];
/** A dot and a word for whether ComfyUI is still on the other end. */
export function createConnection() {
const dot = composer.dot.default({ tone: "neutral", label: "ComfyUI" });
const text = composer.text.xs({ text: "Connecting" });
const SAID = {
[LIVE]: { tone: "good", words: "ComfyUI live" },
[LOST]: { tone: "danger", words: "Reconnecting" },
};
return {
/** Two handles, not a wrapper: the bar it goes in already lays out a row. */
items: [dot, text],
dot, text,
/** Reads the same run state everything else does. */
draw(state) {
const said = SAID[state && state.connection] || { tone: "neutral", words: "Connecting" };
dot.node.className = `cx-dot cx-dot-${said.tone}`;
dot.node.setAttribute("aria-label", said.words);
text.setText(said.words);
},
};
}
/**
* menu(label, items, onPick) -> a button that opens them.
*
* The menu is built on each press rather than kept: what is in it depends on
* what is true when it is opened -- which panel is showing, which theme is on
* -- and a menu built once is a menu that goes stale the first time anything
* changes underneath it.
*/
function menu(label, itemsOf, onPick) {
let open = null;
const button = composer.button.sm({
label, tone: "ghost",
onClick: () => {
if (open) { open.close("toggle"); open = null; return; }
open = composer.menu.dropdown({
anchor: button, side: "bottom", align: "start",
items: itemsOf(),
onPick: (id) => { open = null; onPick(id); },
onClose: () => { open = null; },
});
},
});
return button;
}
export function createMenubar({ workspace, onPipeline, onProject, projects = () => [],
current = () => null, theme = services.theme } = {}) {
const connection = createConnection();
const file = menu("File", () => [
{ id: "new", label: "New project…" },
...(projects().length ? [{ separator: true }] : []),
// A tick rather than leaving the current one out: which project is open is
// a fact about the app, and a menu that hides it makes you press one to find out.
...projects().map((p) => ({ id: p.id, label: p.name, icon: p.id === current() ? "✓" : " " })),
], (id) => { if (onProject) onProject(id); });
const view = menu("View", () => [
{ id: "left", label: `${workspace && workspace.isOpen("left") ? "Hide" : "Show"} Assets` },
{ id: "right", label: `${workspace && workspace.isOpen("right") ? "Hide" : "Show"} Properties` },
{ separator: true },
...THEMES.map((t) => ({
...t,
// A tick rather than a disabled row: which one is on is a fact about the
// app, and a menu that hides it makes the user press one to find out.
icon: theme.get() === t.id ? "✓" : " ",
})),
], (id) => {
if (id === "left" || id === "right") workspace.toggle(id);
else theme.set(id);
});
const settings = menu("Settings", () => [
{ id: "pipeline", label: "Models and pipeline…" },
], (id) => { if (id === "pipeline" && onPipeline) onPipeline(); });
const bar = composer.toolbar.default({
label: "FunPack",
items: [composer.brand.default({ name: "FunPack" }), file, view, settings],
trailing: connection.items,
});
return { node: bar.node, bar, connection, file, view, settings };
}