mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-02 22:37:32 +00:00
260 lines
7.8 KiB
TypeScript
260 lines
7.8 KiB
TypeScript
import type { CanvasPointer, LGraphCanvas, LGraphNode, Size } from "@/litegraph"
|
|
import type { CanvasMouseEvent, CanvasPointerEvent } from "@/types/events"
|
|
import type { IBaseWidget, IWidget } from "@/types/widgets"
|
|
|
|
import { drawTextInArea } from "@/draw"
|
|
import { Rectangle } from "@/infrastructure/Rectangle"
|
|
import { Point } from "@/interfaces"
|
|
import { LiteGraph } from "@/litegraph"
|
|
|
|
export interface DrawWidgetOptions {
|
|
/** The width of the node where this widget will be displayed. */
|
|
width: number
|
|
/** Synonym for "low quality". */
|
|
showText?: boolean
|
|
}
|
|
|
|
export interface DrawTruncatingTextOptions extends DrawWidgetOptions {
|
|
/** The canvas context to draw the text on. */
|
|
ctx: CanvasRenderingContext2D
|
|
/** The amount of padding to add to the left of the text. */
|
|
leftPadding?: number
|
|
/** The amount of padding to add to the right of the text. */
|
|
rightPadding?: number
|
|
}
|
|
|
|
export interface WidgetEventOptions {
|
|
e: CanvasMouseEvent
|
|
node: LGraphNode
|
|
canvas: LGraphCanvas
|
|
}
|
|
|
|
export abstract class BaseWidget<TWidget extends IWidget = IWidget> implements IBaseWidget {
|
|
/** From node edge to widget edge */
|
|
static margin = 15
|
|
/** From widget edge to tip of arrow button */
|
|
static arrowMargin = 6
|
|
/** Arrow button width */
|
|
static arrowWidth = 10
|
|
/** Absolute minimum display width of widget values */
|
|
static minValueWidth = 42
|
|
/** Minimum gap between label and value */
|
|
static labelValueGap = 5
|
|
|
|
linkedWidgets?: IWidget[]
|
|
name: string
|
|
options: TWidget["options"]
|
|
label?: string
|
|
type: TWidget["type"]
|
|
value: TWidget["value"]
|
|
y: number = 0
|
|
last_y?: number
|
|
width?: number
|
|
disabled?: boolean
|
|
computedDisabled?: 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: TWidget) {
|
|
// TODO: Resolve this workaround. Ref: https://github.com/Comfy-Org/litegraph.js/issues/1022
|
|
// @ts-expect-error Prevent naming conflicts with custom nodes.
|
|
// eslint-disable-next-line unused-imports/no-unused-vars
|
|
const { outline_color, background_color, height, text_color, secondary_text_color, disabledTextColor, displayName, displayValue, labelBaseline, ...safeValues } = widget
|
|
|
|
Object.assign(this, safeValues)
|
|
this.name = widget.name
|
|
this.options = widget.options
|
|
this.type = widget.type
|
|
this.value = widget.value
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
get disabledTextColor() {
|
|
return LiteGraph.WIDGET_DISABLED_TEXT_COLOR
|
|
}
|
|
|
|
get displayName() {
|
|
return this.label || this.name
|
|
}
|
|
|
|
get displayValue(): string {
|
|
return String(this.value)
|
|
}
|
|
|
|
get labelBaseline() {
|
|
return this.y + this.height * 0.7
|
|
}
|
|
|
|
/**
|
|
* Draws the widget
|
|
* @param ctx The canvas context
|
|
* @param options The options for drawing the widget
|
|
* @remarks Not naming this `draw` as `draw` conflicts with the `draw` method in
|
|
* custom widgets.
|
|
*/
|
|
abstract drawWidget(ctx: CanvasRenderingContext2D, options: DrawWidgetOptions): void
|
|
|
|
/**
|
|
* Draws the standard widget shape - elongated capsule. The path of the widget shape is not
|
|
* cleared, and may be used for further drawing.
|
|
* @param ctx The canvas context
|
|
* @param options The options for drawing the widget
|
|
* @remarks Leaves {@link ctx} dirty.
|
|
*/
|
|
protected drawWidgetShape(ctx: CanvasRenderingContext2D, { width, showText }: DrawWidgetOptions) {
|
|
const { height, y } = this
|
|
const { margin } = BaseWidget
|
|
|
|
ctx.textAlign = "left"
|
|
ctx.strokeStyle = this.outline_color
|
|
ctx.fillStyle = this.background_color
|
|
ctx.beginPath()
|
|
|
|
if (showText) {
|
|
ctx.roundRect(margin, y, width - margin * 2, height, [height * 0.5])
|
|
} else {
|
|
ctx.rect(margin, y, width - margin * 2, height)
|
|
}
|
|
ctx.fill()
|
|
if (showText && !this.computedDisabled) ctx.stroke()
|
|
}
|
|
|
|
/**
|
|
* A shared routine for drawing a label and value as text, truncated
|
|
* if they exceed the available width.
|
|
*/
|
|
protected drawTruncatingText({
|
|
ctx,
|
|
width,
|
|
leftPadding = 5,
|
|
rightPadding = 20,
|
|
}: DrawTruncatingTextOptions) {
|
|
const { height, y } = this
|
|
const { margin } = BaseWidget
|
|
|
|
// Measure label and value
|
|
const { displayName, displayValue } = this
|
|
const labelWidth = ctx.measureText(displayName).width
|
|
const valueWidth = ctx.measureText(displayValue).width
|
|
|
|
const gap = BaseWidget.labelValueGap
|
|
const x = margin * 2 + leftPadding
|
|
|
|
const totalWidth = width - x - 2 * margin - rightPadding
|
|
const requiredWidth = labelWidth + gap + valueWidth
|
|
|
|
const area = new Rectangle(x, y, totalWidth, height * 0.7)
|
|
|
|
ctx.fillStyle = this.secondary_text_color
|
|
|
|
if (requiredWidth <= totalWidth) {
|
|
// Draw label & value normally
|
|
drawTextInArea({ ctx, text: displayName, area, align: "left" })
|
|
} else if (LiteGraph.truncateWidgetTextEvenly) {
|
|
// Label + value will not fit - scale evenly to fit
|
|
const scale = (totalWidth - gap) / (requiredWidth - gap)
|
|
area.width = labelWidth * scale
|
|
|
|
drawTextInArea({ ctx, text: displayName, area, align: "left" })
|
|
|
|
// Move the area to the right to render the value
|
|
area.right = x + totalWidth
|
|
area.setWidthRightAnchored(valueWidth * scale)
|
|
} else if (LiteGraph.truncateWidgetValuesFirst) {
|
|
// Label + value will not fit - use legacy scaling of value first
|
|
const cappedLabelWidth = Math.min(labelWidth, totalWidth)
|
|
|
|
area.width = cappedLabelWidth
|
|
drawTextInArea({ ctx, text: displayName, area, align: "left" })
|
|
|
|
area.right = x + totalWidth
|
|
area.setWidthRightAnchored(Math.max(totalWidth - gap - cappedLabelWidth, 0))
|
|
} else {
|
|
// Label + value will not fit - scale label first
|
|
const cappedValueWidth = Math.min(valueWidth, totalWidth)
|
|
|
|
area.width = Math.max(totalWidth - gap - cappedValueWidth, 0)
|
|
drawTextInArea({ ctx, text: displayName, area, align: "left" })
|
|
|
|
area.right = x + totalWidth
|
|
area.setWidthRightAnchored(cappedValueWidth)
|
|
}
|
|
ctx.fillStyle = this.text_color
|
|
drawTextInArea({ ctx, text: displayValue, area, align: "right" })
|
|
}
|
|
|
|
/**
|
|
* Handles the click event for the widget
|
|
* @param options The options for handling the click event
|
|
*/
|
|
abstract onClick(options: WidgetEventOptions): void
|
|
|
|
/**
|
|
* Handles the drag event for the widget
|
|
* @param options The options for handling the drag event
|
|
*/
|
|
onDrag?(options: WidgetEventOptions): void
|
|
|
|
/**
|
|
* Sets the value of the widget
|
|
* @param value The value to set
|
|
* @param options The options for setting the value
|
|
*/
|
|
setValue(value: TWidget["value"], { e, node, canvas }: WidgetEventOptions) {
|
|
const oldValue = this.value
|
|
if (value === this.value) return
|
|
|
|
const v = this.type === "number" ? Number(value) : value
|
|
this.value = v
|
|
if (
|
|
this.options?.property &&
|
|
node.properties[this.options.property] !== undefined
|
|
) {
|
|
node.setProperty(this.options.property, v)
|
|
}
|
|
const pos = canvas.graph_mouse
|
|
this.callback?.(this.value, canvas, node, pos, e)
|
|
|
|
node.onWidgetChanged?.(this.name ?? "", v, oldValue, this as IWidget)
|
|
if (node.graph) node.graph._version++
|
|
}
|
|
}
|