Widget overhaul (#1010)

### Widget text overhaul

#### Current
- Numbers and text overlap
- Combo boxes truncate the value before the label

![image](https://github.com/user-attachments/assets/c991b0b6-879f-4455-92d4-4254ef25b55c)

#### 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:
filtered
2025-05-05 10:48:06 +10:00
committed by GitHub
parent 406abd7731
commit 75df19521b
14 changed files with 591 additions and 261 deletions

View File

@@ -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