mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-22 21:38:52 +00:00
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] = '<css-track>' (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.
This commit is contained in:
@@ -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<string, string>
|
||||
|
||||
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<string, unknown>).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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import './slotDefaults'
|
||||
import './uploadAudio'
|
||||
import './uploadImage'
|
||||
import './webcamCapture'
|
||||
import './widgetGridOverrides'
|
||||
import './widgetInputs'
|
||||
|
||||
// Cloud-only extensions - tree-shaken in OSS builds
|
||||
|
||||
131
src/extensions/core/widgetGridOverrides.ts
Normal file
131
src/extensions/core/widgetGridOverrides.ts
Normal file
@@ -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<string, string>
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user