Allow Vue nodes to have their colors changed from selection toolbox (#5720)

## Summary

Fixes https://github.com/Comfy-Org/ComfyUI_frontend/issues/5680 by
allowing Vue nodes to properly synchronize color changes with LiteGraph
nodes, implementing header darkening and light theme adjustments.

<img width="2496" height="1512" alt="Screenshot from 2025-09-21
20-00-36"
src="https://github.com/user-attachments/assets/e3bdf645-1e0b-4d11-9ae5-9401f43e8e96"
/>

## Changes

- **What**: Implemented color property synchronization between LiteGraph
and Vue node rendering systems
- **Core Fix**: Added `nodeData.color` and `nodeData.bgcolor` to [v-memo
dependencies](https://vuejs.org/api/built-in-directives.html#v-memo) to
trigger re-renders on color changes
- **Color Logic**: Added header darkening using [memoized color
adjustments](https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/utils/colorUtil.ts)
to match LiteGraph's ColorOption system
- **Event System**: Enhanced property change instrumentation in
LGraphNode.setColorOption to emit color/bgcolor events

## Review Focus

Vue component reactivity timing - the v-memo fix was critical for
immediate color updates. Verify light theme color adjustments match the
drawNode monkey patch behavior in app.ts.

## Technical Details

```mermaid
graph TD
    A[User Sets Color] --> B[LGraphNode.setColorOption]
    B --> C[Sets node.color & node.bgcolor]
    C --> D[Triggers property:changed events]
    D --> E[Vue Node Manager Updates]
    E --> F[v-memo Detects Change]
    F --> G[NodeHeader Re-renders]
    G --> H[Header Darkening Applied]

    style A fill:#f9f9f9,stroke:#333,color:#000
    style H fill:#f9f9f9,stroke:#333,color:#000
```

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5720-Allow-Vue-nodes-to-have-their-colors-changed-from-selection-toolbox-2766d73d36508123b441d126a74a54b2)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Myestery <Myestery@users.noreply.github.com>
Co-authored-by: DrJKL <DrJKL@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Christian Byrne
2025-09-27 15:12:13 -07:00
committed by GitHub
parent 5b866b74b8
commit a25d89881b
9 changed files with 141 additions and 6 deletions

View File

@@ -0,0 +1,49 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../fixtures/ComfyPage'
test.describe('Vue Node Custom Colors', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.vueNodes.waitForNodes()
})
test('displays color picker button and allows color selection', async ({
comfyPage
}) => {
const loadCheckpointNode = comfyPage.page.locator('[data-node-id]').filter({
hasText: 'Load Checkpoint'
})
await loadCheckpointNode.getByText('Load Checkpoint').click()
await comfyPage.page.locator('.selection-toolbox .pi-circle-fill').click()
await comfyPage.page
.locator('.color-picker-container')
.locator('i[data-testid="blue"]')
.click()
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-node-custom-color-blue.png'
)
})
test('should load node colors from workflow', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('nodes/every_node_color')
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-node-custom-colors-dark-all-colors.png'
)
})
test('should show brightened node colors on light theme', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
await comfyPage.loadWorkflow('nodes/every_node_color')
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-node-custom-colors-light-all-colors.png'
)
})
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

View File

