From 1ef4921c0a52bbddc34f795cd596c7c214ea7565 Mon Sep 17 00:00:00 2001 From: Chenlei Hu Date: Sat, 5 Apr 2025 16:29:37 -0400 Subject: [PATCH] Support associated socket for widgets (#891) This PR is the litegraph side change necessary for widget sockets feature in ComfyUI_frontend. Changes include - Add readonly `Widget.computedDisabled` property for getting the computed disabled state. When the associated socket is connected, the widget is disabled - Dynamically show the associated socket when - the mouse is over the widget - the slot is valid during link drop - the slot is connected - Removes the legacy widget drop behavior Ref: https://github.com/Comfy-Org/rfcs/pull/9 --- src/LGraphCanvas.ts | 24 ------------ src/LGraphNode.ts | 74 ++++++++++++++++++++++++++---------- src/NodeSlot.ts | 3 +- src/types/widgets.ts | 14 +++++++ src/widgets/BaseWidget.ts | 1 + src/widgets/BooleanWidget.ts | 2 +- src/widgets/ButtonWidget.ts | 2 +- src/widgets/ComboWidget.ts | 2 +- src/widgets/KnobWidget.ts | 2 +- src/widgets/NumberWidget.ts | 2 +- src/widgets/SliderWidget.ts | 2 +- src/widgets/TextWidget.ts | 2 +- 12 files changed, 77 insertions(+), 53 deletions(-) diff --git a/src/LGraphCanvas.ts b/src/LGraphCanvas.ts index f406fcd31..b7b494c93 100644 --- a/src/LGraphCanvas.ts +++ b/src/LGraphCanvas.ts @@ -576,11 +576,6 @@ export class LGraphCanvas implements ConnectionColorContext { onNodeSelected?: (node: LGraphNode) => void onNodeDeselected?: (node: LGraphNode) => void onRender?: (canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D) => void - /** Implement this function to allow conversion of widget types to input types, e.g. number -> INT or FLOAT for widget link validation checks */ - getWidgetLinkType?: ( - widget: IWidget, - node: LGraphNode, - ) => string | null | undefined /** * Creates a new instance of LGraphCanvas. @@ -2601,20 +2596,6 @@ export class LGraphCanvas implements ConnectionColorContext { // No link, or none of the dragged links may be dropped here } else if (linkConnector.state.connectingTo === "input") { if (inputId === -1 && outputId === -1) { - // Allow support for linking to widgets, handled externally to LiteGraph - if (this.getWidgetLinkType && overWidget) { - const widgetLinkType = this.getWidgetLinkType(overWidget, node) - if ( - widgetLinkType && - LiteGraph.isValidConnection(linkConnector.renderLinks[0]?.fromSlot.type, widgetLinkType) && - firstLink.node.isValidWidgetLink?.(firstLink.fromSlotIndex, node, overWidget) !== false - ) { - const { pos: [nodeX, nodeY] } = node - highlightPos = [nodeX + 10, nodeY + 10 + overWidget.y] - linkConnector.overWidget = overWidget - linkConnector.overWidgetType = widgetLinkType - } - } // Node background / title under the pointer if (!linkConnector.overWidget) { const result = node.findInputByType(firstLink.fromSlot.type) @@ -5204,12 +5185,7 @@ export class LGraphCanvas implements ConnectionColorContext { posY: number, ctx: CanvasRenderingContext2D, ): void { - const { linkConnector } = this - node.drawWidgets(ctx, { - colorContext: this, - linkOverWidget: linkConnector.overWidget, - linkOverWidgetType: linkConnector.overWidgetType, lowQuality: this.low_quality, editorAlpha: this.editor_alpha, }) diff --git a/src/LGraphNode.ts b/src/LGraphNode.ts index 0fe072f47..84ad98913 100644 --- a/src/LGraphNode.ts +++ b/src/LGraphNode.ts @@ -1585,7 +1585,7 @@ export class LGraphNode implements Positionable, IPinnable, IColorable { let widgets_height = 0 if (this.widgets?.length) { for (const widget of this.widgets) { - if (widget.hidden || (widget.advanced && !this.showAdvanced)) continue + if (!this.isWidgetVisible(widget)) continue let widget_height = 0 if (widget.computeSize) { @@ -1929,9 +1929,8 @@ export class LGraphNode implements Positionable, IPinnable, IColorable { for (const widget of widgets) { if ( - (widget.disabled && !includeDisabled) || - widget.hidden || - (widget.advanced && !this.showAdvanced) + (widget.computedDisabled && !includeDisabled) || + !this.isWidgetVisible(widget) ) { continue } @@ -3364,16 +3363,25 @@ export class LGraphNode implements Positionable, IPinnable, IColorable { } } + /** + * Returns `true` if the widget is visible, otherwise `false`. + */ + isWidgetVisible(widget: IWidget): boolean { + const isHidden = ( + this.collapsed || + widget.hidden || + (widget.advanced && !this.showAdvanced) + ) + return !isHidden + } + drawWidgets(ctx: CanvasRenderingContext2D, options: { - colorContext: ConnectionColorContext - linkOverWidget: IWidget | null | undefined - linkOverWidgetType?: ISlotType lowQuality?: boolean editorAlpha?: number }): void { if (!this.widgets) return - const { colorContext, linkOverWidget, linkOverWidgetType, lowQuality = false, editorAlpha = 1 } = options + const { lowQuality = false, editorAlpha = 1 } = options const width = this.size[0] const widgets = this.widgets @@ -3384,25 +3392,19 @@ export class LGraphNode implements Positionable, IPinnable, IColorable { const margin = 15 for (const w of widgets) { - if (w.hidden || (w.advanced && !this.showAdvanced)) continue + if (!this.isWidgetVisible(w)) continue + const y = w.y const outline_color = w.advanced ? LiteGraph.WIDGET_ADVANCED_OUTLINE_COLOR : LiteGraph.WIDGET_OUTLINE_COLOR - if (w === linkOverWidget) { - // Manually draw a slot next to the widget simulating an input - new NodeInputSlot({ - name: "", - // @ts-expect-error https://github.com/Comfy-Org/litegraph.js/issues/616 - type: linkOverWidgetType, - link: 0, - }).draw(ctx, { pos: [10, y + 10], colorContext }) - } - w.last_y = y + // Disable widget if it is disabled or if the value is passed from socket connection. + w.computedDisabled = w.disabled || this.getSlotFromWidget(w)?.link != null + ctx.strokeStyle = outline_color ctx.fillStyle = "#222" ctx.textAlign = "left" - if (w.disabled) ctx.globalAlpha *= 0.5 + if (w.computedDisabled) ctx.globalAlpha *= 0.5 const widget_width = w.width || width const WidgetClass: typeof WIDGET_TYPE_MAP[string] = WIDGET_TYPE_MAP[w.type] @@ -3524,6 +3526,25 @@ export class LGraphNode implements Positionable, IPinnable, IColorable { return this.#getMouseOverSlot(slot) === slot } + #isMouseOverWidget(widget: IWidget): boolean { + return this.mouseOver?.overWidget === widget + } + + /** + * Returns the input slot that is associated with the given widget. + */ + getSlotFromWidget(widget: IWidget): INodeInputSlot | undefined { + return this.inputs.find(slot => isWidgetInputSlot(slot) && slot.widget.name === widget.name) + } + + /** + * Returns the widget that is associated with the given input slot. + */ + getWidgetFromSlot(slot: INodeInputSlot): IWidget | undefined { + if (!isWidgetInputSlot(slot)) return + return this.widgets?.find(w => w.name === slot.widget.name) + } + /** * Draws the node's input and output slots. */ @@ -3544,7 +3565,18 @@ export class LGraphNode implements Positionable, IPinnable, IColorable { const labelColor = highlight ? this.highlightColor : LiteGraph.NODE_TEXT_COLOR - ctx.globalAlpha = isValid ? editorAlpha : 0.4 * editorAlpha + + // Show slot if it's not a widget input slot + // or if it's a widget input slot and satisfies one of the following: + // - the mouse is over the widget + // - the slot is valid during link drop + // - the slot is connected + const showSlot = !isWidgetInputSlot(slot) || + this.#isMouseOverWidget(this.getWidgetFromSlot(slot)!) || + (fromSlot && slotInstance.isValidTarget(fromSlot)) || + slotInstance.isConnected() + + ctx.globalAlpha = showSlot ? (isValid ? editorAlpha : 0.4 * editorAlpha) : 0 slotInstance.draw(ctx, { pos: layoutElement?.center ?? [0, 0], diff --git a/src/NodeSlot.ts b/src/NodeSlot.ts index 9b962afab..0f5a11937 100644 --- a/src/NodeSlot.ts +++ b/src/NodeSlot.ts @@ -214,7 +214,8 @@ export abstract class NodeSlot implements INodeSlot { if (!lowQuality && doStroke) ctx.stroke() // render slot label - if (!lowQuality) { + const hideLabel = lowQuality || isWidgetInputSlot(this) + if (!hideLabel) { const text = this.renderingLabel if (text) { // TODO: Finish impl. Highlight text on mouseover unless we're connecting links. diff --git a/src/types/widgets.ts b/src/types/widgets.ts index fe7431828..0a9c36cd8 100644 --- a/src/types/widgets.ts +++ b/src/types/widgets.ts @@ -153,24 +153,38 @@ export interface IBaseWidget { /** * The computed height of the widget. Used by customized node resize logic. * See scripts/domWidget.ts for more details. + * @readonly [Computed] This property is computed by the node. */ computedHeight?: number /** * The starting y position of the widget after layout. + * @readonly [Computed] This property is computed by the node. */ y: number /** * The y position of the widget after drawing (rendering). + * @readonly [Computed] This property is computed by the node. * @deprecated There is no longer dynamic y adjustment on rendering anymore. * Use {@link IBaseWidget.y} instead. */ last_y?: number width?: number + /** + * Whether the widget is disabled. Disabled widgets are rendered at half opacity. + * See also {@link IBaseWidget.computedDisabled}. + */ disabled?: boolean + /** + * The disabled state used for rendering based on various conditions including + * {@link IBaseWidget.disabled}. + * @readonly [Computed] This property is computed by the node. + */ + computedDisabled?: boolean + hidden?: boolean advanced?: boolean diff --git a/src/widgets/BaseWidget.ts b/src/widgets/BaseWidget.ts index 2ca7b9401..0fb59d981 100644 --- a/src/widgets/BaseWidget.ts +++ b/src/widgets/BaseWidget.ts @@ -23,6 +23,7 @@ export abstract class BaseWidget implements IBaseWidget { last_y?: number width?: number disabled?: boolean + computedDisabled?: boolean hidden?: boolean advanced?: boolean tooltip?: string diff --git a/src/widgets/BooleanWidget.ts b/src/widgets/BooleanWidget.ts index b0dad4231..e87cc684e 100644 --- a/src/widgets/BooleanWidget.ts +++ b/src/widgets/BooleanWidget.ts @@ -33,7 +33,7 @@ export class BooleanWidget extends BaseWidget implements IBooleanWidget { ctx.roundRect(margin, y, width - margin * 2, height, [height * 0.5]) else ctx.rect(margin, y, width - margin * 2, height) ctx.fill() - if (show_text && !this.disabled) ctx.stroke() + if (show_text && !this.computedDisabled) ctx.stroke() ctx.fillStyle = this.value ? "#89A" : "#333" ctx.beginPath() ctx.arc( diff --git a/src/widgets/ButtonWidget.ts b/src/widgets/ButtonWidget.ts index 2f0410072..8685056e7 100644 --- a/src/widgets/ButtonWidget.ts +++ b/src/widgets/ButtonWidget.ts @@ -45,7 +45,7 @@ export class ButtonWidget extends BaseWidget implements IButtonWidget { ctx.fillRect(margin, y, width - margin * 2, height) // Draw button outline if not disabled - if (show_text && !this.disabled) { + if (show_text && !this.computedDisabled) { ctx.strokeStyle = this.outline_color ctx.strokeRect(margin, y, width - margin * 2, height) } diff --git a/src/widgets/ComboWidget.ts b/src/widgets/ComboWidget.ts index 0d4bd53cc..ddc7b7b76 100644 --- a/src/widgets/ComboWidget.ts +++ b/src/widgets/ComboWidget.ts @@ -49,7 +49,7 @@ export class ComboWidget extends BaseWidget implements IComboWidget { ctx.fill() if (show_text) { - if (!this.disabled) { + if (!this.computedDisabled) { ctx.stroke() // Draw left arrow ctx.fillStyle = this.text_color diff --git a/src/widgets/KnobWidget.ts b/src/widgets/KnobWidget.ts index f7536618a..f01b5e861 100644 --- a/src/widgets/KnobWidget.ts +++ b/src/widgets/KnobWidget.ts @@ -158,7 +158,7 @@ export class KnobWidget extends BaseWidget implements IKnobWidget { ctx.closePath() // Draw outline if not disabled - if (show_text && !this.disabled) { + if (show_text && !this.computedDisabled) { ctx.strokeStyle = this.outline_color // Draw value ctx.beginPath() diff --git a/src/widgets/NumberWidget.ts b/src/widgets/NumberWidget.ts index 8622fbc67..9a95a51e0 100644 --- a/src/widgets/NumberWidget.ts +++ b/src/widgets/NumberWidget.ts @@ -64,7 +64,7 @@ export class NumberWidget extends BaseWidget implements INumericWidget { ctx.fill() if (show_text) { - if (!this.disabled) { + if (!this.computedDisabled) { ctx.stroke() // Draw left arrow ctx.fillStyle = this.text_color diff --git a/src/widgets/SliderWidget.ts b/src/widgets/SliderWidget.ts index be9fabd59..3c04f2260 100644 --- a/src/widgets/SliderWidget.ts +++ b/src/widgets/SliderWidget.ts @@ -54,7 +54,7 @@ export class SliderWidget extends BaseWidget implements ISliderWidget { ctx.fillRect(margin, y, nvalue * (width - margin * 2), height) // Draw outline if not disabled - if (show_text && !this.disabled) { + if (show_text && !this.computedDisabled) { ctx.strokeStyle = this.outline_color ctx.strokeRect(margin, y, width - margin * 2, height) } diff --git a/src/widgets/TextWidget.ts b/src/widgets/TextWidget.ts index 0c2d2b86b..41f097136 100644 --- a/src/widgets/TextWidget.ts +++ b/src/widgets/TextWidget.ts @@ -47,7 +47,7 @@ export class TextWidget extends BaseWidget implements IStringWidget { ctx.fill() if (show_text) { - if (!this.disabled) ctx.stroke() + if (!this.computedDisabled) ctx.stroke() ctx.save() ctx.beginPath() ctx.rect(margin, y, width - margin * 2, height)