diff --git a/src/LGraphCanvas.ts b/src/LGraphCanvas.ts index ff417a294..96b8293c7 100644 --- a/src/LGraphCanvas.ts +++ b/src/LGraphCanvas.ts @@ -65,6 +65,8 @@ import { alignNodes, distributeNodes, getBoundaryNodes } from "./utils/arrange" import { Reroute, type RerouteId } from "./Reroute" import { getAllNestedItems, findFirstNode } from "./utils/collections" import { CanvasPointer } from "./CanvasPointer" +import { BooleanWidget } from "./widgets/BooleanWidget" +import { toClass } from "./utils/type" interface IShowSearchOptions { node_to?: LGraphNode @@ -5954,39 +5956,7 @@ export class LGraphCanvas { } break case "toggle": - 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 && !w.disabled) ctx.stroke() - ctx.fillStyle = w.value ? "#89A" : "#333" - ctx.beginPath() - ctx.arc( - widget_width - margin * 2, - y + H * 0.5, - H * 0.36, - 0, - Math.PI * 2, - ) - ctx.fill() - if (show_text) { - ctx.fillStyle = secondary_text_color - const label = w.label || w.name - if (label != null) { - ctx.fillText(label, margin * 2, y + H * 0.7) - } - ctx.fillStyle = w.value ? text_color : secondary_text_color - ctx.textAlign = "right" - ctx.fillText( - w.value ? w.options.on || "true" : w.options.off || "false", - widget_width - 40, - y + H * 0.7, - ) - } + toClass(BooleanWidget, w).drawWidget(ctx, { y, width: widget_width, show_text, margin }) break case "slider": { ctx.fillStyle = background_color diff --git a/src/LGraphNode.ts b/src/LGraphNode.ts index 9b4149cf2..b4fcd4694 100644 --- a/src/LGraphNode.ts +++ b/src/LGraphNode.ts @@ -32,6 +32,7 @@ import { BadgePosition, LGraphBadge } from "./LGraphBadge" import { type LGraphNodeConstructor, LiteGraph } from "./litegraph" import { isInRectangle, isInRect, snapPoint } from "./measure" import { LLink } from "./LLink" +import { BooleanWidget } from "./widgets/BooleanWidget" export type NodeId = number | string @@ -1655,15 +1656,26 @@ export class LGraphNode implements Positionable, IPinnable { if (type == "combo" && !w.options.values) { throw "LiteGraph addWidget('combo',...) requires to pass values in options: { values:['red','blue'] }" } - this.widgets.push(w) + + const widget = this.addCustomWidget(w) this.setSize(this.computeSize()) - return w + return widget } addCustomWidget(custom_widget: IWidget): IWidget { this.widgets ||= [] - this.widgets.push(custom_widget) - return custom_widget + + let widget: IWidget + switch (custom_widget.type) { + case "toggle": + widget = new BooleanWidget(custom_widget) + break + default: + widget = custom_widget + } + + this.widgets.push(widget) + return widget } move(deltaX: number, deltaY: number): void { diff --git a/src/types/widgets.ts b/src/types/widgets.ts index d78299e13..dc6d8f750 100644 --- a/src/types/widgets.ts +++ b/src/types/widgets.ts @@ -159,5 +159,5 @@ export interface IBaseWidget { * @return Returning `true` from this callback forces Litegraph to ignore the event and * not process it any further. */ - onPointerDown(pointer: CanvasPointer, node: LGraphNode, canvas: LGraphCanvas): boolean + onPointerDown?(pointer: CanvasPointer, node: LGraphNode, canvas: LGraphCanvas): boolean } diff --git a/src/utils/type.ts b/src/utils/type.ts new file mode 100644 index 000000000..b6dc33266 --- /dev/null +++ b/src/utils/type.ts @@ -0,0 +1,9 @@ +/** + * Converts a plain object to a class instance if it is not already an instance of the class. + * @param cls The class to convert to + * @param obj The object to convert + * @returns The class instance + */ +export function toClass(cls: new (plain: P) => C, obj: P | C): C { + return obj instanceof cls ? obj : new cls(obj as P) +} diff --git a/src/widgets/BaseWidget.ts b/src/widgets/BaseWidget.ts new file mode 100644 index 000000000..ee68045f8 --- /dev/null +++ b/src/widgets/BaseWidget.ts @@ -0,0 +1,73 @@ +import { Point } from "@/interfaces" +import { LiteGraph } from "@/litegraph" +import type { CanvasPointer, LGraphCanvas, LGraphNode, Size } from "@/litegraph" +import type { CanvasMouseEvent, CanvasPointerEvent } from "@/types/events" +import type { IBaseWidget, IWidget, IWidgetOptions } from "@/types/widgets" + +export abstract class BaseWidget implements IBaseWidget { + linkedWidgets?: IWidget[] + options: IWidgetOptions + marker?: number + label?: string + clicked?: boolean + name?: string + type?: "string" | "number" | "combo" | "button" | "toggle" | "slider" | "text" | "multiline" | "custom" + value?: string | number | boolean | object + y?: number + last_y?: number + width?: number + disabled?: boolean + hidden?: boolean + advanced?: boolean + tooltip?: string + element?: HTMLElement + callback?( + value: any, + canvas?: LGraphCanvas, + node?: LGraphNode, + pos?: Point, + e?: CanvasMouseEvent, + ): void + mouse?(event: CanvasPointerEvent, pointerOffset: Point, node: LGraphNode): boolean + draw?( + ctx: CanvasRenderingContext2D, + node: LGraphNode, + widget_width: number, + y: number, + H: number, + ): void + computeSize?(width?: number): Size + onPointerDown?(pointer: CanvasPointer, node: LGraphNode, canvas: LGraphCanvas): boolean + + constructor(widget: IBaseWidget) { + Object.assign(this, widget) + this.options = widget.options + } + + get outline_color() { + return this.advanced ? LiteGraph.WIDGET_ADVANCED_OUTLINE_COLOR : LiteGraph.WIDGET_OUTLINE_COLOR + } + + get background_color() { + return LiteGraph.WIDGET_BGCOLOR + } + + get height() { + return LiteGraph.NODE_WIDGET_HEIGHT + } + + get text_color() { + return LiteGraph.WIDGET_TEXT_COLOR + } + + get secondary_text_color() { + return LiteGraph.WIDGET_SECONDARY_TEXT_COLOR + } + + abstract drawWidget(ctx: CanvasRenderingContext2D, options: { + y: number + width: number + show_text?: boolean + margin?: number + }): void +} diff --git a/src/widgets/BooleanWidget.ts b/src/widgets/BooleanWidget.ts new file mode 100644 index 000000000..d4a17ba80 --- /dev/null +++ b/src/widgets/BooleanWidget.ts @@ -0,0 +1,68 @@ +import { IBooleanWidget } from "@/types/widgets" +import { BaseWidget } from "./BaseWidget" + +export class BooleanWidget extends BaseWidget implements IBooleanWidget { + // IBooleanWidget properties + declare type: "toggle" + declare value: boolean + + constructor(widget: IBooleanWidget) { + super(widget) + this.type = "toggle" + this.value = widget.value + } + + /** + * Draws the widget + * @param ctx - The canvas context + * @param options - The options for drawing the widget + * + * @note Not naming this `draw` as `draw` conflicts with the `draw` method in + * custom widgets. + */ + override drawWidget(ctx: CanvasRenderingContext2D, options: { + y: number + width: number + show_text?: boolean + margin?: number + }) { + 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 && !this.disabled) ctx.stroke() + ctx.fillStyle = this.value ? "#89A" : "#333" + ctx.beginPath() + ctx.arc( + widget_width - margin * 2, + y + H * 0.5, + H * 0.36, + 0, + Math.PI * 2, + ) + ctx.fill() + if (show_text) { + ctx.fillStyle = this.secondary_text_color + const label = this.label || this.name + if (label != null) { + ctx.fillText(label, margin * 2, y + H * 0.7) + } + ctx.fillStyle = this.value ? this.text_color : this.secondary_text_color + ctx.textAlign = "right" + ctx.fillText( + this.value ? this.options.on || "true" : this.options.off || "false", + widget_width - 40, + y + H * 0.7, + ) + } + } +}