mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-21 23:09:39 +00:00
Widget overhaul (#1010)
### Widget text overhaul #### Current - Numbers and text overlap - Combo boxes truncate the value before the label  #### Proposed **By default, widgets will now truncate their labels before their values.** https://github.com/user-attachments/assets/296ea5ab-d2ff-44f2-9139-5d97789e4f12 - Changes the way widget text is rendered, calculated, and truncated - Truncation now applies in a standard way to the following widgets: - Text - Combo - Number - Centralises widget draw routines in base class ### Config ```ts // Truncate **both** widgets and labels evenly LiteGraph.truncateWidgetTextEvenly = true // Swap the default from truncating labels before values, to truncating values first (restores legacy behaviour) // truncateWidgetTextEvenly **must** be `false`. LiteGraph.truncateWidgetValuesFirst = true ``` ### API / interfaces - Adds rich `Rectangle` concrete impl., with many methods and helpful accessors (e.g. `right`, `bottom`) - Actually _improves_ performance due to switch from Float32Array to Float64Array - Impact vs plain Float64Array was not detectable outside of a 2M+ instantiation-loop with random data - Lazy `pos` & `size` `subarray` properties - Adds `ReadOnlySize` - Adds higher-level text draw functions to abstract the nitty gritty in a performant way (binary search) - Resolves Comfy-Org/ComfyUI_frontend/issues/457
This commit is contained in:
@@ -2,14 +2,27 @@ import type { CanvasPointer, LGraphCanvas, LGraphNode, Size } from "@/litegraph"
|
||||
import type { CanvasMouseEvent, CanvasPointerEvent } from "@/types/events"
|
||||
import type { IBaseWidget, IWidget, IWidgetOptions, TWidgetType, TWidgetValue } 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
|
||||
@@ -25,6 +38,8 @@ export abstract class BaseWidget implements IBaseWidget {
|
||||
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
|
||||
@@ -90,6 +105,18 @@ export abstract class BaseWidget implements IBaseWidget {
|
||||
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
|
||||
@@ -99,6 +126,95 @@ export abstract class BaseWidget implements IBaseWidget {
|
||||
*/
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user