mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-02 22:37:32 +00:00
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
206
src/widgets/ComboWidget.ts
Normal file
206
src/widgets/ComboWidget.ts
Normal file
@@ -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<string>
|
||||
|
||||
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<string, string> 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<string, string> 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,
|
||||
},
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user