Implement SliderWidget (#485)

This commit is contained in:
Chenlei Hu
2025-02-08 17:28:25 -05:00
committed by GitHub
parent 2d688a896d
commit cdaceebcaa
4 changed files with 171 additions and 55 deletions

View File

@@ -72,6 +72,7 @@ import { ComboWidget } from "./widgets/ComboWidget"
import { NumberWidget } from "./widgets/NumberWidget"
import { ButtonWidget } from "./widgets/ButtonWidget"
import { TextWidget } from "./widgets/TextWidget"
import { SliderWidget } from "./widgets/SliderWidget"
interface IShowSearchOptions {
node_to?: LGraphNode
@@ -2574,17 +2575,15 @@ export class LGraphCanvas implements ConnectionColorContext {
}
break
case "slider": {
if (widget.options.read_only) break
pointer.onDrag = (eMove) => {
const x = eMove.canvasX - node.pos[0]
const slideFactor = clamp((x - 15) / (width - 30), 0, 1)
widget.value = widget.options.min + (widget.options.max - widget.options.min) * slideFactor
if (oldValue != widget.value) {
setWidgetValue(this, node, widget, widget.value)
}
this.dirty_canvas = true
}
pointer.onClick = () => toClass(SliderWidget, widget).onClick({
e,
node,
canvas: this,
})
pointer.onDrag = eMove => toClass(SliderWidget, widget).onDrag({
e: eMove,
node,
})
break
}
case "number": {
@@ -5845,50 +5844,9 @@ export class LGraphCanvas implements ConnectionColorContext {
case "toggle":
toClass(BooleanWidget, w).drawWidget(ctx, { y, width: widget_width, show_text, margin })
break
case "slider": {
ctx.fillStyle = background_color
ctx.fillRect(margin, y, widget_width - margin * 2, H)
const range = w.options.max - w.options.min
let nvalue = (w.value - w.options.min) / range
if (nvalue < 0.0) nvalue = 0.0
if (nvalue > 1.0) nvalue = 1.0
ctx.fillStyle = w.options.hasOwnProperty("slider_color")
? w.options.slider_color
: active_widget == w
? "#89A"
: "#678"
ctx.fillRect(margin, y, nvalue * (widget_width - margin * 2), H)
if (show_text && !w.disabled)
ctx.strokeRect(margin, y, widget_width - margin * 2, H)
if (w.marker) {
let marker_nvalue = (w.marker - w.options.min) / range
if (marker_nvalue < 0.0) marker_nvalue = 0.0
if (marker_nvalue > 1.0) marker_nvalue = 1.0
ctx.fillStyle = w.options.hasOwnProperty("marker_color")
? w.options.marker_color
: "#AA9"
ctx.fillRect(
margin + marker_nvalue * (widget_width - margin * 2),
y,
2,
H,
)
}
if (show_text) {
ctx.textAlign = "center"
ctx.fillStyle = text_color
ctx.fillText(
(w.label || w.name) +
" " +
Number(w.value).toFixed(
w.options.precision != null ? w.options.precision : 3,
),
widget_width * 0.5,
y + H * 0.7,
)
}
case "slider":
toClass(SliderWidget, w).drawWidget(ctx, { y, width: widget_width, show_text, margin })
break
}
case "combo":
toClass(ComboWidget, w).drawWidget(ctx, { y, width: widget_width, show_text, margin })
break

View File

@@ -38,6 +38,7 @@ import { NumberWidget } from "./widgets/NumberWidget"
import { ButtonWidget } from "./widgets/ButtonWidget"
import { NodeInputSlot, NodeOutputSlot } from "./NodeSlot"
import { TextWidget } from "./widgets/TextWidget"
import { SliderWidget } from "./widgets/SliderWidget"
export type NodeId = number | string
@@ -1688,6 +1689,9 @@ export class LGraphNode implements Positionable, IPinnable {
case "string":
widget = new TextWidget(custom_widget)
break
case "slider":
widget = new SliderWidget(custom_widget)
break
default:
widget = custom_widget
}

View File

@@ -21,6 +21,14 @@ export interface IWidgetOptions<TValue = unknown> extends Record<string, unknown
callback?: IWidget["callback"]
}
export interface IWidgetSliderOptions extends IWidgetOptions<number> {
min: number
max: number
step: number
slider_color?: CanvasColour
marker_color?: CanvasColour
}
/**
* A widget for a node.
* All types are based on IBaseWidget - additions can be made there or directly on individual types.
@@ -36,6 +44,7 @@ export type IWidget =
| IMultilineStringWidget
| IComboWidget
| ICustomWidget
| ISliderWidget
export interface IBooleanWidget extends IBaseWidget {
type?: "toggle"
@@ -44,10 +53,16 @@ export interface IBooleanWidget extends IBaseWidget {
/** Any widget that uses a numeric backing */
export interface INumericWidget extends IBaseWidget {
type?: "slider" | "number"
type?: "number"
value: number
}
export interface ISliderWidget extends IBaseWidget {
type?: "slider"
value: number
options: IWidgetSliderOptions
}
/** A combo-box widget (dropdown, select, etc) */
export interface IComboWidget extends IBaseWidget {
type?: "combo"

139
src/widgets/SliderWidget.ts Normal file
View File

@@ -0,0 +1,139 @@
import type { ISliderWidget, IWidgetSliderOptions } from "@/types/widgets"
import { BaseWidget } from "./BaseWidget"
import type { LGraphNode } from "@/LGraphNode"
import type { CanvasMouseEvent } from "@/types/events"
import type { LGraphCanvas } from "@/LGraphCanvas"
import { clamp } from "@/litegraph"
export class SliderWidget extends BaseWidget implements ISliderWidget {
// ISliderWidget properties
declare type: "slider"
declare value: number
declare options: IWidgetSliderOptions
constructor(widget: ISliderWidget) {
super(widget)
this.type = "slider"
this.value = widget.value
this.options = widget.options
}
/**
* Draws the widget
* @param ctx - The canvas context
* @param options - The options for drawing the widget
*/
override drawWidget(ctx: CanvasRenderingContext2D, options: {
y: number
width: number
show_text?: boolean
margin?: number
}) {
// Store original context attributes
const originalTextAlign = ctx.textAlign
const originalStrokeStyle = ctx.strokeStyle
const originalFillStyle = ctx.fillStyle
const { y, width: widget_width, show_text = true, margin = 15 } = options
const H = this.height
// Draw background
ctx.fillStyle = this.background_color
ctx.fillRect(margin, y, widget_width - margin * 2, H)
// Calculate normalized value
const range = this.options.max - this.options.min
let nvalue = (this.value - this.options.min) / range
nvalue = clamp(nvalue, 0, 1)
// Draw slider bar
ctx.fillStyle = this.options.slider_color ?? "#678"
ctx.fillRect(margin, y, nvalue * (widget_width - margin * 2), H)
// Draw outline if not disabled
if (show_text && !this.disabled) {
ctx.strokeStyle = this.outline_color
ctx.strokeRect(margin, y, widget_width - margin * 2, H)
}
// Draw marker if present
if (this.marker != null) {
let marker_nvalue = (this.marker - this.options.min) / range
marker_nvalue = clamp(marker_nvalue, 0, 1)
ctx.fillStyle = this.options.marker_color ?? "#AA9"
ctx.fillRect(
margin + marker_nvalue * (widget_width - margin * 2),
y,
2,
H,
)
}
// Draw text
if (show_text) {
ctx.textAlign = "center"
ctx.fillStyle = this.text_color
ctx.fillText(
(this.label || this.name) +
" " +
Number(this.value).toFixed(
this.options.precision != null ? this.options.precision : 3,
),
widget_width * 0.5,
y + H * 0.7,
)
}
// Restore original context attributes
ctx.textAlign = originalTextAlign
ctx.strokeStyle = originalStrokeStyle
ctx.fillStyle = originalFillStyle
}
/**
* Handles click events for the slider widget
*/
override onClick(options: {
e: CanvasMouseEvent
node: LGraphNode
canvas: LGraphCanvas
}) {
if (this.options.read_only) return
const { e, node } = options
const width = this.width || node.size[0]
const x = e.canvasX - node.pos[0]
// Calculate new value based on click position
const slideFactor = clamp((x - 15) / (width - 30), 0, 1)
const newValue = this.options.min + (this.options.max - this.options.min) * slideFactor
if (newValue !== this.value) {
this.setValue(newValue, options)
}
}
/**
* Handles drag events for the slider widget
*/
override onDrag(options: {
e: CanvasMouseEvent
node: LGraphNode
}) {
if (this.options.read_only) return false
const { e, node } = options
const width = this.width || node.size[0]
const x = e.canvasX - node.pos[0]
// Calculate new value based on drag position
const slideFactor = clamp((x - 15) / (width - 30), 0, 1)
const newValue = this.options.min + (this.options.max - this.options.min) * slideFactor
if (newValue !== this.value) {
this.value = newValue
return true
}
return false
}
}