From bc966a39b710e362007fcd99c96d42cbbf52d4d0 Mon Sep 17 00:00:00 2001 From: Glary-Bot Date: Tue, 12 May 2026 05:37:32 +0000 Subject: [PATCH] feat: per-widget grid override via node.properties (PoC) Adds a 'grid overrides' concept on node properties keyed by widget input name. Each entry overrides the CSS grid-template-rows track for that widget in the node's widget grid (e.g. '200px', 'minmax(150px, 300px)', '1fr'), letting users size individual textareas/widgets independently of the node's auto-fill layout. Storage: node.properties.gridOverrides[widgetName] = '' (persists with the workflow JSON). Rendering: useProcessedWidgets consults overrides before falling back to the existing shouldExpand/hasLayoutSize default. Testing UI: right-click a node -> 'Widget Grid Sizes' submenu lists each widget; pick one to set or clear a custom track value. --- src/composables/graph/useGraphNodeManager.ts | 28 ++++ src/extensions/core/index.ts | 1 + src/extensions/core/widgetGridOverrides.ts | 131 ++++++++++++++++++ .../composables/useProcessedWidgets.ts | 15 +- 4 files changed, 169 insertions(+), 6 deletions(-) create mode 100644 src/extensions/core/widgetGridOverrides.ts diff --git a/src/composables/graph/useGraphNodeManager.ts b/src/composables/graph/useGraphNodeManager.ts index efcf059518..219d210676 100644 --- a/src/composables/graph/useGraphNodeManager.ts +++ b/src/composables/graph/useGraphNodeManager.ts @@ -99,6 +99,13 @@ export interface SafeWidgetData { promotedLabel?: string } +/** + * Maps widget name -> CSS grid-template-rows track value + * (e.g. '200px', 'minmax(150px, 300px)', '1fr', 'auto'). + * Persisted on `node.properties.gridOverrides`. + */ +export type WidgetGridOverrides = Record + export interface VueNodeData { executing: boolean id: NodeId @@ -115,6 +122,7 @@ export interface VueNodeData { ghost?: boolean pinned?: boolean } + gridOverrides?: WidgetGridOverrides hasErrors?: boolean inputs?: INodeInputSlot[] outputs?: INodeOutputSlot[] @@ -133,6 +141,9 @@ export interface GraphNodeManager { // Access to original LiteGraph nodes (non-reactive) getNode(id: string): LGraphNode | undefined + // Re-extract VueNodeData for fields not covered by tracked-property events + refreshNode(id: string): void + // Lifecycle methods cleanup(): void } @@ -513,12 +524,23 @@ export function extractVueNodeData(node: LGraphNode): VueNodeData { flags: node.flags ? { ...node.flags } : undefined, color: node.color || undefined, bgcolor: node.bgcolor || undefined, + gridOverrides: readGridOverrides(node), resizable: node.resizable, shape: node.shape, showAdvanced: node.showAdvanced } } +function readGridOverrides(node: LGraphNode): WidgetGridOverrides | undefined { + const raw = node.properties?.gridOverrides + if (!raw || typeof raw !== 'object') return undefined + const entries = Object.entries(raw as Record).filter( + (entry): entry is [string, string] => typeof entry[1] === 'string' + ) + if (entries.length === 0) return undefined + return Object.fromEntries(entries) +} + export function useGraphNodeManager(graph: LGraph): GraphNodeManager { // Get layout mutations composable const { createNode, deleteNode, setSource } = useLayoutMutations() @@ -858,9 +880,15 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager { }) } + const refreshNode = (id: string) => { + const nodeRef = nodeRefs.get(id) + if (nodeRef) vueNodeData.set(id, extractVueNodeData(nodeRef)) + } + return { vueNodeData, getNode, + refreshNode, cleanup } } diff --git a/src/extensions/core/index.ts b/src/extensions/core/index.ts index 4cc1c977cf..9e6b8cbbd8 100644 --- a/src/extensions/core/index.ts +++ b/src/extensions/core/index.ts @@ -30,6 +30,7 @@ import './slotDefaults' import './uploadAudio' import './uploadImage' import './webcamCapture' +import './widgetGridOverrides' import './widgetInputs' // Cloud-only extensions - tree-shaken in OSS builds diff --git a/src/extensions/core/widgetGridOverrides.ts b/src/extensions/core/widgetGridOverrides.ts new file mode 100644 index 0000000000..4cdd16e70d --- /dev/null +++ b/src/extensions/core/widgetGridOverrides.ts @@ -0,0 +1,131 @@ +import type { IContextMenuValue } from '@/lib/litegraph/src/interfaces' +import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' +import { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle' +import { app } from '@/scripts/app' +import { useExtensionService } from '@/services/extensionService' + +const PROPERTY_KEY = 'gridOverrides' +const PROMPT_LABEL = + 'Grid row size (e.g. 200px, minmax(150px, 300px), 1fr, auto)' + +type GridOverrides = Record + +function readOverrides(node: LGraphNode): GridOverrides { + const raw = node.properties?.[PROPERTY_KEY] + if (!raw || typeof raw !== 'object') return {} + return { ...(raw as GridOverrides) } +} + +function writeOverrides(node: LGraphNode, next: GridOverrides): void { + if (Object.keys(next).length === 0) { + delete node.properties[PROPERTY_KEY] + } else { + node.properties[PROPERTY_KEY] = next + } + refreshVueNode(String(node.id)) + app.canvas?.setDirty(true, true) +} + +function refreshVueNode(nodeId: string): void { + const manager = useVueNodeLifecycle().nodeManager.value + manager?.refreshNode(nodeId) +} + +function promptForRowSize( + current: string, + onSubmit: (value: string) => void +): void { + const input = window.prompt(PROMPT_LABEL, current) + if (input == null) return + const trimmed = input.trim() + if (trimmed.length === 0) return + onSubmit(trimmed) +} + +function setOverride( + node: LGraphNode, + widgetName: string, + value: string +): void { + const next = readOverrides(node) + next[widgetName] = value + writeOverrides(node, next) +} + +function clearOverride(node: LGraphNode, widgetName: string): void { + const next = readOverrides(node) + delete next[widgetName] + writeOverrides(node, next) +} + +function buildWidgetMenuItem( + node: LGraphNode, + widgetName: string +): IContextMenuValue { + const overrides = readOverrides(node) + const current = overrides[widgetName] + const label = current + ? `${widgetName} → ${current}` + : `${widgetName} → (auto)` + + return { + content: label, + has_submenu: true, + callback: () => { + promptForRowSize(current ?? '200px', (value) => + setOverride(node, widgetName, value) + ) + }, + submenu: { + options: [ + { + content: 'Set size…', + callback: () => { + promptForRowSize(current ?? '200px', (value) => + setOverride(node, widgetName, value) + ) + } + }, + { + content: 'Clear override', + disabled: !current, + callback: () => clearOverride(node, widgetName) + } + ] + } + } +} + +useExtensionService().registerExtension({ + name: 'Comfy.WidgetGridOverrides', + getNodeMenuItems(node: LGraphNode): (IContextMenuValue | null)[] { + const widgets = node.widgets ?? [] + if (widgets.length === 0) return [] + + const overrides = readOverrides(node) + const hasAny = Object.keys(overrides).length > 0 + + const widgetItems: (IContextMenuValue | null)[] = widgets.map((widget) => + buildWidgetMenuItem(node, widget.name) + ) + + if (hasAny) { + widgetItems.push(null, { + content: 'Clear all overrides', + callback: () => writeOverrides(node, {}) + }) + } + + return [ + null, + { + content: 'Widget Grid Sizes', + has_submenu: true, + callback: () => {}, + submenu: { + options: widgetItems + } + } + ] + } +}) diff --git a/src/renderer/extensions/vueNodes/composables/useProcessedWidgets.ts b/src/renderer/extensions/vueNodes/composables/useProcessedWidgets.ts index c1b11ed571..3eb52baa62 100644 --- a/src/renderer/extensions/vueNodes/composables/useProcessedWidgets.ts +++ b/src/renderer/extensions/vueNodes/composables/useProcessedWidgets.ts @@ -412,13 +412,16 @@ export function useProcessedWidgets( ) ) - const gridTemplateRows = computed((): string => - visibleWidgets.value - .map((w) => - shouldExpand(w.type) || w.hasLayoutSize ? 'auto' : 'min-content' - ) + const gridTemplateRows = computed((): string => { + const overrides = nodeDataGetter()?.gridOverrides + return visibleWidgets.value + .map((w) => { + const override = overrides?.[w.name] + if (override) return override + return shouldExpand(w.type) || w.hasLayoutSize ? 'auto' : 'min-content' + }) .join(' ') - ) + }) return { canSelectInputs,