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:
Terry Jia
2026-02-20 20:51:10 -05:00
committed by GitHub
parent 4103379901
commit f7a83f6dfa
14 changed files with 590 additions and 24 deletions

View File

@@ -6,6 +6,7 @@ import type {
SimplifiedWidget
} from '@/types/simplifiedWidget'
import WidgetInputNumberGradientSlider from './WidgetInputNumberGradientSlider.vue'
import WidgetInputNumberInput from './WidgetInputNumberInput.vue'
import WidgetInputNumberSlider from './WidgetInputNumberSlider.vue'
import WidgetWithControl from './WidgetWithControl.vue'
@@ -16,28 +17,33 @@ const props = defineProps<{
const modelValue = defineModel<number>({ default: 0 })
const hasControlAfterGenerate = computed(() => {
return !!props.widget.controlWidget
const controlWidget = computed<SimplifiedControlWidget<number> | null>(() =>
props.widget.controlWidget
? (props.widget as SimplifiedControlWidget<number>)
: null
)
const widgetComponent = computed(() => {
switch (props.widget.type) {
case 'gradientslider':
return WidgetInputNumberGradientSlider
case 'slider':
return WidgetInputNumberSlider
default:
return WidgetInputNumberInput
}
})
</script>
<template>
<WidgetWithControl
v-if="hasControlAfterGenerate"
v-if="controlWidget"
v-model="modelValue"
:widget="widget as SimplifiedControlWidget<number>"
:component="
widget.type === 'slider'
? WidgetInputNumberSlider
: WidgetInputNumberInput
"
:widget="controlWidget"
:component="widgetComponent"
/>
<component
:is="
widget.type === 'slider'
? WidgetInputNumberSlider
: WidgetInputNumberInput
"
:is="widgetComponent"
v-else
v-model="modelValue"
:widget="widget"

View File

@@ -0,0 +1,93 @@
<template>
<WidgetLayoutField :widget="widget">
<div :class="cn(WidgetInputBaseClass, 'flex items-center gap-2 pl-3 pr-2')">
<GradientSlider
v-model="modelValue"
:stops="gradientStops"
:min="widget.options?.min ?? 0"
:max="widget.options?.max ?? 100"
:step="stepValue"
:disabled="widget.options?.disabled"
:aria-label="widget.name"
class="flex-1 min-w-0"
/>
<InputNumber
:key="timesEmptied"
:model-value="modelValue"
v-bind="filteredProps"
:step="stepValue"
:min-fraction-digits="precision"
:max-fraction-digits="precision"
:aria-label="widget.name"
size="small"
pt:pc-input-text:root="min-w-[4ch] bg-transparent border-none text-center truncate"
class="w-16 shrink-0"
:pt="numberPt"
@update:model-value="handleNumberInputUpdate"
/>
</div>
</WidgetLayoutField>
</template>
<script setup lang="ts">
import InputNumber from 'primevue/inputnumber'
import { computed, ref } from 'vue'
import GradientSlider from '@/components/gradientslider/GradientSlider.vue'
import type { ColorStop } from '@/lib/litegraph/src/interfaces'
import type { IWidgetGradientSliderOptions } from '@/lib/litegraph/src/types/widgets'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import { cn } from '@/utils/tailwindUtil'
import {
STANDARD_EXCLUDED_PROPS,
filterWidgetProps
} from '@/utils/widgetPropFilter'
import { useNumberStepCalculation } from '../composables/useNumberStepCalculation'
import { useNumberWidgetButtonPt } from '../composables/useNumberWidgetButtonPt'
import { WidgetInputBaseClass } from './layout'
import WidgetLayoutField from './layout/WidgetLayoutField.vue'
const DEFAULT_GRADIENT_STOPS: ColorStop[] = [
{ offset: 0, color: [0, 0, 0] },
{ offset: 1, color: [255, 255, 255] }
]
const { widget } = defineProps<{
widget: SimplifiedWidget<number, IWidgetGradientSliderOptions>
}>()
const modelValue = defineModel<number>({ default: 0 })
const timesEmptied = ref(0)
const handleNumberInputUpdate = (newValue: number | undefined) => {
if (newValue !== undefined) {
modelValue.value = newValue
return
}
timesEmptied.value += 1
}
const gradientStops = computed<ColorStop[]>(() => {
const stops = widget.options?.gradient_stops
if (stops && stops.length >= 2) return stops
return DEFAULT_GRADIENT_STOPS
})
const filteredProps = computed(() =>
filterWidgetProps(widget.options, STANDARD_EXCLUDED_PROPS)
)
const precision = computed(() => {
const p = widget.options?.precision
return typeof p === 'number' && p >= 0 ? p : undefined
})
const stepValue = useNumberStepCalculation(widget.options, precision, true)
const numberPt = useNumberWidgetButtonPt({
roundedLeft: true,
roundedRight: true
})
</script>

View File

@@ -43,11 +43,13 @@ export const useFloatWidget = () => {
const display_type = inputSpec.display
const widgetType =
sliderEnabled && display_type == 'slider'
? 'slider'
: display_type == 'knob'
? 'knob'
: 'number'
display_type == 'gradientslider'
? 'gradientslider'
: sliderEnabled && display_type == 'slider'
? 'slider'
: display_type == 'knob'
? 'knob'
: 'number'
const step = inputSpec.step ?? 0.5
const precision =
@@ -72,7 +74,10 @@ export const useFloatWidget = () => {
/** @deprecated Use step2 instead. The 10x value is a legacy implementation. */
step: step * 10.0,
step2: step,
precision
precision,
...(inputSpec.gradient_stops
? { gradient_stops: inputSpec.gradient_stops }
: {})
}
)

View File

@@ -93,7 +93,7 @@ const coreWidgetDefinitions: Array<[string, WidgetDefinition]> = [
'float',
{
component: WidgetInputNumber,
aliases: ['FLOAT', 'number', 'slider'],
aliases: ['FLOAT', 'number', 'slider', 'gradientslider'],
essential: true
}
],