mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-29 18:52:19 +00:00
Implement a legacy canvas widget for vue mode (#6011)
 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6011-Implement-a-legacy-canvas-widget-for-vue-mode-2896d73d36508127a5d1debcccb519a0) by [Unito](https://www.unito.io) --------- Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
@@ -2801,7 +2801,7 @@ export class LGraphCanvas
|
|||||||
// Widget
|
// Widget
|
||||||
const widget = node.getWidgetOnPos(x, y)
|
const widget = node.getWidgetOnPos(x, y)
|
||||||
if (widget) {
|
if (widget) {
|
||||||
this.#processWidgetClick(e, node, widget)
|
this.processWidgetClick(e, node, widget)
|
||||||
this.node_widget = [node, widget]
|
this.node_widget = [node, widget]
|
||||||
} else {
|
} else {
|
||||||
// Node background
|
// Node background
|
||||||
@@ -2981,13 +2981,12 @@ export class LGraphCanvas
|
|||||||
this.dirty_canvas = true
|
this.dirty_canvas = true
|
||||||
}
|
}
|
||||||
|
|
||||||
#processWidgetClick(
|
processWidgetClick(
|
||||||
e: CanvasPointerEvent,
|
e: CanvasPointerEvent,
|
||||||
node: LGraphNode,
|
node: LGraphNode,
|
||||||
widget: IBaseWidget
|
widget: IBaseWidget,
|
||||||
|
pointer = this.pointer
|
||||||
) {
|
) {
|
||||||
const { pointer } = this
|
|
||||||
|
|
||||||
// Custom widget - CanvasPointer
|
// Custom widget - CanvasPointer
|
||||||
if (typeof widget.onPointerDown === 'function') {
|
if (typeof widget.onPointerDown === 'function') {
|
||||||
const handled = widget.onPointerDown(pointer, node, this)
|
const handled = widget.onPointerDown(pointer, node, this)
|
||||||
|
|||||||
@@ -363,6 +363,14 @@ export interface IBaseWidget<
|
|||||||
lowQuality?: boolean
|
lowQuality?: boolean
|
||||||
): void
|
): void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compatibility method for widgets implementing the draw
|
||||||
|
* method when displayed in non-canvas renderers.
|
||||||
|
* Set by the current renderer implementation.
|
||||||
|
* When called, performs a draw operation.
|
||||||
|
*/
|
||||||
|
triggerDraw?: () => void
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compute the size of the widget. Overrides {@link IBaseWidget.computeSize}.
|
* Compute the size of the widget. Overrides {@link IBaseWidget.computeSize}.
|
||||||
* @param width The width of the widget.
|
* @param width The width of the widget.
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
<!-- Widget Input Slot Dot -->
|
<!-- Widget Input Slot Dot -->
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="opacity-0 transition-opacity duration-150 group-hover:opacity-100"
|
class="z-10 opacity-0 transition-opacity duration-150 group-hover:opacity-100"
|
||||||
>
|
>
|
||||||
<InputSlot
|
<InputSlot
|
||||||
:slot-data="{
|
:slot-data="{
|
||||||
@@ -63,7 +63,8 @@ import { useErrorHandling } from '@/composables/useErrorHandling'
|
|||||||
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
||||||
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
|
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
|
||||||
import WidgetDOM from '@/renderer/extensions/vueNodes/widgets/components/WidgetDOM.vue'
|
import WidgetDOM from '@/renderer/extensions/vueNodes/widgets/components/WidgetDOM.vue'
|
||||||
import WidgetInputText from '@/renderer/extensions/vueNodes/widgets/components/WidgetInputText.vue'
|
// Import widget components directly
|
||||||
|
import WidgetLegacy from '@/renderer/extensions/vueNodes/widgets/components/WidgetLegacy.vue'
|
||||||
import {
|
import {
|
||||||
getComponent,
|
getComponent,
|
||||||
shouldRenderAsVue
|
shouldRenderAsVue
|
||||||
@@ -129,7 +130,7 @@ const processedWidgets = computed((): ProcessedWidget[] => {
|
|||||||
|
|
||||||
const vueComponent =
|
const vueComponent =
|
||||||
getComponent(widget.type, widget.name) ||
|
getComponent(widget.type, widget.name) ||
|
||||||
(widget.isDOMWidget ? WidgetDOM : WidgetInputText)
|
(widget.isDOMWidget ? WidgetDOM : WidgetLegacy)
|
||||||
|
|
||||||
const slotMetadata = widget.slotMetadata
|
const slotMetadata = widget.slotMetadata
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useResizeObserver } from '@vueuse/core'
|
||||||
|
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||||
|
import { CanvasPointer } from '@/lib/litegraph/src/CanvasPointer'
|
||||||
|
import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
|
||||||
|
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||||
|
import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events'
|
||||||
|
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||||
|
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||||
|
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||||
|
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
widget: SimplifiedWidget<void>
|
||||||
|
readonly?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const canvasEl = ref()
|
||||||
|
|
||||||
|
const canvas: LGraphCanvas = useCanvasStore().canvas as LGraphCanvas
|
||||||
|
let node: LGraphNode | undefined
|
||||||
|
let widgetInstance: IBaseWidget | undefined
|
||||||
|
let pointer: CanvasPointer | undefined
|
||||||
|
const scaleFactor = 2
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
node =
|
||||||
|
canvas?.graph?.getNodeById(
|
||||||
|
canvasEl.value.parentElement.attributes['node-id'].value
|
||||||
|
) ?? undefined
|
||||||
|
if (!node) return
|
||||||
|
widgetInstance = node.widgets?.find((w) => w.name === props.widget.name)
|
||||||
|
if (!widgetInstance) return
|
||||||
|
canvasEl.value.width *= scaleFactor
|
||||||
|
if (!widgetInstance.triggerDraw)
|
||||||
|
widgetInstance.callback = useChainCallback(
|
||||||
|
widgetInstance.callback,
|
||||||
|
function (this: IBaseWidget) {
|
||||||
|
this?.triggerDraw?.()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
widgetInstance.triggerDraw = draw
|
||||||
|
useResizeObserver(canvasEl.value.parentElement, draw)
|
||||||
|
watch(() => useColorPaletteStore().activePaletteId, draw)
|
||||||
|
pointer = new CanvasPointer(canvasEl.value)
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (widgetInstance) widgetInstance.triggerDraw = () => {}
|
||||||
|
})
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
if (!widgetInstance || !node) return
|
||||||
|
const width = canvasEl.value.parentElement.clientWidth
|
||||||
|
const height = widgetInstance.computeSize
|
||||||
|
? widgetInstance.computeSize(width)[1]
|
||||||
|
: 20
|
||||||
|
widgetInstance.y = 0
|
||||||
|
canvasEl.value.height = (height + 2) * scaleFactor
|
||||||
|
canvasEl.value.width = width * scaleFactor
|
||||||
|
const ctx = canvasEl.value?.getContext('2d')
|
||||||
|
if (!ctx) return
|
||||||
|
ctx.scale(scaleFactor, scaleFactor)
|
||||||
|
widgetInstance.draw?.(ctx, node, width, 1, height)
|
||||||
|
}
|
||||||
|
function translateEvent(e: PointerEvent): asserts e is CanvasPointerEvent {
|
||||||
|
if (!node) return
|
||||||
|
canvas.adjustMouseEvent(e)
|
||||||
|
canvas.graph_mouse[0] = e.offsetX + node.pos[0]
|
||||||
|
canvas.graph_mouse[1] = e.offsetY + node.pos[1]
|
||||||
|
}
|
||||||
|
//See LGraphCanvas.processWidgetClick
|
||||||
|
function handleDown(e: PointerEvent) {
|
||||||
|
if (!node || !widgetInstance || !pointer) return
|
||||||
|
translateEvent(e)
|
||||||
|
pointer.down(e)
|
||||||
|
if (widgetInstance.mouse)
|
||||||
|
pointer.onDrag = (e) =>
|
||||||
|
widgetInstance!.mouse?.(e, [e.offsetX, e.offsetY], node!)
|
||||||
|
//NOTE: a mouseUp event is already registed under pointer.finally
|
||||||
|
canvas.processWidgetClick(e, node, widgetInstance, pointer)
|
||||||
|
}
|
||||||
|
function handleUp(e: PointerEvent) {
|
||||||
|
if (!pointer) return
|
||||||
|
translateEvent(e)
|
||||||
|
e.click_time = e.timeStamp - (pointer?.eDown?.timeStamp ?? 0)
|
||||||
|
pointer.up(e)
|
||||||
|
}
|
||||||
|
function handleMove(e: PointerEvent) {
|
||||||
|
if (!pointer) return
|
||||||
|
translateEvent(e)
|
||||||
|
pointer.move(e)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="relative mx-[-12px] min-w-0 basis-0">
|
||||||
|
<canvas
|
||||||
|
ref="canvasEl"
|
||||||
|
class="absolute mt-[-13px] w-full cursor-crosshair"
|
||||||
|
@pointerdown="handleDown"
|
||||||
|
@pointerup="handleUp"
|
||||||
|
@pointermove="handleMove"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -14,6 +14,7 @@ import WidgetGalleria from '../components/WidgetGalleria.vue'
|
|||||||
import WidgetImageCompare from '../components/WidgetImageCompare.vue'
|
import WidgetImageCompare from '../components/WidgetImageCompare.vue'
|
||||||
import WidgetInputNumber from '../components/WidgetInputNumber.vue'
|
import WidgetInputNumber from '../components/WidgetInputNumber.vue'
|
||||||
import WidgetInputText from '../components/WidgetInputText.vue'
|
import WidgetInputText from '../components/WidgetInputText.vue'
|
||||||
|
import WidgetLegacy from '../components/WidgetLegacy.vue'
|
||||||
import WidgetMarkdown from '../components/WidgetMarkdown.vue'
|
import WidgetMarkdown from '../components/WidgetMarkdown.vue'
|
||||||
import WidgetMultiSelect from '../components/WidgetMultiSelect.vue'
|
import WidgetMultiSelect from '../components/WidgetMultiSelect.vue'
|
||||||
import WidgetRecordAudio from '../components/WidgetRecordAudio.vue'
|
import WidgetRecordAudio from '../components/WidgetRecordAudio.vue'
|
||||||
@@ -114,6 +115,7 @@ const coreWidgetDefinitions: Array<[string, WidgetDefinition]> = [
|
|||||||
'markdown',
|
'markdown',
|
||||||
{ component: WidgetMarkdown, aliases: ['MARKDOWN'], essential: false }
|
{ component: WidgetMarkdown, aliases: ['MARKDOWN'], essential: false }
|
||||||
],
|
],
|
||||||
|
['legacy', { component: WidgetLegacy, aliases: [], essential: true }],
|
||||||
[
|
[
|
||||||
'audiorecord',
|
'audiorecord',
|
||||||
{
|
{
|
||||||
@@ -161,19 +163,11 @@ export const getComponent = (type: string, name: string): Component | null => {
|
|||||||
return widgets.get(canonicalType)?.component || null
|
return widgets.get(canonicalType)?.component || null
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSupported = (type: string): boolean => {
|
|
||||||
const canonicalType = getCanonicalType(type)
|
|
||||||
return widgets.has(canonicalType)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isEssential = (type: string): boolean => {
|
export const isEssential = (type: string): boolean => {
|
||||||
const canonicalType = getCanonicalType(type)
|
const canonicalType = getCanonicalType(type)
|
||||||
return widgets.get(canonicalType)?.essential || false
|
return widgets.get(canonicalType)?.essential || false
|
||||||
}
|
}
|
||||||
|
|
||||||
export const shouldRenderAsVue = (widget: Partial<SafeWidgetData>): boolean => {
|
export const shouldRenderAsVue = (widget: Partial<SafeWidgetData>): boolean => {
|
||||||
if (widget.options?.canvasOnly) return false
|
return !widget.options?.canvasOnly && !!widget.type
|
||||||
if (widget.isDOMWidget) return true
|
|
||||||
if (!widget.type) return false
|
|
||||||
return isSupported(widget.type)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,10 +123,6 @@ describe('widgetRegistry', () => {
|
|||||||
expect(shouldRenderAsVue({ type: 'combo' })).toBe(true)
|
expect(shouldRenderAsVue({ type: 'combo' })).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return false for unknown types', () => {
|
|
||||||
expect(shouldRenderAsVue({ type: 'unknown_type' })).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respect options while checking type', () => {
|
it('should respect options while checking type', () => {
|
||||||
const widget = { type: 'text', options: { someOption: 'value' } }
|
const widget = { type: 'text', options: { someOption: 'value' } }
|
||||||
expect(shouldRenderAsVue(widget)).toBe(true)
|
expect(shouldRenderAsVue(widget)).toBe(true)
|
||||||
|
|||||||
Reference in New Issue
Block a user