@@ -41,6 +41,8 @@ export interface VueNodeData {
collapsed?: boolean
pinned?: boolean
}
color?: string
bgcolor?: string
}
export interface GraphNodeManager {
@@ -126,7 +128,9 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
widgets: safeWidgets,
inputs: node.inputs ? [...node.inputs] : undefined,
outputs: node.outputs ? [...node.outputs] : undefined,
flags: node.flags ? { ...node.flags } : undefined
flags: node.flags ? { ...node.flags } : undefined,
color: node.color || undefined,
bgcolor: node.bgcolor || undefined
}
}
@@ -449,6 +453,24 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
...currentData,
mode: typeof event.newValue === 'number' ? event.newValue : 0
})
break
case 'color':
vueNodeData.set(nodeId, {
...currentData,
color:
typeof event.newValue === 'string'
? event.newValue
: undefined
})
break
case 'bgcolor':
vueNodeData.set(nodeId, {
...currentData,
bgcolor:
typeof event.newValue === 'string'
? event.newValue
: undefined
})
}
}
} else if (

View File

@@ -7,7 +7,9 @@ const DEFAULT_TRACKED_PROPERTIES: string[] = [
'title',
'flags.collapsed',
'flags.pinned',
'mode'
'mode',
'color',
'bgcolor'
]
/**

View File

@@ -34,7 +34,9 @@
:style="[
{
transform: `translate(${position.x ?? 0}px, ${(position.y ?? 0) - LiteGraph.NODE_TITLE_HEIGHT}px)`,
zIndex: zIndex
zIndex: zIndex,
backgroundColor: nodeBodyBackgroundColor,
opacity: nodeOpacity
},
dragStyle
]"
@@ -47,9 +49,14 @@
<SlotConnectionDot multi class="absolute left-0 -translate-x-1/2" />
<SlotConnectionDot multi class="absolute right-0 translate-x-1/2" />
</template>
<!-- Header only updates on title/color changes -->
<NodeHeader
v-memo="[nodeData.title, isCollapsed, nodeData.flags?.pinned]"
v-memo="[
nodeData.title,
nodeData.color,
nodeData.bgcolor,
isCollapsed,
nodeData.flags?.pinned
]"
:node-data="nodeData"
:readonly="readonly"
:collapsed="isCollapsed"
@@ -139,6 +146,7 @@ import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import { toggleNodeOptions } from '@/composables/graph/useMoreOptionsMenu'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import { TransformStateKey } from '@/renderer/core/layout/injectionKeys'
@@ -148,9 +156,11 @@ import { useVueElementTracking } from '@/renderer/extensions/vueNodes/composable
import { useNodeExecutionState } from '@/renderer/extensions/vueNodes/execution/useNodeExecutionState'
import { useNodeLayout } from '@/renderer/extensions/vueNodes/layout/useNodeLayout'
import { useNodePreviewState } from '@/renderer/extensions/vueNodes/preview/useNodePreviewState'
import { applyLightThemeColor } from '@/renderer/extensions/vueNodes/utils/nodeStyleUtils'
import { app } from '@/scripts/app'
import { useExecutionStore } from '@/stores/executionStore'
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import {
getLocatorIdFromNodeData,
getNodeByLocatorId
@@ -221,6 +231,23 @@ const hasAnyError = computed((): boolean => {
const bypassed = computed((): boolean => nodeData.mode === 4)
const muted = computed((): boolean => nodeData.mode === 2) // NEVER mode
const nodeBodyBackgroundColor = computed(() => {
const colorPaletteStore = useColorPaletteStore()
if (!nodeData.bgcolor) {
return ''
}
return applyLightThemeColor(
nodeData.bgcolor,
Boolean(colorPaletteStore.completedActivePalette.light_theme)
)
})
const nodeOpacity = computed(
() => useSettingStore().get('Comfy.Node.Opacity') ?? 1
)
// Use canvas interactions for proper wheel event handling and pointer event capture control
const { handleWheel, shouldHandleNodePointerEvents } = useCanvasInteractions()

View File

@@ -4,7 +4,8 @@
</div>
<div
v-else
class="lg-node-header p-4 rounded-t-2xl cursor-move"
class="lg-node-header p-4 rounded-t-2xl w-full cursor-move"
:style="headerStyle"
:data-testid="`node-header-${nodeData?.id || ''}`"
@dblclick="handleDoubleClick"
>
@@ -71,8 +72,11 @@ import EditableText from '@/components/common/EditableText.vue'
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { st } from '@/i18n'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
import { applyLightThemeColor } from '@/renderer/extensions/vueNodes/utils/nodeStyleUtils'
import { app } from '@/scripts/app'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { normalizeI18nKey } from '@/utils/formatUtil'
import {
getLocatorIdFromNodeData,
@@ -123,6 +127,23 @@ const tooltipConfig = computed(() => {
return createTooltipConfig(description)
})
const headerStyle = computed(() => {
const colorPaletteStore = useColorPaletteStore()
const opacity = useSettingStore().get('Comfy.Node.Opacity') ?? 1
if (!nodeData?.color) {
return { backgroundColor: '', opacity }
}
const headerColor = applyLightThemeColor(
nodeData.color,
Boolean(colorPaletteStore.completedActivePalette.light_theme)
)
return { backgroundColor: headerColor, opacity }
})
const resolveTitle = (info: VueNodeData | undefined) => {
const title = (info?.title ?? '').trim()
if (title.length > 0) return title

View File

@@ -0,0 +1,14 @@
import { adjustColor } from '@/utils/colorUtil'
/**
* Applies light theme color adjustments to a color
*/
export function applyLightThemeColor(
color: string,
isLightTheme: boolean
): string {
if (!color || !isLightTheme) {
return color
}
return adjustColor(color, { lightness: 0.5 })
}