diff --git a/src/LGraphCanvas.ts b/src/LGraphCanvas.ts index 40ac0934e..b46d10c19 100644 --- a/src/LGraphCanvas.ts +++ b/src/LGraphCanvas.ts @@ -69,6 +69,7 @@ import { BooleanWidget } from "./widgets/BooleanWidget" import { toClass } from "./utils/type" import { NodeInputSlot, NodeOutputSlot, type ConnectionColorContext } from "./NodeSlot" import { ComboWidget } from "./widgets/ComboWidget" +import { NumberWidget } from "./widgets/NumberWidget" interface IShowSearchOptions { node_to?: LGraphNode @@ -2583,54 +2584,20 @@ export class LGraphCanvas implements ConnectionColorContext { break } case "number": { - const delta = x < 40 - ? -1 - : x > width - 40 - ? 1 - : 0 - pointer.onClick = (upEvent) => { - // 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 + const numberWidget = toClass(NumberWidget, widget) + pointer.onClick = () => { + numberWidget.onClick({ + e, + node, + canvas: this, + }) } - // Click & drag from widget centre area pointer.onDrag = (eMove) => { - const x = eMove.canvasX - node.pos[0] - if (delta && (x > -3 && x < width + 3)) return - - 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) + numberWidget.onDrag({ + e: eMove, + node, + }) } break } @@ -5937,85 +5904,7 @@ export class LGraphCanvas implements ConnectionColorContext { toClass(ComboWidget, w).drawWidget(ctx, { y, width: widget_width, show_text, margin }) break case "number": - ctx.textAlign = "left" - 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, - ) - } - } + toClass(NumberWidget, w).drawWidget(ctx, { y, width: widget_width, show_text, margin }) break case "string": case "text": diff --git a/src/LGraphNode.ts b/src/LGraphNode.ts index c949e813b..eefbdbde4 100644 --- a/src/LGraphNode.ts +++ b/src/LGraphNode.ts @@ -16,7 +16,7 @@ import type { Size, } from "./interfaces" 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 { LGraphCanvas } from "./LGraphCanvas" import type { CanvasMouseEvent } from "./types/events" @@ -34,6 +34,7 @@ import { isInRectangle, isInRect, snapPoint } from "./measure" import { LLink } from "./LLink" import { BooleanWidget } from "./widgets/BooleanWidget" import { ComboWidget } from "./widgets/ComboWidget" +import { NumberWidget } from "./widgets/NumberWidget" import { NodeInputSlot, NodeOutputSlot } from "./NodeSlot" export type NodeId = number | string @@ -1675,6 +1676,9 @@ export class LGraphNode implements Positionable, IPinnable { case "combo": widget = new ComboWidget(custom_widget) break + case "number": + widget = new NumberWidget(custom_widget) + break default: widget = custom_widget } diff --git a/src/widgets/BaseWidget.ts b/src/widgets/BaseWidget.ts index 62201311a..036846f57 100644 --- a/src/widgets/BaseWidget.ts +++ b/src/widgets/BaseWidget.ts @@ -83,11 +83,20 @@ export abstract class BaseWidget implements IBaseWidget { * Handles the click event for the widget * @param options - The options for handling the click event */ - abstract onClick(options: { + onClick(options: { e: CanvasMouseEvent node: LGraphNode 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 diff --git a/src/widgets/NumberWidget.ts b/src/widgets/NumberWidget.ts new file mode 100644 index 000000000..903afd169 --- /dev/null +++ b/src/widgets/NumberWidget.ts @@ -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 + + 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 + } +}