diff --git a/src/LGraphCanvas.ts b/src/LGraphCanvas.ts index 2f6abe960..40ac0934e 100644 --- a/src/LGraphCanvas.ts +++ b/src/LGraphCanvas.ts @@ -68,6 +68,7 @@ import { CanvasPointer } from "./CanvasPointer" import { BooleanWidget } from "./widgets/BooleanWidget" import { toClass } from "./utils/type" import { NodeInputSlot, NodeOutputSlot, type ConnectionColorContext } from "./NodeSlot" +import { ComboWidget } from "./widgets/ComboWidget" interface IShowSearchOptions { node_to?: LGraphNode @@ -2634,61 +2635,11 @@ export class LGraphCanvas implements ConnectionColorContext { break } case "combo": { - // TODO: Type checks on widget values - let values: string[] - let values_list: string[] - - pointer.onClick = (upEvent) => { - const delta = x < 40 - ? -1 - : x > width - 40 - ? 1 - : 0 - - // Combo buttons - values = widget.options.values - if (typeof values === "function") { - // @ts-expect-error - values = values(widget, node) - } - values_list = null - - values_list = Array.isArray(values) ? values : Object.keys(values) - - // Left/right arrows - if (delta) { - let index = -1 - this.last_mouseclick = 0 // avoids dobl click event - index = typeof values === "object" - ? values_list.indexOf(String(widget.value)) + delta - // @ts-expect-error - : values_list.indexOf(widget.value) + delta - - if (index >= values_list.length) index = values_list.length - 1 - if (index < 0) index = 0 - - widget.value = Array.isArray(values) - ? values[index] - : index - - if (oldValue != widget.value) setWidgetValue(this, node, widget, widget.value) - this.dirty_canvas = true - return - } - const text_values = values != values_list ? Object.values(values) : values - new LiteGraph.ContextMenu(text_values, { - scale: Math.max(1, this.ds.scale), - event: e, - className: "dark", - callback: (value: string) => { - widget.value = values != values_list - ? text_values.indexOf(value) - : value - - setWidgetValue(this, node, widget, widget.value) - this.dirty_canvas = true - return false - }, + pointer.onClick = () => { + toClass(ComboWidget, widget).onClick({ + e, + node, + canvas: this, }) } break @@ -5982,8 +5933,10 @@ export class LGraphCanvas implements ConnectionColorContext { } break } - case "number": case "combo": + 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 @@ -6022,7 +5975,7 @@ export class LGraphCanvas implements ConnectionColorContext { y + H * 0.7, ) } else { - let v = typeof w.value === "number" ? String(w.value) : w.value + let v = String(w.value) if (w.options.values) { let values = w.options.values if (typeof values === "function") diff --git a/src/LGraphNode.ts b/src/LGraphNode.ts index acc6f8cc6..c949e813b 100644 --- a/src/LGraphNode.ts +++ b/src/LGraphNode.ts @@ -33,6 +33,7 @@ import { type LGraphNodeConstructor, LiteGraph } from "./litegraph" import { isInRectangle, isInRect, snapPoint } from "./measure" import { LLink } from "./LLink" import { BooleanWidget } from "./widgets/BooleanWidget" +import { ComboWidget } from "./widgets/ComboWidget" import { NodeInputSlot, NodeOutputSlot } from "./NodeSlot" export type NodeId = number | string @@ -1671,6 +1672,9 @@ export class LGraphNode implements Positionable, IPinnable { case "toggle": widget = new BooleanWidget(custom_widget) break + case "combo": + widget = new ComboWidget(custom_widget) + break default: widget = custom_widget } diff --git a/src/widgets/ComboWidget.ts b/src/widgets/ComboWidget.ts new file mode 100644 index 000000000..05cdf83a4 --- /dev/null +++ b/src/widgets/ComboWidget.ts @@ -0,0 +1,206 @@ +import type { IComboWidget, IWidgetOptions } from "@/types/widgets" +import { BaseWidget } from "./BaseWidget" +import { LiteGraph } from "@/litegraph" +import type { LGraphNode } from "@/LGraphNode" +import type { CanvasMouseEvent } from "@/types/events" +import type { LGraphCanvas } from "@/LGraphCanvas" + +export class ComboWidget extends BaseWidget implements IComboWidget { + // IComboWidget properties + declare type: "combo" + declare value: string | number + declare options: IWidgetOptions + + constructor(widget: IComboWidget) { + super(widget) + this.type = "combo" + 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" + + let displayValue = typeof this.value === "number" ? String(this.value) : this.value + if (this.options.values) { + let values = this.options.values + if (typeof values === "function") { + // @ts-expect-error handle () => string[] type that is not typed in IWidgetOptions + values = values() + } + if (values && !Array.isArray(values)) { + displayValue = values[this.value] + } + } + + const labelWidth = ctx.measureText(label || "").width + margin * 2 + const inputWidth = widget_width - margin * 4 + const availableWidth = inputWidth - labelWidth + const textWidth = ctx.measureText(displayValue).width + + if (textWidth > availableWidth) { + const ELLIPSIS = "\u2026" + const ellipsisWidth = ctx.measureText(ELLIPSIS).width + const charWidthAvg = ctx.measureText("a").width + + if (availableWidth <= ellipsisWidth) { + displayValue = "\u2024" // One dot leader + } else { + displayValue = `${displayValue}` + 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) + displayValue = displayValue.substr(0, preTruncateCt) + } + + while (ctx.measureText(displayValue).width + ellipsisWidth > availableWidth) { + displayValue = displayValue.substr(0, displayValue.length - 1) + } + displayValue += ELLIPSIS + } + } + + ctx.fillText( + displayValue, + 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 + + // Get values + let values = this.options.values + if (typeof values === "function") { + // @ts-expect-error handle () => string[] type that is not typed in IWidgetOptions + values = values(this, node) + } + // @ts-ignore Record is not typed in IWidgetOptions + const values_list = Array.isArray(values) ? values : Object.keys(values) + + // Handle left/right arrow clicks + if (delta) { + let index = -1 + canvas.last_mouseclick = 0 // avoids double click event + index = typeof values === "object" + ? values_list.indexOf(String(this.value)) + delta + // @ts-expect-error handle non-string values + : values_list.indexOf(this.value) + delta + + if (index >= values_list.length) index = values_list.length - 1 + if (index < 0) index = 0 + + this.setValue( + Array.isArray(values) + ? values[index] + : index, + { + e, + node, + canvas, + }, + ) + return + } + + // Handle center click - show dropdown menu + // @ts-ignore Record is not typed in IWidgetOptions + const text_values = values != values_list ? Object.values(values) : values + new LiteGraph.ContextMenu(text_values, { + scale: Math.max(1, canvas.ds.scale), + event: e, + className: "dark", + callback: (value: string) => { + this.setValue( + values != values_list + ? text_values.indexOf(value) + : value, + { + e, + node, + canvas, + }, + ) + }, + }) + } +}