diff --git a/web/grid_image_pool.js b/web/grid_image_pool.js index 9cb2b71..e49f464 100644 --- a/web/grid_image_pool.js +++ b/web/grid_image_pool.js @@ -16,8 +16,7 @@ const PAD = 4; // .gip-grid padding const TOOLBAR_H = 26; const ROW_H = CELL + GAP; const MAX_ROWS = 4; // beyond this the grid scrolls internally -const COLS = 4; // fixed column count -const MIN_W = 560; // minimum node/grid width (the grey area) +const MIN_W = 560; // default node width (the grey area); freely resizable // ComfyUI insets DOM widgets by DEFAULT_MARGIN (10px) on every side and forces // our element to h-full/w-full of the (computedHeight - 2*MARGIN) box. Reserve // that or the grid eats into the toolbar's space. @@ -113,23 +112,32 @@ function viewUrl(poolId, name, bust) { // size the node to fit them exactly. An explicit column count avoids depending // on ComfyUI's DOM-widget width tracking, which doesn't reliably follow the node // width in frontend 1.45 (columns would otherwise collapse on click). +// Height reservation only. Columns are handled by flex-wrap (the grid wraps to +// however many cells fit the actual width — so it never clips), so we estimate +// rows from the current node width for the height floor. function recomputeSize(node, count) { - const rows = count > 0 ? Math.ceil(count / COLS) : 1; + const width = Math.max(node.size?.[0] || MIN_W, MIN_W); + const inner = width - 2 * MARGIN - 2 * PAD; + const perRow = Math.max(1, Math.floor((inner + GAP) / ROW_H)); + const rows = count > 0 ? Math.ceil(count / perRow) : 1; const cap = MAX_ROWS * ROW_H - GAP + 2 * PAD; const full = count > 0 ? rows * ROW_H - GAP + 2 * PAD : 56; - node._gridGridMax = cap; node._gridWidgetH = 2 * MARGIN + TOOLBAR_H + 6 + Math.min(full, cap); - // node width that fits exactly COLS cells: cells + gaps + grid padding + margins - const content = COLS * CELL + (COLS - 1) * GAP + 2 * PAD; - node._gridWidthWanted = Math.max(MIN_W, content + 2 * MARGIN); } -// Resize to the fixed-column width (exact) and content height (grow-only so a -// manual taller resize is respected between content changes). +// DomWidgets.vue sizes the grid container from `posWidget.width ?? posNode.width`; +// on this version that can lag node.size[0], leaving the grey area narrow. Pin the +// widget's own width to the node width so the grid (flex-wrap) reflows to fill it. +function syncWidgetWidth(node) { + if (node._gridWidget) node._gridWidget.width = node.size?.[0] || MIN_W; +} + +// Grow the node height to fit content; keep the user's width (no width floor, so +// the node resizes down freely). Width is the user's to control. function resizeToContent(node) { - const w = node._gridWidthWanted || MIN_W; const h = node.computeSize()[1]; - node.setSize([w, Math.max(node.size?.[1] || 0, h)]); + node.setSize([node.size?.[0] || MIN_W, Math.max(node.size?.[1] || 0, h)]); + syncWidgetWidth(node); node.setDirtyCanvas(true, true); } @@ -464,14 +472,13 @@ function injectStyles() { min-height:${TOOLBAR_H - 2}px; } .gip-toolbar button { font-size:11px; padding:2px 8px; cursor:pointer; } .gip-count { font-size:11px; opacity:0.7; margin-left:auto; } - /* scrolling body — explicit N columns (set via --gip-cols / inline style) so - the layout never depends on the DOM-widget width tracking */ - .gip-grid { display:grid; grid-template-columns:repeat(${COLS}, ${CELL}px); gap:${GAP}px; - justify-content:start; align-content:start; overflow-y:auto; - padding:${PAD}px; background:rgba(0,0,0,0.15); border-radius:4px; - flex:1 1 auto; min-height:0; } + /* scrolling body — flex-wrap so cells wrap to fit the actual width (never + clips); columns = however many ${CELL}px cells fit */ + .gip-grid { display:flex; flex-wrap:wrap; gap:${GAP}px; align-content:flex-start; + overflow-y:auto; padding:${PAD}px; background:rgba(0,0,0,0.15); + border-radius:4px; flex:1 1 auto; min-height:0; } .gip-grid.gip-dragover { outline:2px dashed #6cf; outline-offset:-2px; } - .gip-empty { font-size:12px; opacity:0.6; padding:12px; grid-column:1 / -1; text-align:center; } + .gip-empty { font-size:12px; opacity:0.6; padding:12px; width:100%; text-align:center; } .gip-cell { position:relative; width:${CELL}px; height:${CELL}px; border:2px solid transparent; border-radius:4px; overflow:hidden; background:#222; transition:border-color .1s; } .gip-cell:hover { border-color:#555; } @@ -563,11 +570,21 @@ function setupGridNode(node) { // Pinning a max (or overriding widget.computeSize) locks the widget to a fixed // size while the node frame keeps resizing — they diverge and the grid appears // to detach / stop expanding on click. - node.addDOMWidget("grid", "div", wrap, { + node._gridWidget = node.addDOMWidget("grid", "div", wrap, { serialize: false, getMinHeight: () => node._gridWidgetH || 120, }); + // on manual resize: keep the DOM widget width synced (so the flex-wrap grid + // reflows to fill) and re-estimate the height floor for the new width + const onResize = node.onResize; + node.onResize = function () { + const r = onResize?.apply(this, arguments); + recomputeSize(node, node._lastCount || 0); + syncWidgetWidth(node); + return r; + }; + wireIngest(node, grid, uploadBtn, fileInput); // a refresh that also updates the count label @@ -577,23 +594,12 @@ function setupGridNode(node) { if (node._countEl) node._countEl.textContent = `${n} image${n === 1 ? "" : "s"}`; }; - // ComfyUI snaps the node to computeSize() on selection, and computeSize's WIDTH - // ignores DOM widgets — it collapses to ~the title/index width, clipping our - // grid on click. Floor the computed width to the column-fit width so the snap - // keeps the node wide enough. (Only width is touched; the height path through - // computeLayoutSize is untouched, so the DOM overlay stays in sync.) - const origComputeSize = node.computeSize; - node.computeSize = function (out) { - const sz = origComputeSize ? origComputeSize.call(this, out) : [MIN_W, 120]; - sz[0] = Math.max(sz[0], node._gridWidthWanted || MIN_W); - return sz; - }; - - // initial width (fits COLS columns) + content-driven height; the first refresh - // resizes once if the pool already has images + // initial width (a sensible wide default) + content-driven height; the node + // stays freely resizable (no width floor) and the grid flex-wraps to fit. node._lastCount = 0; recomputeSize(node, 0); - node.setSize([node._gridWidthWanted || MIN_W, node.computeSize()[1]]); + node.setSize([Math.max(node.size?.[0] || 0, MIN_W), node.computeSize()[1]]); + syncWidgetWidth(node); node._gridRefresh(); }