Fade invalid increment/decrement widget buttons (#978)

When a widget cannot be incremented/decremented further, show a disabled
effect on the widget buttons.


https://github.com/user-attachments/assets/7770a9f1-31f6-430b-ab02-420327621148
This commit is contained in:
filtered
2025-04-28 05:47:48 +10:00
committed by GitHub
parent 85b1534263
commit ee222c794b
6 changed files with 160 additions and 64 deletions

View File

@@ -66,6 +66,7 @@ export class LiteGraphGlobal {
WIDGET_ADVANCED_OUTLINE_COLOR = "rgba(56, 139, 253, 0.8)"
WIDGET_TEXT_COLOR = "#DDD"
WIDGET_SECONDARY_TEXT_COLOR = "#999"
WIDGET_DISABLED_TEXT_COLOR = "#666"
LINK_COLOR = "#9A9"
EVENT_LINK_COLOR = "#A86"

View File

@@ -0,0 +1,57 @@
import { BaseWidget, type WidgetEventOptions } from "./BaseWidget"
/**
* Base class for widgets that have increment and decrement buttons.
*/
export abstract class BaseSteppedWidget extends BaseWidget {
/**
* Whether the widget can increment its value
* @returns `true` if the widget can increment its value, otherwise `false`
*/
abstract canIncrement(): boolean
/**
* Whether the widget can decrement its value
* @returns `true` if the widget can decrement its value, otherwise `false`
*/
abstract canDecrement(): boolean
/**
* Increment the value of the widget
* @param options The options for the widget event
*/
abstract incrementValue(options: WidgetEventOptions): void
/**
* Decrement the value of the widget
* @param options The options for the widget event
*/
abstract decrementValue(options: WidgetEventOptions): void
/**
* Draw the arrow buttons for the widget
* @param ctx The canvas rendering context
* @param margin The margin of the widget
* @param y The y position of the widget
* @param width The width of the widget
*/
drawArrowButtons(ctx: CanvasRenderingContext2D, margin: number, y: number, width: number) {
const { height, text_color, disabledTextColor } = this
const { arrowMargin, arrowWidth } = BaseWidget
const arrowTipX = margin + arrowMargin
const arrowInnerX = arrowTipX + arrowWidth
// Draw left arrow
ctx.fillStyle = this.canDecrement() ? text_color : disabledTextColor
ctx.beginPath()
ctx.moveTo(arrowInnerX, y + 5)
ctx.lineTo(arrowTipX, y + height * 0.5)
ctx.lineTo(arrowInnerX, y + height - 5)
ctx.fill()
// Draw right arrow
ctx.fillStyle = this.canIncrement() ? text_color : disabledTextColor
ctx.beginPath()
ctx.moveTo(width - arrowInnerX, y + 5)
ctx.lineTo(width - arrowTipX, y + height * 0.5)
ctx.lineTo(width - arrowInnerX, y + height - 5)
ctx.fill()
}
}

View File

@@ -88,6 +88,10 @@ export abstract class BaseWidget implements IBaseWidget {
return LiteGraph.WIDGET_SECONDARY_TEXT_COLOR
}
get disabledTextColor() {
return LiteGraph.WIDGET_DISABLED_TEXT_COLOR
}
/**
* Draws the widget
* @param ctx The canvas context
@@ -97,28 +101,6 @@ export abstract class BaseWidget implements IBaseWidget {
*/
abstract drawWidget(ctx: CanvasRenderingContext2D, options: DrawWidgetOptions): void
drawArrowButtons(ctx: CanvasRenderingContext2D, margin: number, y: number, width: number) {
const { height } = this
const { arrowMargin, arrowWidth } = BaseWidget
const arrowTipX = margin + arrowMargin
const arrowInnerX = arrowTipX + arrowWidth
// Draw left arrow
ctx.fillStyle = this.text_color
ctx.beginPath()
ctx.moveTo(arrowInnerX, y + 5)
ctx.lineTo(arrowTipX, y + height * 0.5)
ctx.lineTo(arrowInnerX, y + height - 5)
ctx.fill()
// Draw right arrow
ctx.beginPath()
ctx.moveTo(width - arrowInnerX, y + 5)
ctx.lineTo(width - arrowTipX, y + height * 0.5)
ctx.lineTo(width - arrowInnerX, y + height - 5)
ctx.fill()
}
/**
* Handles the click event for the widget
* @param options The options for handling the click event

View File

@@ -1,14 +1,22 @@
import type { IComboWidget, IWidgetOptions } from "@/types/widgets"
import { LiteGraph } from "@/litegraph"
import { clamp, LiteGraph } from "@/litegraph"
import { BaseSteppedWidget } from "./BaseSteppedWidget"
import { BaseWidget, type DrawWidgetOptions, type WidgetEventOptions } from "./BaseWidget"
export class ComboWidget extends BaseWidget implements IComboWidget {
type Values = string[] | Record<string, string>
function toArray(values: Values): string[] {
return Array.isArray(values) ? values : Object.keys(values)
}
export class ComboWidget extends BaseSteppedWidget implements IComboWidget {
// IComboWidget properties
declare type: "combo"
declare value: string | number
declare options: IWidgetOptions<string>
// @ts-expect-error Workaround for Record<string, string> not being typed in IWidgetOptions
declare options: Omit<IWidgetOptions<string>, "values"> & { values: Values }
constructor(widget: IComboWidget) {
super(widget)
@@ -16,6 +24,66 @@ export class ComboWidget extends BaseWidget implements IComboWidget {
this.value = widget.value
}
#getValues(): Values {
const { values } = this.options
if (values == null) throw new Error("[ComboWidget]: values is required")
return typeof values === "function"
// @ts-expect-error handle () => string[] type that is not typed in IWidgetOptions
? values(this, node)
: values
}
/**
* Checks if the value is {@link Array.at at} the given index in the combo list.
* @param index The index to check against
* @returns `true` if the value is at the given index, otherwise `false`.
*/
#valueIsAt(index: number): boolean {
const { values } = this.options
// If using legacy duck-typed method, just return true
if (typeof values === "function") return true
const valuesArray = toArray(values)
return this.value === valuesArray.at(index)
}
override canIncrement(): boolean {
return !this.#valueIsAt(-1)
}
override canDecrement(): boolean {
return !this.#valueIsAt(0)
}
override incrementValue(options: WidgetEventOptions): void {
this.#tryChangeValue(1, options)
}
override decrementValue(options: WidgetEventOptions): void {
this.#tryChangeValue(-1, options)
}
#tryChangeValue(delta: number, options: WidgetEventOptions): void {
const values = this.#getValues()
const indexedValues = toArray(values)
// avoids double click event
options.canvas.last_mouseclick = 0
const foundIndex = typeof values === "object"
? indexedValues.indexOf(String(this.value)) + delta
// @ts-expect-error handle non-string values
: indexedValues.indexOf(this.value) + delta
const index = clamp(foundIndex, 0, indexedValues.length - 1)
const value = Array.isArray(values)
? values[index]
: index
this.setValue(value, options)
}
/**
* Draws the widget
* @param ctx The canvas context
@@ -123,45 +191,14 @@ export class ComboWidget extends BaseWidget implements IComboWidget {
const width = this.width || node.size[0]
// Determine if clicked on left/right arrows
const delta = x < 40
? -1
: (x > width - 40
? 1
: 0)
if (x < 40) return this.decrementValue({ e, node, canvas })
if (x > width - 40) return this.incrementValue({ e, node, canvas })
// 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-expect-error 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
// avoids double click event
canvas.last_mouseclick = 0
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
}
// Otherwise, show dropdown menu
const values = this.#getValues()
const values_list = toArray(values)
// Handle center click - show dropdown menu
// @ts-expect-error 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),

View File

@@ -2,9 +2,10 @@ import type { INumericWidget, IWidgetOptions } from "@/types/widgets"
import { getWidgetStep } from "@/utils/widget"
import { BaseSteppedWidget } from "./BaseSteppedWidget"
import { BaseWidget, type DrawWidgetOptions, type WidgetEventOptions } from "./BaseWidget"
export class NumberWidget extends BaseWidget implements INumericWidget {
export class NumberWidget extends BaseSteppedWidget implements INumericWidget {
// INumberWidget properties
declare type: "number"
declare value: number
@@ -16,6 +17,24 @@ export class NumberWidget extends BaseWidget implements INumericWidget {
this.value = widget.value
}
override canIncrement(): boolean {
const { max } = this.options
return max == null || this.value < max
}
override canDecrement(): boolean {
const { min } = this.options
return min == null || this.value > min
}
override incrementValue(options: WidgetEventOptions): void {
this.setValue(this.value + getWidgetStep(this.options), options)
}
override decrementValue(options: WidgetEventOptions): void {
this.setValue(this.value - getWidgetStep(this.options), options)
}
override setValue(value: number, options: WidgetEventOptions) {
let newValue = value
if (this.options.min != null && newValue < this.options.min) {
@@ -126,8 +145,7 @@ export class NumberWidget extends BaseWidget implements INumericWidget {
* Handles drag events for the number widget
* @param options The options for handling the drag event
*/
override onDrag(options: WidgetEventOptions) {
const { e, node, canvas } = options
override onDrag({ e, node, canvas }: WidgetEventOptions) {
const width = this.width || node.width
const x = e.canvasX - node.pos[0]
const delta = x < 40

View File

@@ -133,6 +133,7 @@ LiteGraphGlobal {
"VERTICAL_LAYOUT": "vertical",
"WIDGET_ADVANCED_OUTLINE_COLOR": "rgba(56, 139, 253, 0.8)",
"WIDGET_BGCOLOR": "#222",
"WIDGET_DISABLED_TEXT_COLOR": "#666",
"WIDGET_OUTLINE_COLOR": "#666",
"WIDGET_SECONDARY_TEXT_COLOR": "#999",
"WIDGET_TEXT_COLOR": "#DDD",