| import { app } from "scripts/app.js";
|
| import type {
|
| IWidget,
|
| LGraphNode,
|
| LGraphCanvas as TLGraphCanvas,
|
| Vector2,
|
| AdjustedMouseEvent,
|
| Vector4,
|
| } from "../typings/litegraph.js";
|
| import { drawNodeWidget, drawRoundedRectangle, fitString, isLowQuality } from "./utils_canvas.js";
|
|
|
| |
| |
|
|
| export function drawLabelAndValue(
|
| ctx: CanvasRenderingContext2D,
|
| label: string,
|
| value: string,
|
| width: number,
|
| posY: number,
|
| height: number,
|
| options?: { offsetLeft: number },
|
| ) {
|
| const outerMargin = 15;
|
| const innerMargin = 10;
|
| const midY = posY + height / 2;
|
| ctx.save();
|
| ctx.textAlign = "left";
|
| ctx.textBaseline = "middle";
|
| ctx.fillStyle = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR;
|
| const labelX = outerMargin + innerMargin + (options?.offsetLeft ?? 0);
|
| ctx.fillText(label, labelX, midY);
|
|
|
| const valueXLeft = labelX + ctx.measureText(label).width + 7;
|
| const valueXRight = width - (outerMargin + innerMargin);
|
|
|
| ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR;
|
| ctx.textAlign = "right";
|
| ctx.fillText(fitString(ctx, value, valueXRight - valueXLeft), valueXRight, midY);
|
| ctx.restore();
|
| }
|
|
|
| export type RgthreeBaseWidgetBounds = {
|
|
|
| bounds: Vector2 | Vector4;
|
| onDown?(event: AdjustedMouseEvent, pos: Vector2, node: LGraphNode): boolean | void;
|
| onDown?(
|
| event: AdjustedMouseEvent,
|
| pos: Vector2,
|
| node: LGraphNode,
|
| bounds: RgthreeBaseWidgetBounds,
|
| ): boolean | void;
|
| onUp?(event: AdjustedMouseEvent, pos: Vector2, node: LGraphNode): boolean | void;
|
| onUp?(
|
| event: AdjustedMouseEvent,
|
| pos: Vector2,
|
| node: LGraphNode,
|
| bounds: RgthreeBaseWidgetBounds,
|
| ): boolean | void;
|
| onMove?(event: AdjustedMouseEvent, pos: Vector2, node: LGraphNode): boolean | void;
|
| onMove?(
|
| event: AdjustedMouseEvent,
|
| pos: Vector2,
|
| node: LGraphNode,
|
| bounds: RgthreeBaseWidgetBounds,
|
| ): boolean | void;
|
| data?: any;
|
| };
|
|
|
| export type RgthreeBaseHitAreas<Keys extends string> = {
|
| [K in Keys]: RgthreeBaseWidgetBounds;
|
| };
|
|
|
| |
| |
|
|
| export abstract class RgthreeBaseWidget<T> implements IWidget<T, any> {
|
|
|
|
|
|
|
| abstract value: T extends Array<any> ? never : T;
|
|
|
| name: string;
|
| last_y: number = 0;
|
|
|
| protected mouseDowned: Vector2 | null = null;
|
| protected isMouseDownedAndOver: boolean = false;
|
|
|
|
|
| protected readonly hitAreas: RgthreeBaseHitAreas<any> = {};
|
| private downedHitAreasForMove: RgthreeBaseWidgetBounds[] = [];
|
|
|
| constructor(name: string) {
|
| this.name = name;
|
| }
|
|
|
| private clickWasWithinBounds(pos: Vector2, bounds: Vector2 | Vector4) {
|
| let xStart = bounds[0];
|
| let xEnd = xStart + (bounds.length > 2 ? bounds[2]! : bounds[1]!);
|
| const clickedX = pos[0] >= xStart && pos[0] <= xEnd;
|
| if (bounds.length === 2) {
|
| return clickedX;
|
| }
|
| return clickedX && pos[1] >= bounds[1] && pos[1] <= bounds[1] + bounds[3];
|
| }
|
|
|
| mouse(event: AdjustedMouseEvent, pos: Vector2, node: LGraphNode) {
|
| const canvas = app.canvas as TLGraphCanvas;
|
|
|
| if (event.type == "pointerdown") {
|
| this.mouseDowned = [...pos];
|
| this.isMouseDownedAndOver = true;
|
| this.downedHitAreasForMove.length = 0;
|
|
|
| let anyHandled = false;
|
| for (const part of Object.values(this.hitAreas)) {
|
| if ((part.onDown || part.onMove) && this.clickWasWithinBounds(pos, part.bounds)) {
|
| if (part.onMove) {
|
| this.downedHitAreasForMove.push(part);
|
| }
|
| if (part.onDown) {
|
| const thisHandled = part.onDown.apply(this, [event, pos, node, part]);
|
| anyHandled = anyHandled || thisHandled == true;
|
| }
|
| }
|
| }
|
| return this.onMouseDown(event, pos, node) ?? anyHandled;
|
| }
|
|
|
|
|
|
|
| if (event.type == "pointerup") {
|
| if (!this.mouseDowned) return true;
|
| this.downedHitAreasForMove.length = 0;
|
| this.cancelMouseDown();
|
| let anyHandled = false;
|
| for (const part of Object.values(this.hitAreas)) {
|
| if (part.onUp && this.clickWasWithinBounds(pos, part.bounds)) {
|
| const thisHandled = part.onUp.apply(this, [event, pos, node, part]);
|
| anyHandled = anyHandled || thisHandled == true;
|
| }
|
| }
|
| return this.onMouseUp(event, pos, node) ?? anyHandled;
|
| }
|
|
|
|
|
| if (event.type == "pointermove") {
|
| this.isMouseDownedAndOver = !!this.mouseDowned;
|
|
|
| if (
|
| this.mouseDowned &&
|
| (pos[0] < 15 ||
|
| pos[0] > node.size[0] - 15 ||
|
| pos[1] < this.last_y ||
|
| pos[1] > this.last_y + LiteGraph.NODE_WIDGET_HEIGHT)
|
| ) {
|
| this.isMouseDownedAndOver = false;
|
| }
|
| for (const part of this.downedHitAreasForMove) {
|
| part.onMove!.apply(this, [event, pos, node, part]);
|
| }
|
| return this.onMouseMove(event, pos, node) ?? true;
|
| }
|
| return false;
|
| }
|
|
|
|
|
| cancelMouseDown() {
|
| this.mouseDowned = null;
|
| this.isMouseDownedAndOver = false;
|
| this.downedHitAreasForMove.length = 0;
|
| }
|
|
|
|
|
| onMouseDown(event: AdjustedMouseEvent, pos: Vector2, node: LGraphNode): boolean | void {
|
| return;
|
| }
|
|
|
| |
| |
| |
|
|
| onMouseUp(event: AdjustedMouseEvent, pos: Vector2, node: LGraphNode): boolean | void {
|
| return;
|
| }
|
|
|
| |
| |
| |
| |
|
|
| onMouseMove(event: AdjustedMouseEvent, pos: Vector2, node: LGraphNode): boolean | void {
|
| return;
|
| }
|
| }
|
|
|
| |
| |
|
|
| export class RgthreeBetterButtonWidget extends RgthreeBaseWidget<string> {
|
| value: string = "";
|
| mouseUpCallback: (event: AdjustedMouseEvent, pos: Vector2, node: LGraphNode) => boolean | void;
|
|
|
| constructor(
|
| name: string,
|
| mouseUpCallback: (event: AdjustedMouseEvent, pos: Vector2, node: LGraphNode) => boolean | void,
|
| ) {
|
| super(name);
|
| this.mouseUpCallback = mouseUpCallback;
|
| }
|
|
|
| draw(ctx: CanvasRenderingContext2D, node: LGraphNode, width: number, y: number, height: number) {
|
| drawWidgetButton({ctx, node, width, height, y}, this.name, this.isMouseDownedAndOver);
|
| }
|
|
|
| override onMouseUp(event: AdjustedMouseEvent, pos: Vector2, node: LGraphNode) {
|
| return this.mouseUpCallback(event, pos, node);
|
| }
|
| }
|
|
|
| |
| |
|
|
| export class RgthreeBetterTextWidget implements IWidget<string> {
|
| name: string;
|
| value: string;
|
|
|
| constructor(name: string, value: string) {
|
| this.name = name;
|
| this.value = value;
|
| }
|
|
|
| draw(ctx: CanvasRenderingContext2D, node: LGraphNode, width: number, y: number, height: number) {
|
| const widgetData = drawNodeWidget(ctx, { width, height, posY: y });
|
|
|
| if (!widgetData.lowQuality) {
|
| drawLabelAndValue(ctx, this.name, this.value, width, y, height);
|
| }
|
| }
|
|
|
| mouse(event: MouseEvent, pos: Vector2, node: LGraphNode) {
|
| const canvas = app.canvas as TLGraphCanvas;
|
| if (event.type == "pointerdown") {
|
| canvas.prompt("Label", this.value, (v: string) => (this.value = v), event);
|
| return true;
|
| }
|
| return false;
|
| }
|
| }
|
|
|
| |
| |
|
|
| type RgthreeDividerWidgetOptions = {
|
| marginTop: number;
|
| marginBottom: number;
|
| marginLeft: number;
|
| marginRight: number;
|
| color: string;
|
| thickness: number;
|
| };
|
|
|
| |
| |
|
|
| export class RgthreeDividerWidget implements IWidget<null> {
|
| options = { serialize: false };
|
| value = null;
|
| name = "divider";
|
|
|
| private readonly widgetOptions: RgthreeDividerWidgetOptions = {
|
| marginTop: 7,
|
| marginBottom: 7,
|
| marginLeft: 15,
|
| marginRight: 15,
|
| color: LiteGraph.WIDGET_OUTLINE_COLOR,
|
| thickness: 1,
|
| };
|
|
|
| constructor(widgetOptions?: Partial<RgthreeDividerWidgetOptions>) {
|
| Object.assign(this.widgetOptions, widgetOptions || {});
|
| }
|
|
|
| draw(ctx: CanvasRenderingContext2D, node: LGraphNode, width: number, posY: number, h: number) {
|
| if (this.widgetOptions.thickness) {
|
| ctx.strokeStyle = this.widgetOptions.color;
|
| const x = this.widgetOptions.marginLeft;
|
| const y = posY + this.widgetOptions.marginTop;
|
| const w = width - this.widgetOptions.marginLeft - this.widgetOptions.marginRight;
|
| ctx.stroke(new Path2D(`M ${x} ${y} h ${w}`));
|
| }
|
| }
|
|
|
| computeSize(width: number): [number, number] {
|
| return [
|
| width,
|
| this.widgetOptions.marginTop + this.widgetOptions.marginBottom + this.widgetOptions.thickness,
|
| ];
|
| }
|
| }
|
|
|
| |
| |
|
|
| export type RgthreeLabelWidgetOptions = {
|
| align?: "left" | "center" | "right";
|
| color?: string;
|
| italic?: boolean;
|
| size?: number;
|
|
|
|
|
| actionLabel?: "__PLUS_ICON__" | string;
|
| actionCallback?: (event: PointerEvent) => void;
|
| };
|
|
|
| |
| |
|
|
| export class RgthreeLabelWidget implements IWidget<null> {
|
| options = { serialize: false };
|
| value = null;
|
| name: string;
|
|
|
| private readonly widgetOptions: RgthreeLabelWidgetOptions = {};
|
| private posY: number = 0;
|
|
|
| constructor(name: string, widgetOptions?: RgthreeLabelWidgetOptions) {
|
| this.name = name;
|
| Object.assign(this.widgetOptions, widgetOptions);
|
| }
|
|
|
| draw(
|
| ctx: CanvasRenderingContext2D,
|
| node: LGraphNode,
|
| width: number,
|
| posY: number,
|
| height: number,
|
| ) {
|
| this.posY = posY;
|
| ctx.save();
|
|
|
| ctx.textAlign = this.widgetOptions.align || "left";
|
| ctx.fillStyle = this.widgetOptions.color || LiteGraph.WIDGET_TEXT_COLOR;
|
| const oldFont = ctx.font;
|
| if (this.widgetOptions.italic) {
|
| ctx.font = "italic " + ctx.font;
|
| }
|
| if (this.widgetOptions.size) {
|
| ctx.font = ctx.font.replace(/\d+px/, `${this.widgetOptions.size}px`);
|
| }
|
|
|
| const midY = posY + height / 2;
|
| ctx.textBaseline = "middle";
|
|
|
| if (this.widgetOptions.align === "center") {
|
| ctx.fillText(this.name, node.size[0] / 2, midY);
|
| } else {
|
| ctx.fillText(this.name, 15, midY);
|
| }
|
|
|
| ctx.font = oldFont;
|
|
|
| if (this.widgetOptions.actionLabel === "__PLUS_ICON__") {
|
| const plus = new Path2D(
|
| `M${node.size[0] - 15 - 2} ${posY + 7} v4 h-4 v4 h-4 v-4 h-4 v-4 h4 v-4 h4 v4 h4 z`,
|
| );
|
| ctx.lineJoin = "round";
|
| ctx.lineCap = "round";
|
| ctx.fillStyle = "#3a3";
|
| ctx.strokeStyle = "#383";
|
| ctx.fill(plus);
|
| ctx.stroke(plus);
|
| }
|
| ctx.restore();
|
| }
|
|
|
| mouse(event: PointerEvent, nodePos: Vector2, node: LGraphNode) {
|
| if (
|
| event.type !== "pointerdown" ||
|
| isLowQuality() ||
|
| !this.widgetOptions.actionLabel ||
|
| !this.widgetOptions.actionCallback
|
| ) {
|
| return false;
|
| }
|
|
|
| const pos: Vector2 = [nodePos[0], nodePos[1] - this.posY];
|
| const rightX = node.size[0] - 15;
|
| if (pos[0] > rightX || pos[0] < rightX - 16) {
|
| return false;
|
| }
|
| this.widgetOptions.actionCallback(event);
|
| return true;
|
| }
|
| }
|
|
|
|
|
| export class RgthreeInvisibleWidget<T> implements IWidget<T> {
|
| name: string;
|
| type: string;
|
| value: T;
|
| serializeValue: IWidget['serializeValue'] = undefined;
|
|
|
| constructor(name: string, type: string, value: T, serializeValueFn: ()=> T) {
|
| this.name = name;
|
| this.type = type;
|
| this.value = value;
|
| if (serializeValueFn) {
|
| this.serializeValue = serializeValueFn
|
| }
|
| }
|
| draw() { return; }
|
| computeSize(width: number) : Vector2 { return [0, 0]; }
|
| }
|
|
|
|
|
| type DrawContext = {
|
| ctx: CanvasRenderingContext2D,
|
| node: LGraphNode,
|
| width: number,
|
| y: number,
|
| height: number,
|
| }
|
|
|
| |
| |
|
|
| export function drawWidgetButton(drawCtx: DrawContext, text: string, isMouseDownedAndOver: boolean = false) {
|
|
|
| if (!isLowQuality() && !isMouseDownedAndOver) {
|
| drawRoundedRectangle(drawCtx.ctx, {
|
| width: drawCtx.width - 30 - 2,
|
| height: drawCtx.height,
|
| posY: drawCtx.y + 1,
|
| posX: 15 + 1,
|
| borderRadius: 4,
|
| colorBackground: "#000000aa",
|
| colorStroke: "#000000aa",
|
| });
|
| }
|
|
|
| drawRoundedRectangle(drawCtx.ctx, {
|
| width: drawCtx.width - 30,
|
| height: drawCtx.height,
|
| posY: drawCtx.y + (isMouseDownedAndOver ? 1 : 0),
|
| posX: 15,
|
| borderRadius: isLowQuality() ? 0 : 4,
|
| colorBackground: isMouseDownedAndOver ? "#444" : LiteGraph.WIDGET_BGCOLOR,
|
| });
|
|
|
| if (!isLowQuality()) {
|
| drawCtx.ctx.textBaseline = "middle";
|
| drawCtx.ctx.textAlign = "center";
|
| drawCtx.ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR;
|
| drawCtx.ctx.fillText(
|
| text,
|
| drawCtx.node.size[0] / 2,
|
| drawCtx.y + drawCtx.height / 2 + (isMouseDownedAndOver ? 1 : 0),
|
| );
|
| }
|
| } |