mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-17 09:18:26 +00:00
## Summary Refactor node footer from absolute overlay to inline flow layout, fixing the selection bounding box not encompassing footer buttons and collapsed node dimensions. ## Background The node footer (Enter Subgraph, Advanced, Error buttons) was rendered as an absolute overlay (`absolute top-full`) outside the node body. This caused: 1. **Selection bounding box** did not include footer height — the dashed multi-select border cut through footer buttons 2. **Footer offset compensation** required 3 hardcoded computed classes (`footerStateOutlineBottomClass`, `footerRootBorderBottomClass`, `footerResizeHandleBottomClass`) with magic pixel values (31px, 35px, etc.) that had to stay in sync with CSS ## Solution: Inline Footer with `isolate -z-1` The footer is moved into normal document flow (no longer `absolute top-full`). The key challenge was keeping the footer visually behind the body's rounded bottom edge (the "tuck under" effect) without adding `z-index` to the body — because adding `z-index` to the body creates a stacking context that traps slot connection dots, making them appear behind overlay borders. The solution uses CSS `isolation: isolate` combined with `-z-1` on the footer wrapper: - **`isolate`** creates an independent stacking context for the footer, so internal z-index (Error button `z-10` above Enter button) does not leak to the parent - **`-z-1`** places the entire footer behind the body (`z-index: auto`), achieving the visual overlap without touching the body's stacking behavior - **Slot dots remain free** — the body has no explicit z-index, so slots participate in the root stacking context and are never trapped behind overlay borders This eliminates all 3 footer offset computed classes and their hardcoded pixel values. ## Selection Box: `min-height` on root + unified size path Moving `min-h-(--node-height)` from the body (`node-inner-wrapper`) to the root element makes the footer height naturally included in `node.size` via ResizeObserver → layoutStore → litegraph sync. This means `boundingRect` is automatically correct for expanded nodes — no callbacks or overrides needed. For collapsed nodes, a pre-existing issue (since v1.40) caused `_collapsed_width` to fall back to `NODE_COLLAPSED_WIDTH = 80px` because Vue nodes lack a canvas context for text measurement. The fix lets collapsed dimensions flow through the **same** `batchUpdateNodeBounds` path as expanded nodes — no parallel data structure, no separate accessor, no cache: 1. ResizeObserver writes the collapsed DOM dimensions to `layoutStore.size` via `batchUpdateNodeBounds` 2. `useLayoutSync` syncs `layoutStore.size` → `liteNode.size` as it does for any other size change 3. The expanded size survives the collapse→expand round trip via CSS custom properties — the `isCollapsed` watcher in `LGraphNode.vue` swaps `--node-width` to `--node-width-x` on collapse and restores it on expand 4. `measure()` reads `this.size` directly for Vue collapsed nodes via a one-line gate: `if (!this.flags?.collapsed || LiteGraph.vueNodesMode)`. Legacy behavior is unchanged. ## Changes - **NodeFooter.vue**: `absolute top-full` overlay → inline flow with `isolate -z-1` wrappers, Error/Enter button layering via `-mr-5` + DOM order, reactive props destructuring, static `RADIUS_CLASS` lookup for Tailwind scanning, Vue 3.3+ `defineEmits` property syntax - **LGraphNode.vue**: Move `min-h-(--node-height)` from body to root; remove `footerStateOutlineBottomClass`, `footerRootBorderBottomClass`, `footerResizeHandleBottomClass`, `hasFooter` computed; replace dynamic `beforeShapeClass` interpolation with static `bypassOverlayClass`/`mutedOverlayClass` computeds for Tailwind scanning - **LGraphNode.ts**: `measure()` collapsed branch gated by `|| LiteGraph.vueNodesMode` — Vue mode defers to `this.size`; legacy path unchanged - **useVueNodeResizeTracking.ts**: Collapsed and expanded nodes both flow through `batchUpdateNodeBounds`; narrowed `useVueElementTracking` parameter from `MaybeRefOrGetter<string>` to `string`; `deferredElements.delete(element)` on unmount to prevent memory retention - **selectionBorder.ts**: Unchanged — `createBounds` just works because `boundingRect` is now correct - **12 parameterized E2E tests**: Vue mode (subgraph/regular × expanded/collapsed × bottom-left/bottom-right) + legacy mode (expanded/collapsed × bottom-left/bottom-right), driven by `keyboard.collapse()` (Alt+C) - **Unit tests**: `measure()` branching (legacy fallback, Vue `this.size` usage, expanded parity) - **Shared test helpers**: `repositionNodes`, `KeyboardHelper.collapse`, `measureSelectionBounds`, `assertSelectionEncompassesNodes` ## Review Focus - `isolate -z-1` CSS layering pattern — is this acceptable long-term? - `measure()` collapsed branch gated on `LiteGraph.vueNodesMode` — one-line gate to avoid the canvas-ctx-less fallback in Vue mode - Footer button overlap design (`-mr-5` with DOM order for painting) ## Screenshots <img width="1392" height="800" alt="image" src="https://github.com/user-attachments/assets/abaebff5-bb8c-4b5b-8734-8d44fdee4cb9" /> <img width="1493" height="872" alt="image" src="https://github.com/user-attachments/assets/6b9c77f9-e3ae-4d4e-81dc-acfa9a24c768" /> <img width="813" height="515" alt="image" src="https://github.com/user-attachments/assets/ce15bafb-e157-408c-971b-a650088f316a" /> <img width="1031" height="669" alt="image" src="https://github.com/user-attachments/assets/20fdc336-4bc2-4d47-ab7e-c0cbcee0d150" /> <img width="753" height="525" alt="image" src="https://github.com/user-attachments/assets/2dccbe31-7d18-49bc-9ed4-158b1659fddf" /> <img width="730" height="370" alt="image" src="https://github.com/user-attachments/assets/ab87edfa-a4b4-46f7-86ae-4965a4509b42" /> <img width="1132" height="465" alt="image" src="https://github.com/user-attachments/assets/54643f5b-4a31-4c3d-9475-c433f87aedb0" /> <img width="1102" height="449" alt="image" src="https://github.com/user-attachments/assets/9c045df3-e1f5-481e-b1cb-ead1db1626f5" /> --------- Co-authored-by: github-actions <github-actions@github.com>
73 lines
2.0 KiB
TypeScript
73 lines
2.0 KiB
TypeScript
import { SELECTION_BOUNDS_PADDING } from '@/base/common/selectionBounds'
|
|
import type { LGraphCanvas, Rectangle } from '@/lib/litegraph/src/litegraph'
|
|
import { createBounds } from '@/lib/litegraph/src/litegraph'
|
|
import { app } from '@/scripts/app'
|
|
|
|
/**
|
|
* Draws a dashed border around selected items that maintains constant pixel size
|
|
* regardless of zoom level, similar to the DOM selection overlay.
|
|
*/
|
|
function drawSelectionBorder(
|
|
ctx: CanvasRenderingContext2D,
|
|
canvas: LGraphCanvas
|
|
) {
|
|
const selectedItems = canvas.selectedItems
|
|
|
|
// Only draw if multiple items selected
|
|
if (selectedItems.size <= 1) return
|
|
|
|
// Use the same bounds calculation as the toolbox
|
|
const bounds = createBounds(selectedItems, SELECTION_BOUNDS_PADDING)
|
|
if (!bounds) return
|
|
|
|
const [x, y, width, height] = bounds
|
|
|
|
// Save context state
|
|
ctx.save()
|
|
|
|
// Set up dashed line style that doesn't scale with zoom
|
|
const borderWidth = 2 / canvas.ds.scale // Constant 2px regardless of zoom
|
|
ctx.lineWidth = borderWidth
|
|
ctx.strokeStyle =
|
|
getComputedStyle(document.documentElement)
|
|
.getPropertyValue('--border-color')
|
|
.trim() || '#ffffff66'
|
|
|
|
// Create dash pattern that maintains visual size
|
|
const dashSize = 5 / canvas.ds.scale
|
|
ctx.setLineDash([dashSize, dashSize])
|
|
|
|
// Draw the border using the bounds directly
|
|
ctx.beginPath()
|
|
ctx.roundRect(x, y, width, height, 8 / canvas.ds.scale)
|
|
ctx.stroke()
|
|
|
|
// Restore context
|
|
ctx.restore()
|
|
}
|
|
|
|
/**
|
|
* Extension that adds a dashed selection border for multiple selected nodes
|
|
*/
|
|
const ext = {
|
|
name: 'Comfy.SelectionBorder',
|
|
|
|
async init() {
|
|
// Hook into the canvas drawing
|
|
const originalDrawForeground = app.canvas.onDrawForeground
|
|
|
|
app.canvas.onDrawForeground = function (
|
|
ctx: CanvasRenderingContext2D,
|
|
visibleArea: Rectangle
|
|
) {
|
|
// Call original if it exists
|
|
originalDrawForeground?.call(this, ctx, visibleArea)
|
|
|
|
// Draw our selection border
|
|
drawSelectionBorder(ctx, app.canvas)
|
|
}
|
|
}
|
|
}
|
|
|
|
app.registerExtension(ext)
|