Files
ausboss-ComfyUI-AusBoss/js/shared/panel_layout.mjs
T
ausboss 2bc842bd41 Workflow Note: grow to fit its text in the classic canvas
A note saved a few pixels shorter than its content showed a scrollbar and
hid its last row. The LTX 2.3 Video Outpaint note in the smaller example
layout (440 x 810) was 6 px short, so its links sat behind the scrollbar.
The classic canvas keeps a node's saved height; Nodes 2.0 already sized
the note to its content.

The note now measures its card's natural height at the node's width and
grows the node to fit when a workflow opens, after an edit, after the file
check, and when the node's width changes, since the text rewraps. It never
shrinks the node. The load fit runs before ComfyUI snapshots the opened
workflow, so a note that grows on load does not mark the workflow modified.
A corner drag stops at the fit, as it does in Nodes 2.0: holdNodeMinHeight
lifts the node's computeSize height to the measured floor. A card less
than half a pixel short is left alone, because Chrome rounds that away and
draws no scrollbar. Nodes 2.0 is untouched.
2026-09-29 10:50:57 -05:00

132 lines
6.1 KiB
JavaScript

// How a DOM panel claims its share of a node's height. No DOM and no
// ComfyUI imports in here, so it stays testable under node:test.
//
// The frontend arranges a node's widgets in one pass
// (LGraphNode._arrangeWidgets):
//
// if (w.computeSize) -> fixed height, kept OUT of the split
// else if (w.computeLayoutSize) -> joins distributeSpace(freeSpace, ...)
// else -> one standard widget row
//
// It is an else-if, so a widget declaring BOTH is pinned by computeSize and
// its computeLayoutSize is never called. Every stage, player and filmstrip in
// this pack derived that fixed height from the node's WIDTH, which is why
// dragging a node taller only added dead space underneath: the panel had
// already been given a height and excluded from the leftover-space split.
//
// The frontend mounts a DOM widget's element inside a frame: DomWidgets.vue
// insets it by `options.margin` per side (default 10), so the element gets
// 20 fewer CSS pixels of height than the layout hands the widget. Any floor
// meant to guarantee room for fixed-height content must add this allowance,
// or the panel's bottom edge renders clipped flat - which is how the LoRA
// stack's rounded bottom border once went missing.
export const WIDGET_FRAME = 20;
// The frontend pads a DOM widget's minWidth before it becomes the node's
// floor: LGraphNode.computeSize adds the room a number widget's value box
// takes (BaseWidget.minValueWidth plus both arrows and margins - 104px on
// frontend 1.53), and a corner drag never goes below computeSize. So a
// panel declaring 320 cannot be dragged under 424, and a node that opened
// at 340 jumps to 444 the moment its corner is touched. This measures that
// padding from the frontend itself instead of copying its constants: a
// probe minWidth goes in, and whatever computeSize adds on top comes out.
// null when it cannot be measured right now (the widget is hidden, or the
// node cannot size itself), so the caller tries again later.
const PROBE_WIDTH = 100000;
let probing = false;
export function measureLayoutWidthPadding(node, widget) {
if (probing || typeof node?.computeSize !== "function" || !widget) return null;
const own = widget.computeLayoutSize;
probing = true;
widget.computeLayoutSize = () => ({ minWidth: PROBE_WIDTH, minHeight: 0 });
let width = NaN;
try {
width = Number(node.computeSize()?.[0]);
} catch {
// A node that cannot size itself right now is measured next time.
} finally {
widget.computeLayoutSize = own;
probing = false;
}
return Number.isFinite(width) && width >= PROBE_WIDTH ? width - PROBE_WIDTH : null;
}
// The padding is the frontend's, the same for every node: measured once.
let layoutWidthPadding = null;
// distributeSpace reads a missing maxSize as Infinity, so declaring a floor
// with no ceiling means "take whatever is left" - which is exactly "fill the
// node". minWidth/minHeight accept a number or a function, for panels whose
// floor depends on state (the frame chooser is shorter until it has frames).
// exactMinWidth: true makes minWidth the node's real floor (see above);
// without it the frontend's padding comes on top, as it always has.
export function fillNodeHeight(widget, { minWidth = 0, minHeight = 0, minNodeSize, exactMinWidth = false } = {}) {
if (!widget) return widget;
const floor = (value) => {
const resolved = Number(typeof value === "function" ? value() : value);
return Number.isFinite(resolved) ? Math.max(0, resolved) : 0;
};
const padding = (node) => {
if (!exactMinWidth) return 0;
if (layoutWidthPadding === null) {
layoutWidthPadding = measureLayoutWidthPadding(node ?? widget.node, widget);
}
return layoutWidthPadding ?? 0;
};
// Deleted, not overwritten: any own computeSize would win the else-if above.
delete widget.computeSize;
widget.computeLayoutSize = (node) => ({
minWidth: Math.max(0, floor(minWidth) - padding(node)),
minHeight: floor(minHeight),
});
widget.options ??= {};
if (minNodeSize) widget.options.minNodeSize = minNodeSize;
return widget;
}
// Nodes 2.0 (the Vue renderer) has no layout API for a node's minimum
// width: its corner drag stops at the node element's inline min-width, and
// at 225px when there is none. Lend it the panel's floor whenever the panel
// is laid out inside a Vue node. The classic renderer mounts panels outside
// any [data-node-id] element, so there this does nothing.
export function holdVueNodeMinWidth(panel, width) {
const host = panel?.closest?.("[data-node-id]");
const value = Number(width);
if (!host?.style || !Number.isFinite(value) || value <= 0) return false;
const css = `${Math.round(value)}px`;
if (host.style.minWidth === css) return false;
host.style.minWidth = css;
return true;
}
// Grow a node to the height its widgets ask for. A workflow saved before a
// panel or card existed carries the node's old size, and the frontend keeps
// that size on load - the new panel is then squeezed under its floor and
// clipped flat. Returns true when the node was resized.
export function ensureNodeMinHeight(node) {
const min = Number(node?.computeSize?.()?.[1]);
const current = Number(node?.size?.[1]);
if (!Number.isFinite(min) || !Number.isFinite(current) || current >= min) return false;
node.setSize?.([node.size[0], min]);
return true;
}
// Lift a node's computeSize() height to a floor the panel measures, such as
// the height a card's text needs. A corner drag stops at computeSize(), so
// the node cannot be dragged shorter than that. The floor is read on every
// call; anything but a finite number leaves the frontend's own height.
// This wraps the node's own method, not the prototype and not the widget,
// so the panel still takes all the height above its floor.
export function holdNodeMinHeight(node, floor) {
const own = node?.computeSize;
if (typeof own !== "function" || typeof floor !== "function") return node;
node.computeSize = function (...args) {
const size = own.apply(this, args);
const min = Number(floor());
if (size && Number.isFinite(min) && min > size[1]) size[1] = min;
return size;
};
return node;
}