mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-25 16:59:45 +00:00
feat: add gradient-slider widget for FLOAT inputs (#8992)
## Summary Add a new 'gradient-slider' display mode for FLOAT widget inputs. Nodes can specify gradient_stops (color stop arrays) to render a colored gradient track behind the slider thumb, useful for color adjustment parameters like hue, saturation, brightness, etc. - GradientSlider.vue: reusable Reka UI-based gradient slider component - GradientSliderWidget.ts: litegraph canvas-mode fallback rendering - WidgetInputNumberGradientSlider.vue: Vue node widget integration - Schema, registry, and type updates for gradient-slider support this is prerequisite for color correct and balance BE changes https://github.com/Comfy-Org/ComfyUI/pull/12536 ## Screenshots (if applicable) <img width="610" height="237" alt="image" src="https://github.com/user-attachments/assets/b0577ca8-8576-4062-8f14-0a3612e56242" /> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8992-feat-add-gradient-slider-widget-for-FLOAT-inputs-30d6d73d36508199b3e8db6a0c213ab4) by [Unito](https://www.unito.io) --------- Co-authored-by: Alexander Brown <drjkl@comfy.org>
This commit is contained in:
107
src/extensions/core/customWidgets.test.ts
Normal file
107
src/extensions/core/customWidgets.test.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
describe('PrimitiveFloat widget type bridging', () => {
|
||||
function createMockNodeAndWidget() {
|
||||
const properties: Record<string, unknown> = {}
|
||||
const options: Record<string, unknown> = {
|
||||
min: -Infinity,
|
||||
max: Infinity
|
||||
}
|
||||
const widget = { type: 'number', options, value: 0, callback: undefined }
|
||||
return { properties, widget }
|
||||
}
|
||||
|
||||
function applyFloatPropertyBridges(
|
||||
properties: Record<string, unknown>,
|
||||
widget: { type: string; options: Record<string, unknown> }
|
||||
) {
|
||||
const DISPLAY_WIDGET_TYPES = new Set(['gradientslider', 'slider', 'knob'])
|
||||
|
||||
let baseType = widget.type
|
||||
Object.defineProperty(widget, 'type', {
|
||||
get: () => {
|
||||
const display = properties.display as string | undefined
|
||||
if (display && DISPLAY_WIDGET_TYPES.has(display)) return display
|
||||
return baseType
|
||||
},
|
||||
set: (v: string) => {
|
||||
baseType = v
|
||||
}
|
||||
})
|
||||
|
||||
Object.defineProperty(widget.options, 'gradient_stops', {
|
||||
get: () => properties.gradient_stops,
|
||||
set: (v) => {
|
||||
properties.gradient_stops = v
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
it('returns base type when display property is not set', () => {
|
||||
const { properties, widget } = createMockNodeAndWidget()
|
||||
applyFloatPropertyBridges(properties, widget)
|
||||
|
||||
expect(widget.type).toBe('number')
|
||||
})
|
||||
|
||||
it('returns gradientslider when display property is set', () => {
|
||||
const { properties, widget } = createMockNodeAndWidget()
|
||||
applyFloatPropertyBridges(properties, widget)
|
||||
|
||||
properties.display = 'gradientslider'
|
||||
expect(widget.type).toBe('gradientslider')
|
||||
})
|
||||
|
||||
it('returns slider when display property is slider', () => {
|
||||
const { properties, widget } = createMockNodeAndWidget()
|
||||
applyFloatPropertyBridges(properties, widget)
|
||||
|
||||
properties.display = 'slider'
|
||||
expect(widget.type).toBe('slider')
|
||||
})
|
||||
|
||||
it('returns base type for unknown display values', () => {
|
||||
const { properties, widget } = createMockNodeAndWidget()
|
||||
applyFloatPropertyBridges(properties, widget)
|
||||
|
||||
properties.display = 'unknown'
|
||||
expect(widget.type).toBe('number')
|
||||
})
|
||||
|
||||
it('bridges gradient_stops from properties to options', () => {
|
||||
const { properties, widget } = createMockNodeAndWidget()
|
||||
applyFloatPropertyBridges(properties, widget)
|
||||
|
||||
expect(widget.options.gradient_stops).toBeUndefined()
|
||||
|
||||
const stops = [
|
||||
{ offset: 0, color: [255, 0, 0] },
|
||||
{ offset: 1, color: [0, 0, 255] }
|
||||
]
|
||||
properties.gradient_stops = stops
|
||||
expect(widget.options.gradient_stops).toBe(stops)
|
||||
})
|
||||
|
||||
it('writes gradient_stops back to properties', () => {
|
||||
const { properties, widget } = createMockNodeAndWidget()
|
||||
applyFloatPropertyBridges(properties, widget)
|
||||
|
||||
const stops = [
|
||||
{ offset: 0, color: [0, 0, 0] },
|
||||
{ offset: 1, color: [255, 255, 255] }
|
||||
]
|
||||
widget.options.gradient_stops = stops
|
||||
expect(properties.gradient_stops).toBe(stops)
|
||||
})
|
||||
|
||||
it('allows type to be set and overridden', () => {
|
||||
const { properties, widget } = createMockNodeAndWidget()
|
||||
applyFloatPropertyBridges(properties, widget)
|
||||
|
||||
widget.type = 'slider'
|
||||
expect(widget.type).toBe('slider')
|
||||
|
||||
properties.display = 'gradientslider'
|
||||
expect(widget.type).toBe('gradientslider')
|
||||
})
|
||||
})
|
||||
@@ -141,10 +141,30 @@ function onCustomIntCreated(this: LGraphNode) {
|
||||
}
|
||||
})
|
||||
}
|
||||
const DISPLAY_WIDGET_TYPES = new Set(['gradientslider', 'slider', 'knob'])
|
||||
|
||||
function onCustomFloatCreated(this: LGraphNode) {
|
||||
const valueWidget = this.widgets?.[0]
|
||||
if (!valueWidget) return
|
||||
|
||||
let baseType = valueWidget.type
|
||||
Object.defineProperty(valueWidget, 'type', {
|
||||
get: () => {
|
||||
const display = this.properties.display as string | undefined
|
||||
if (display && DISPLAY_WIDGET_TYPES.has(display)) return display
|
||||
return baseType
|
||||
},
|
||||
set: (v: string) => {
|
||||
baseType = v
|
||||
}
|
||||
})
|
||||
|
||||
Object.defineProperty(valueWidget.options, 'gradient_stops', {
|
||||
get: () => this.properties.gradient_stops,
|
||||
set: (v) => {
|
||||
this.properties.gradient_stops = v
|
||||
}
|
||||
})
|
||||
Object.defineProperty(valueWidget.options, 'min', {
|
||||
get: () => this.properties.min ?? -Infinity,
|
||||
set: (v) => {
|
||||
|
||||
Reference in New Issue
Block a user