mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-29 10:42:44 +00:00
Implement NumberWidget (#481)
This commit is contained in:
@@ -69,6 +69,7 @@ import { BooleanWidget } from "./widgets/BooleanWidget"
|
|||||||
import { toClass } from "./utils/type"
|
import { toClass } from "./utils/type"
|
||||||
import { NodeInputSlot, NodeOutputSlot, type ConnectionColorContext } from "./NodeSlot"
|
import { NodeInputSlot, NodeOutputSlot, type ConnectionColorContext } from "./NodeSlot"
|
||||||
import { ComboWidget } from "./widgets/ComboWidget"
|
import { ComboWidget } from "./widgets/ComboWidget"
|
||||||
|
import { NumberWidget } from "./widgets/NumberWidget"
|
||||||
|
|
||||||
interface IShowSearchOptions {
|
interface IShowSearchOptions {
|
||||||
node_to?: LGraphNode
|
node_to?: LGraphNode
|
||||||
@@ -2583,54 +2584,20 @@ export class LGraphCanvas implements ConnectionColorContext {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "number": {
|
case "number": {
|
||||||
const delta = x < 40
|
const numberWidget = toClass(NumberWidget, widget)
|
||||||
? -1
|
pointer.onClick = () => {
|
||||||
: x > width - 40
|
numberWidget.onClick({
|
||||||
? 1
|
e,
|
||||||
: 0
|
node,
|
||||||
pointer.onClick = (upEvent) => {
|
canvas: this,
|
||||||
// Left/right arrows
|
})
|
||||||
let newValue = widget.value + delta * 0.1 * (widget.options.step || 1)
|
|
||||||
if (widget.options.min != null && newValue < widget.options.min) {
|
|
||||||
newValue = widget.options.min
|
|
||||||
}
|
|
||||||
if (widget.options.max != null && newValue > widget.options.max) {
|
|
||||||
newValue = widget.options.max
|
|
||||||
}
|
|
||||||
if (newValue !== widget.value) setWidgetValue(this, node, widget, newValue)
|
|
||||||
|
|
||||||
if (delta !== 0) return
|
|
||||||
|
|
||||||
// Click in widget centre area - prompt user for input
|
|
||||||
this.prompt("Value", widget.value, (v: string) => {
|
|
||||||
// check if v is a valid equation or a number
|
|
||||||
if (/^[0-9+\-*/()\s]+|\d+\.\d+$/.test(v)) {
|
|
||||||
// solve the equation if possible
|
|
||||||
try {
|
|
||||||
v = eval(v)
|
|
||||||
} catch { }
|
|
||||||
}
|
|
||||||
widget.value = Number(v)
|
|
||||||
setWidgetValue(this, node, widget, widget.value)
|
|
||||||
}, e)
|
|
||||||
this.dirty_canvas = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Click & drag from widget centre area
|
// Click & drag from widget centre area
|
||||||
pointer.onDrag = (eMove) => {
|
pointer.onDrag = (eMove) => {
|
||||||
const x = eMove.canvasX - node.pos[0]
|
numberWidget.onDrag({
|
||||||
if (delta && (x > -3 && x < width + 3)) return
|
e: eMove,
|
||||||
|
node,
|
||||||
let newValue = widget.value
|
})
|
||||||
if (eMove.deltaX) newValue += eMove.deltaX * 0.1 * (widget.options.step || 1)
|
|
||||||
|
|
||||||
if (widget.options.min != null && newValue < widget.options.min) {
|
|
||||||
newValue = widget.options.min
|
|
||||||
}
|
|
||||||
if (widget.options.max != null && newValue > widget.options.max) {
|
|
||||||
newValue = widget.options.max
|
|
||||||
}
|
|
||||||
if (newValue !== widget.value) setWidgetValue(this, node, widget, newValue)
|
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -5937,85 +5904,7 @@ export class LGraphCanvas implements ConnectionColorContext {
|
|||||||
toClass(ComboWidget, w).drawWidget(ctx, { y, width: widget_width, show_text, margin })
|
toClass(ComboWidget, w).drawWidget(ctx, { y, width: widget_width, show_text, margin })
|
||||||
break
|
break
|
||||||
case "number":
|
case "number":
|
||||||
ctx.textAlign = "left"
|
toClass(NumberWidget, w).drawWidget(ctx, { y, width: widget_width, show_text, margin })
|
||||||
ctx.strokeStyle = outline_color
|
|
||||||
ctx.fillStyle = background_color
|
|
||||||
ctx.beginPath()
|
|
||||||
if (show_text)
|
|
||||||
ctx.roundRect(margin, y, widget_width - margin * 2, H, [H * 0.5])
|
|
||||||
else ctx.rect(margin, y, widget_width - margin * 2, H)
|
|
||||||
ctx.fill()
|
|
||||||
if (show_text) {
|
|
||||||
if (!w.disabled) ctx.stroke()
|
|
||||||
ctx.fillStyle = text_color
|
|
||||||
if (!w.disabled) {
|
|
||||||
ctx.beginPath()
|
|
||||||
ctx.moveTo(margin + 16, y + 5)
|
|
||||||
ctx.lineTo(margin + 6, y + H * 0.5)
|
|
||||||
ctx.lineTo(margin + 16, y + H - 5)
|
|
||||||
ctx.fill()
|
|
||||||
ctx.beginPath()
|
|
||||||
ctx.moveTo(widget_width - margin - 16, y + 5)
|
|
||||||
ctx.lineTo(widget_width - margin - 6, y + H * 0.5)
|
|
||||||
ctx.lineTo(widget_width - margin - 16, y + H - 5)
|
|
||||||
ctx.fill()
|
|
||||||
}
|
|
||||||
ctx.fillStyle = secondary_text_color
|
|
||||||
ctx.fillText(w.label || w.name, margin * 2 + 5, y + H * 0.7)
|
|
||||||
ctx.fillStyle = text_color
|
|
||||||
ctx.textAlign = "right"
|
|
||||||
if (w.type == "number") {
|
|
||||||
ctx.fillText(
|
|
||||||
Number(w.value).toFixed(
|
|
||||||
w.options.precision !== undefined
|
|
||||||
? w.options.precision
|
|
||||||
: 3,
|
|
||||||
),
|
|
||||||
widget_width - margin * 2 - 20,
|
|
||||||
y + H * 0.7,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
let v = String(w.value)
|
|
||||||
if (w.options.values) {
|
|
||||||
let values = w.options.values
|
|
||||||
if (typeof values === "function")
|
|
||||||
// @ts-expect-error
|
|
||||||
values = values()
|
|
||||||
if (values && !Array.isArray(values))
|
|
||||||
v = values[w.value]
|
|
||||||
}
|
|
||||||
const labelWidth = ctx.measureText(w.label || w.name).width + margin * 2
|
|
||||||
const inputWidth = widget_width - margin * 4
|
|
||||||
const availableWidth = inputWidth - labelWidth
|
|
||||||
const textWidth = ctx.measureText(v).width
|
|
||||||
if (textWidth > availableWidth) {
|
|
||||||
const ELLIPSIS = "\u2026"
|
|
||||||
const ellipsisWidth = ctx.measureText(ELLIPSIS).width
|
|
||||||
const charWidthAvg = ctx.measureText("a").width
|
|
||||||
if (availableWidth <= ellipsisWidth) {
|
|
||||||
v = "\u2024" // One dot leader
|
|
||||||
} else {
|
|
||||||
v = `${v}`
|
|
||||||
const overflowWidth = (textWidth + ellipsisWidth) - availableWidth
|
|
||||||
// Only first 3 characters need to be measured precisely
|
|
||||||
if (overflowWidth + charWidthAvg * 3 > availableWidth) {
|
|
||||||
const preciseRange = availableWidth + charWidthAvg * 3
|
|
||||||
const preTruncateCt = Math.floor((preciseRange - ellipsisWidth) / charWidthAvg)
|
|
||||||
v = v.substr(0, preTruncateCt)
|
|
||||||
}
|
|
||||||
while (ctx.measureText(v).width + ellipsisWidth > availableWidth) {
|
|
||||||
v = v.substr(0, v.length - 1)
|
|
||||||
}
|
|
||||||
v += ELLIPSIS
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx.fillText(
|
|
||||||
v,
|
|
||||||
widget_width - margin * 2 - 20,
|
|
||||||
y + H * 0.7,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
case "string":
|
case "string":
|
||||||
case "text":
|
case "text":
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import type {
|
|||||||
Size,
|
Size,
|
||||||
} from "./interfaces"
|
} from "./interfaces"
|
||||||
import type { LGraph } from "./LGraph"
|
import type { LGraph } from "./LGraph"
|
||||||
import type { IBaseWidget, IWidget, TWidgetValue } from "./types/widgets"
|
import type { IWidget, TWidgetValue } from "./types/widgets"
|
||||||
import type { ISerialisedNode } from "./types/serialisation"
|
import type { ISerialisedNode } from "./types/serialisation"
|
||||||
import type { LGraphCanvas } from "./LGraphCanvas"
|
import type { LGraphCanvas } from "./LGraphCanvas"
|
||||||
import type { CanvasMouseEvent } from "./types/events"
|
import type { CanvasMouseEvent } from "./types/events"
|
||||||
@@ -34,6 +34,7 @@ import { isInRectangle, isInRect, snapPoint } from "./measure"
|
|||||||
import { LLink } from "./LLink"
|
import { LLink } from "./LLink"
|
||||||
import { BooleanWidget } from "./widgets/BooleanWidget"
|
import { BooleanWidget } from "./widgets/BooleanWidget"
|
||||||
import { ComboWidget } from "./widgets/ComboWidget"
|
import { ComboWidget } from "./widgets/ComboWidget"
|
||||||
|
import { NumberWidget } from "./widgets/NumberWidget"
|
||||||
import { NodeInputSlot, NodeOutputSlot } from "./NodeSlot"
|
import { NodeInputSlot, NodeOutputSlot } from "./NodeSlot"
|
||||||
|
|
||||||
export type NodeId = number | string
|
export type NodeId = number | string
|
||||||
@@ -1675,6 +1676,9 @@ export class LGraphNode implements Positionable, IPinnable {
|
|||||||
case "combo":
|
case "combo":
|
||||||
widget = new ComboWidget(custom_widget)
|
widget = new ComboWidget(custom_widget)
|
||||||
break
|
break
|
||||||
|
case "number":
|
||||||
|
widget = new NumberWidget(custom_widget)
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
widget = custom_widget
|
widget = custom_widget
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,11 +83,20 @@ export abstract class BaseWidget implements IBaseWidget {
|
|||||||
* Handles the click event for the widget
|
* Handles the click event for the widget
|
||||||
* @param options - The options for handling the click event
|
* @param options - The options for handling the click event
|
||||||
*/
|
*/
|
||||||
abstract onClick(options: {
|
onClick(options: {
|
||||||
e: CanvasMouseEvent
|
e: CanvasMouseEvent
|
||||||
node: LGraphNode
|
node: LGraphNode
|
||||||
canvas: LGraphCanvas
|
canvas: LGraphCanvas
|
||||||
}): void
|
}): void {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the drag event for the widget
|
||||||
|
* @param options - The options for handling the drag event
|
||||||
|
*/
|
||||||
|
onDrag(options: {
|
||||||
|
e: CanvasMouseEvent
|
||||||
|
node: LGraphNode
|
||||||
|
}): void {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the value of the widget
|
* Sets the value of the widget
|
||||||
|
|||||||
176
src/widgets/NumberWidget.ts
Normal file
176
src/widgets/NumberWidget.ts
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
import type { INumericWidget, IWidgetOptions } from "@/types/widgets"
|
||||||
|
import { BaseWidget } from "./BaseWidget"
|
||||||
|
import type { LGraphNode } from "@/LGraphNode"
|
||||||
|
import type { CanvasMouseEvent } from "@/types/events"
|
||||||
|
import type { LGraphCanvas } from "@/LGraphCanvas"
|
||||||
|
|
||||||
|
export class NumberWidget extends BaseWidget implements INumericWidget {
|
||||||
|
// INumberWidget properties
|
||||||
|
declare type: "number"
|
||||||
|
declare value: number
|
||||||
|
declare options: IWidgetOptions<number>
|
||||||
|
|
||||||
|
constructor(widget: INumericWidget) {
|
||||||
|
super(widget)
|
||||||
|
this.type = "number"
|
||||||
|
this.value = widget.value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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, show_text = true, margin = 15 } = options
|
||||||
|
const widget_width = width
|
||||||
|
const H = this.height
|
||||||
|
|
||||||
|
ctx.textAlign = "left"
|
||||||
|
ctx.strokeStyle = this.outline_color
|
||||||
|
ctx.fillStyle = this.background_color
|
||||||
|
ctx.beginPath()
|
||||||
|
|
||||||
|
if (show_text)
|
||||||
|
ctx.roundRect(margin, y, widget_width - margin * 2, H, [H * 0.5])
|
||||||
|
else
|
||||||
|
ctx.rect(margin, y, widget_width - margin * 2, H)
|
||||||
|
ctx.fill()
|
||||||
|
|
||||||
|
if (show_text) {
|
||||||
|
if (!this.disabled) {
|
||||||
|
ctx.stroke()
|
||||||
|
// Draw left arrow
|
||||||
|
ctx.fillStyle = this.text_color
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(margin + 16, y + 5)
|
||||||
|
ctx.lineTo(margin + 6, y + H * 0.5)
|
||||||
|
ctx.lineTo(margin + 16, y + H - 5)
|
||||||
|
ctx.fill()
|
||||||
|
// Draw right arrow
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(widget_width - margin - 16, y + 5)
|
||||||
|
ctx.lineTo(widget_width - margin - 6, y + H * 0.5)
|
||||||
|
ctx.lineTo(widget_width - margin - 16, y + H - 5)
|
||||||
|
ctx.fill()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw label
|
||||||
|
ctx.fillStyle = this.secondary_text_color
|
||||||
|
const label = this.label || this.name
|
||||||
|
if (label != null) {
|
||||||
|
ctx.fillText(label, margin * 2 + 5, y + H * 0.7)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw value
|
||||||
|
ctx.fillStyle = this.text_color
|
||||||
|
ctx.textAlign = "right"
|
||||||
|
ctx.fillText(
|
||||||
|
Number(this.value).toFixed(
|
||||||
|
this.options.precision !== undefined
|
||||||
|
? this.options.precision
|
||||||
|
: 3,
|
||||||
|
),
|
||||||
|
widget_width - margin * 2 - 20,
|
||||||
|
y + H * 0.7,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore original context attributes
|
||||||
|
ctx.textAlign = originalTextAlign
|
||||||
|
ctx.strokeStyle = originalStrokeStyle
|
||||||
|
ctx.fillStyle = originalFillStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
override onClick(options: {
|
||||||
|
e: CanvasMouseEvent
|
||||||
|
node: LGraphNode
|
||||||
|
canvas: LGraphCanvas
|
||||||
|
}) {
|
||||||
|
const { e, node, canvas } = options
|
||||||
|
const x = e.canvasX - node.pos[0]
|
||||||
|
const width = this.width || node.size[0]
|
||||||
|
|
||||||
|
// Determine if clicked on left/right arrows
|
||||||
|
const delta = x < 40
|
||||||
|
? -1
|
||||||
|
: x > width - 40
|
||||||
|
? 1
|
||||||
|
: 0
|
||||||
|
|
||||||
|
if (delta) {
|
||||||
|
// Handle left/right arrow clicks
|
||||||
|
let newValue = this.value + delta * 0.1 * (this.options.step || 1)
|
||||||
|
if (this.options.min != null && newValue < this.options.min) {
|
||||||
|
newValue = this.options.min
|
||||||
|
}
|
||||||
|
if (this.options.max != null && newValue > this.options.max) {
|
||||||
|
newValue = this.options.max
|
||||||
|
}
|
||||||
|
if (newValue !== this.value) {
|
||||||
|
this.setValue(newValue, { e, node, canvas })
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle center click - show prompt
|
||||||
|
canvas.prompt("Value", this.value, (v: string) => {
|
||||||
|
// Check if v is a valid equation or a number
|
||||||
|
if (/^[0-9+\-*/()\s]+|\d+\.\d+$/.test(v)) {
|
||||||
|
// Solve the equation if possible
|
||||||
|
try {
|
||||||
|
v = eval(v)
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
const newValue = Number(v)
|
||||||
|
if (!isNaN(newValue)) {
|
||||||
|
this.setValue(newValue, { e, node, canvas })
|
||||||
|
}
|
||||||
|
}, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles drag events for the number widget
|
||||||
|
* @param options - The options for handling the drag event
|
||||||
|
*/
|
||||||
|
override onDrag(options: {
|
||||||
|
e: CanvasMouseEvent
|
||||||
|
node: LGraphNode
|
||||||
|
}) {
|
||||||
|
const { e, node } = options
|
||||||
|
const width = this.width || node.width
|
||||||
|
const x = e.canvasX - node.pos[0]
|
||||||
|
const delta = x < 40
|
||||||
|
? -1
|
||||||
|
: x > width - 40
|
||||||
|
? 1
|
||||||
|
: 0
|
||||||
|
|
||||||
|
if (delta && (x > -3 && x < width + 3)) return
|
||||||
|
|
||||||
|
let newValue = this.value
|
||||||
|
if (e.deltaX) newValue += e.deltaX * 0.1 * (this.options.step || 1)
|
||||||
|
|
||||||
|
if (this.options.min != null && newValue < this.options.min) {
|
||||||
|
newValue = this.options.min
|
||||||
|
}
|
||||||
|
if (this.options.max != null && newValue > this.options.max) {
|
||||||
|
newValue = this.options.max
|
||||||
|
}
|
||||||
|
if (newValue !== this.value) {
|
||||||
|
this.value = newValue
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user