From ee222c794b70e3d83741e0b81fec293b06565f0c Mon Sep 17 00:00:00 2001 From: filtered <176114999+webfiltered@users.noreply.github.com> Date: Mon, 28 Apr 2025 05:47:48 +1000 Subject: [PATCH] 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 --- src/LiteGraphGlobal.ts | 1 + src/widgets/BaseSteppedWidget.ts | 57 +++++++++++ src/widgets/BaseWidget.ts | 26 +---- src/widgets/ComboWidget.ts | 115 ++++++++++++++-------- src/widgets/NumberWidget.ts | 24 ++++- test/__snapshots__/litegraph.test.ts.snap | 1 + 6 files changed, 160 insertions(+), 64 deletions(-) create mode 100644 src/widgets/BaseSteppedWidget.ts diff --git a/src/LiteGraphGlobal.ts b/src/LiteGraphGlobal.ts index 6e0be273b9..c3ba12401a 100644 --- a/src/LiteGraphGlobal.ts +++ b/src/LiteGraphGlobal.ts @@ -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" diff --git a/src/widgets/BaseSteppedWidget.ts b/src/widgets/BaseSteppedWidget.ts new file mode 100644 index 0000000000..434fcc64d8 --- /dev/null +++ b/src/widgets/BaseSteppedWidget.ts @@ -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() + } +} diff --git a/src/widgets/BaseWidget.ts b/src/widgets/BaseWidget.ts index a3cac25cfd..fefd6b0b41 100644 --- a/src/widgets/BaseWidget.ts +++ b/src/widgets/BaseWidget.ts @@ -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 diff --git a/src/widgets/ComboWidget.ts b/src/widgets/ComboWidget.ts index e2edffc873..4de0a816c2 100644 --- a/src/widgets/ComboWidget.ts +++ b/src/widgets/ComboWidget.ts @@ -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 + +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 + // @ts-expect-error Workaround for Record not being typed in IWidgetOptions + declare options: Omit, "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 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 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), diff --git a/src/widgets/NumberWidget.ts b/src/widgets/NumberWidget.ts index ed4eb8f31f..cc51eb9ff4 100644 --- a/src/widgets/NumberWidget.ts +++ b/src/widgets/NumberWidget.ts @@ -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 diff --git a/test/__snapshots__/litegraph.test.ts.snap b/test/__snapshots__/litegraph.test.ts.snap index 177c6e896e..ca0dc56f1e 100644 --- a/test/__snapshots__/litegraph.test.ts.snap +++ b/test/__snapshots__/litegraph.test.ts.snap @@ -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",