mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +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
|
||||
const widget = node.getWidgetOnPos(x, y)
|
||||
if (widget) {
|
||||
this.#processWidgetClick(e, node, widget)
|
||||
this.processWidgetClick(e, node, widget)
|
||||
this.node_widget = [node, widget]
|
||||
} else {
|
||||
// Node background
|
||||
@@ -2981,13 +2981,12 @@ export class LGraphCanvas
|
||||
this.dirty_canvas = true
|
||||
}
|
||||
|
||||
#processWidgetClick(
|
||||
processWidgetClick(
|
||||
e: CanvasPointerEvent,
|
||||
node: LGraphNode,
|
||||
widget: IBaseWidget
|
||||
widget: IBaseWidget,
|
||||
pointer = this.pointer
|
||||
) {
|
||||
const { pointer } = this
|
||||
|
||||
// Custom widget - CanvasPointer
|
||||
if (typeof widget.onPointerDown === 'function') {
|
||||
const handled = widget.onPointerDown(pointer, node, this)
|
||||
|
||||
@@ -363,6 +363,14 @@ export interface IBaseWidget<
|
||||
lowQuality?: boolean
|
||||
): 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}.
|
||||
* @param width The width of the widget.
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<!-- Widget Input Slot Dot -->
|
||||
|
||||
<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
|
||||
:slot-data="{
|
||||
@@ -63,7 +63,8 @@ import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
||||
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
|
||||
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 {
|
||||
getComponent,
|
||||
shouldRenderAsVue
|
||||
@@ -129,7 +130,7 @@ const processedWidgets = computed((): ProcessedWidget[] => {
|
||||
|
||||
const vueComponent =
|
||||
getComponent(widget.type, widget.name) ||
|
||||
(widget.isDOMWidget ? WidgetDOM : WidgetInputText)
|
||||
(widget.isDOMWidget ? WidgetDOM : WidgetLegacy)
|
||||
|
||||
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 WidgetInputNumber from '../components/WidgetInputNumber.vue'
|
||||
import WidgetInputText from '../components/WidgetInputText.vue'
|
||||
import WidgetLegacy from '../components/WidgetLegacy.vue'
|
||||
import WidgetMarkdown from '../components/WidgetMarkdown.vue'
|
||||
import WidgetMultiSelect from '../components/WidgetMultiSelect.vue'
|
||||
import WidgetRecordAudio from '../components/WidgetRecordAudio.vue'
|
||||
@@ -114,6 +115,7 @@ const coreWidgetDefinitions: Array<[string, WidgetDefinition]> = [
|
||||
'markdown',
|
||||
{ component: WidgetMarkdown, aliases: ['MARKDOWN'], essential: false }
|
||||
],
|
||||
['legacy', { component: WidgetLegacy, aliases: [], essential: true }],
|
||||
[
|
||||
'audiorecord',
|
||||
{
|
||||
@@ -161,19 +163,11 @@ export const getComponent = (type: string, name: string): 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 => {
|
||||
const canonicalType = getCanonicalType(type)
|
||||
return widgets.get(canonicalType)?.essential || false
|
||||
}
|
||||
|
||||
export const shouldRenderAsVue = (widget: Partial<SafeWidgetData>): boolean => {
|
||||
if (widget.options?.canvasOnly) return false
|
||||
if (widget.isDOMWidget) return true
|
||||
if (!widget.type) return false
|
||||
return isSupported(widget.type)
|
||||
return !widget.options?.canvasOnly && !!widget.type
|
||||
}
|
||||
|
||||
@@ -123,10 +123,6 @@ describe('widgetRegistry', () => {
|
||||
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', () => {
|
||||
const widget = { type: 'text', options: { someOption: 'value' } }
|
||||
expect(shouldRenderAsVue(widget)).toBe(true)
|
||||
|
||||
Reference in New Issue
Block a user