mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-10 10:00:08 +00:00
Manage style of DOM widgets in Vue (#2946)
This commit is contained in:
@@ -1,24 +1,15 @@
|
||||
import { LGraphCanvas, LGraphNode } from '@comfyorg/litegraph'
|
||||
import type { Vector4 } from '@comfyorg/litegraph'
|
||||
import type { ISerialisedNode } from '@comfyorg/litegraph/dist/types/serialisation'
|
||||
import { LGraphNode } from '@comfyorg/litegraph'
|
||||
import type {
|
||||
ICustomWidget,
|
||||
IWidgetOptions
|
||||
} from '@comfyorg/litegraph/dist/types/widgets'
|
||||
import _ from 'lodash'
|
||||
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { generateRandomSuffix } from '@/utils/formatUtil'
|
||||
|
||||
interface Rect {
|
||||
height: number
|
||||
width: number
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export interface DOMWidget<T extends HTMLElement, V extends object | string>
|
||||
extends ICustomWidget<T> {
|
||||
// ICustomWidget properties
|
||||
@@ -36,6 +27,10 @@ export interface DOMWidget<T extends HTMLElement, V extends object | string>
|
||||
// DOMWidget properties
|
||||
/** The unique ID of the widget. */
|
||||
id: string
|
||||
/** The node that the widget belongs to. */
|
||||
node: LGraphNode
|
||||
/** Whether the widget is visible. */
|
||||
isVisible(): boolean
|
||||
}
|
||||
|
||||
export interface DOMWidgetOptions<
|
||||
@@ -62,92 +57,6 @@ export interface DOMWidgetOptions<
|
||||
afterResize?: (this: DOMWidget<T, V>, node: LGraphNode) => void
|
||||
}
|
||||
|
||||
function intersect(a: Rect, b: Rect): Vector4 | null {
|
||||
const x = Math.max(a.x, b.x)
|
||||
const num1 = Math.min(a.x + a.width, b.x + b.width)
|
||||
const y = Math.max(a.y, b.y)
|
||||
const num2 = Math.min(a.y + a.height, b.y + b.height)
|
||||
if (num1 >= x && num2 >= y) return [x, y, num1 - x, num2 - y]
|
||||
else return null
|
||||
}
|
||||
|
||||
function getClipPath(
|
||||
node: LGraphNode,
|
||||
element: HTMLElement,
|
||||
canvasRect: DOMRect
|
||||
): string {
|
||||
const selectedNode: LGraphNode = Object.values(
|
||||
app.canvas.selected_nodes ?? {}
|
||||
)[0] as LGraphNode
|
||||
if (selectedNode && selectedNode !== node) {
|
||||
const elRect = element.getBoundingClientRect()
|
||||
const MARGIN = 4
|
||||
const { offset, scale } = app.canvas.ds
|
||||
const { renderArea } = selectedNode
|
||||
|
||||
// Get intersection in browser space
|
||||
const intersection = intersect(
|
||||
{
|
||||
x: elRect.left - canvasRect.left,
|
||||
y: elRect.top - canvasRect.top,
|
||||
width: elRect.width,
|
||||
height: elRect.height
|
||||
},
|
||||
{
|
||||
x: (renderArea[0] + offset[0] - MARGIN) * scale,
|
||||
y: (renderArea[1] + offset[1] - MARGIN) * scale,
|
||||
width: (renderArea[2] + 2 * MARGIN) * scale,
|
||||
height: (renderArea[3] + 2 * MARGIN) * scale
|
||||
}
|
||||
)
|
||||
|
||||
if (!intersection) {
|
||||
return ''
|
||||
}
|
||||
|
||||
// Convert intersection to canvas scale (element has scale transform)
|
||||
const clipX =
|
||||
(intersection[0] - elRect.left + canvasRect.left) / scale + 'px'
|
||||
const clipY = (intersection[1] - elRect.top + canvasRect.top) / scale + 'px'
|
||||
const clipWidth = intersection[2] / scale + 'px'
|
||||
const clipHeight = intersection[3] / scale + 'px'
|
||||
const path = `polygon(0% 0%, 0% 100%, ${clipX} 100%, ${clipX} ${clipY}, calc(${clipX} + ${clipWidth}) ${clipY}, calc(${clipX} + ${clipWidth}) calc(${clipY} + ${clipHeight}), ${clipX} calc(${clipY} + ${clipHeight}), ${clipX} 100%, 100% 100%, 100% 0%)`
|
||||
return path
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// Override the compute visible nodes function to allow us to hide/show DOM elements when the node goes offscreen
|
||||
const elementWidgets = new Set<LGraphNode>()
|
||||
const computeVisibleNodes = LGraphCanvas.prototype.computeVisibleNodes
|
||||
LGraphCanvas.prototype.computeVisibleNodes = function (
|
||||
nodes?: LGraphNode[],
|
||||
out?: LGraphNode[]
|
||||
): LGraphNode[] {
|
||||
const visibleNodes = computeVisibleNodes.call(this, nodes, out)
|
||||
|
||||
for (const node of app.graph.nodes) {
|
||||
if (elementWidgets.has(node)) {
|
||||
const hidden = visibleNodes.indexOf(node) === -1
|
||||
for (const w of node.widgets ?? []) {
|
||||
if (w.element) {
|
||||
w.element.dataset.isInVisibleNodes = hidden ? 'false' : 'true'
|
||||
const shouldOtherwiseHide = w.element.dataset.shouldHide === 'true'
|
||||
const wasHidden = w.element.hidden
|
||||
const actualHidden = hidden || shouldOtherwiseHide || node.collapsed
|
||||
w.element.hidden = actualHidden
|
||||
w.element.style.display = actualHidden ? 'none' : ''
|
||||
if (actualHidden && !wasHidden) {
|
||||
w.options.onHide?.(w as DOMWidget<HTMLElement, object>)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return visibleNodes
|
||||
}
|
||||
|
||||
export class DOMWidgetImpl<T extends HTMLElement, V extends object | string>
|
||||
implements DOMWidget<T, V>
|
||||
{
|
||||
@@ -159,10 +68,12 @@ export class DOMWidgetImpl<T extends HTMLElement, V extends object | string>
|
||||
callback?: (value: V) => void
|
||||
|
||||
readonly id: string
|
||||
readonly node: LGraphNode
|
||||
mouseDownHandler?: (event: MouseEvent) => void
|
||||
|
||||
constructor(obj: {
|
||||
id: string
|
||||
node: LGraphNode
|
||||
name: string
|
||||
type: string
|
||||
element: T
|
||||
@@ -175,6 +86,7 @@ export class DOMWidgetImpl<T extends HTMLElement, V extends object | string>
|
||||
this.options = obj.options
|
||||
|
||||
this.id = obj.id
|
||||
this.node = obj.node
|
||||
|
||||
if (this.element.blur) {
|
||||
this.mouseDownHandler = (event) => {
|
||||
@@ -238,59 +150,16 @@ export class DOMWidgetImpl<T extends HTMLElement, V extends object | string>
|
||||
}
|
||||
}
|
||||
|
||||
draw(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
node: LGraphNode,
|
||||
widgetWidth: number,
|
||||
y: number
|
||||
): void {
|
||||
const { offset, scale } = app.canvas.ds
|
||||
const hidden =
|
||||
(!!this.options.hideOnZoom && app.canvas.low_quality) ||
|
||||
(this.computedHeight ?? 0) <= 0 ||
|
||||
// @ts-expect-error custom widget type
|
||||
this.type === 'converted-widget' ||
|
||||
// @ts-expect-error custom widget type
|
||||
this.type === 'hidden' ||
|
||||
node.collapsed
|
||||
|
||||
this.element.dataset.shouldHide = hidden ? 'true' : 'false'
|
||||
const isInVisibleNodes = this.element.dataset.isInVisibleNodes === 'true'
|
||||
const actualHidden = hidden || !isInVisibleNodes
|
||||
const wasHidden = this.element.hidden
|
||||
this.element.hidden = actualHidden
|
||||
this.element.style.display = actualHidden ? 'none' : ''
|
||||
|
||||
if (actualHidden && !wasHidden) {
|
||||
this.options.onHide?.(this)
|
||||
}
|
||||
if (actualHidden) {
|
||||
return
|
||||
}
|
||||
|
||||
const elRect = ctx.canvas.getBoundingClientRect()
|
||||
const margin = 10
|
||||
const top = node.pos[0] + offset[0] + margin
|
||||
const left = node.pos[1] + offset[1] + margin + y
|
||||
|
||||
Object.assign(this.element.style, {
|
||||
transformOrigin: '0 0',
|
||||
transform: `scale(${scale})`,
|
||||
left: `${top * scale}px`,
|
||||
top: `${left * scale}px`,
|
||||
width: `${widgetWidth - margin * 2}px`,
|
||||
height: `${(this.computedHeight ?? 50) - margin * 2}px`,
|
||||
position: 'absolute',
|
||||
zIndex: app.graph.nodes.indexOf(node),
|
||||
pointerEvents: app.canvas.read_only ? 'none' : 'auto'
|
||||
})
|
||||
|
||||
if (useSettingStore().get('Comfy.DOMClippingEnabled')) {
|
||||
const clipPath = getClipPath(node, this.element, elRect)
|
||||
this.element.style.clipPath = clipPath ?? 'none'
|
||||
this.element.style.willChange = 'clip-path'
|
||||
}
|
||||
isVisible(): boolean {
|
||||
return (
|
||||
!_.isNil(this.computedHeight) &&
|
||||
this.computedHeight > 0 &&
|
||||
!['converted-widget', 'hidden'].includes(this.type) &&
|
||||
!this.node.collapsed
|
||||
)
|
||||
}
|
||||
|
||||
draw(): void {
|
||||
this.options.onDraw?.(this)
|
||||
}
|
||||
|
||||
@@ -315,18 +184,15 @@ LGraphNode.prototype.addDOMWidget = function <
|
||||
): DOMWidget<T, V> {
|
||||
options = { hideOnZoom: true, selectOn: ['focus', 'click'], ...options }
|
||||
|
||||
element.hidden = true
|
||||
element.style.display = 'none'
|
||||
|
||||
const { nodeData } = this.constructor
|
||||
const tooltip = (nodeData?.input.required?.[name] ??
|
||||
nodeData?.input.optional?.[name])?.[1]?.tooltip
|
||||
const tooltip = nodeData?.inputs?.[name]?.tooltip
|
||||
if (tooltip && !element.title) {
|
||||
element.title = tooltip
|
||||
}
|
||||
|
||||
const widget = new DOMWidgetImpl({
|
||||
id: `${this.id}:${name}:${generateRandomSuffix()}`,
|
||||
node: this,
|
||||
name,
|
||||
type,
|
||||
element,
|
||||
@@ -356,14 +222,10 @@ LGraphNode.prototype.addDOMWidget = function <
|
||||
}
|
||||
|
||||
this.addCustomWidget(widget)
|
||||
elementWidgets.add(this)
|
||||
|
||||
const onRemoved = this.onRemoved
|
||||
this.onRemoved = function (this: LGraphNode) {
|
||||
this.onRemoved = useChainCallback(this.onRemoved, () => {
|
||||
widget.onRemove()
|
||||
elementWidgets.delete(this)
|
||||
onRemoved?.call(this)
|
||||
}
|
||||
})
|
||||
|
||||
this.onResize = useChainCallback(this.onResize, () => {
|
||||
options.beforeResize?.call(widget, this)
|
||||
|
||||
Reference in New Issue
Block a user