From bc85d4e87b5bac70bf68ed5f3e7d83f2a8e7250d Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Thu, 18 Sep 2025 15:43:35 -0700 Subject: [PATCH] Make Vue nodes read-only when in panning mode (#5574) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Integrated Vue node components with canvas panning mode to prevent UI interference during navigation. ## Changes - **What**: Added [canCapturePointerEvents](https://docs.comfy.org/guide/vue-nodes) computed property to `useCanvasInteractions` composable that checks canvas read-only state - **What**: Modified Vue node components (LGraphNode, NodeWidgets) to conditionally handle pointer events based on canvas navigation mode - **What**: Updated node event handlers to respect panning mode and forward events to canvas when appropriate ## Review Focus Event forwarding logic in panning mode and pointer event capture state management across Vue node hierarchy. ```mermaid graph TD A[User Interaction] --> B{Canvas in Panning Mode?} B -->|Yes| C[Forward to Canvas] B -->|No| D[Handle in Vue Component] C --> E[Canvas Navigation] D --> F[Node Selection/Widget Interaction] G[canCapturePointerEvents] --> H{read_only === false} H -->|Yes| I[Allow Vue Events] H -->|No| J[Block Vue Events] style A fill:#f9f9f9,stroke:#333,color:#000 style E fill:#f9f9f9,stroke:#333,color:#000 style F fill:#f9f9f9,stroke:#333,color:#000 style I fill:#e1f5fe,stroke:#01579b,color:#000 style J fill:#ffebee,stroke:#c62828,color:#000 ``` ## Screenshots https://github.com/user-attachments/assets/00dc5e4a-2b56-43be-b92e-eaf511e52542 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-5574-Make-Vue-nodes-read-only-when-in-panning-mode-26f6d73d3650818c951cd82c8fe58972) by [Unito](https://www.unito.io) --------- Co-authored-by: GitHub Action --- src/components/graph/GraphCanvas.vue | 2 +- src/components/graph/GraphCanvasMenu.vue | 2 +- src/components/graph/SelectionToolbox.spec.ts | 4 +- src/components/graph/SelectionToolbox.vue | 2 +- src/composables/node/useNodeImage.ts | 2 +- .../core/canvas}/useCanvasInteractions.ts | 14 ++++++- .../vueNodes/components/LGraphNode.vue | 40 ++++++++++++++++++- .../vueNodes/components/NodeWidgets.vue | 25 +++++++++++- .../composables/useNodeEventHandlers.ts | 18 +++++++++ .../canvas}/useCanvasInteractions.test.ts | 2 +- .../composables/useNodeEventHandlers.test.ts | 25 +++++++++++- 11 files changed, 123 insertions(+), 13 deletions(-) rename src/{composables/graph => renderer/core/canvas}/useCanvasInteractions.ts (87%) rename tests-ui/tests/{composables/graph => renderer/core/canvas}/useCanvasInteractions.test.ts (98%) diff --git a/src/components/graph/GraphCanvas.vue b/src/components/graph/GraphCanvas.vue index 49f325c0c..91c91d704 100644 --- a/src/components/graph/GraphCanvas.vue +++ b/src/components/graph/GraphCanvas.vue @@ -96,7 +96,6 @@ import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vu import SideToolbar from '@/components/sidebar/SideToolbar.vue' import SecondRowWorkflowTabs from '@/components/topbar/SecondRowWorkflowTabs.vue' import { useChainCallback } from '@/composables/functional/useChainCallback' -import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions' import { useViewportCulling } from '@/composables/graph/useViewportCulling' import { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle' import { useNodeBadge } from '@/composables/node/useNodeBadge' @@ -118,6 +117,7 @@ import { useWorkflowAutoSave } from '@/platform/workflow/persistence/composables import { useWorkflowPersistence } from '@/platform/workflow/persistence/composables/useWorkflowPersistence' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import { SelectedNodeIdsKey } from '@/renderer/core/canvas/injectionKeys' +import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions' import TransformPane from '@/renderer/core/layout/transform/TransformPane.vue' import MiniMap from '@/renderer/extensions/minimap/MiniMap.vue' import VueGraphNode from '@/renderer/extensions/vueNodes/components/LGraphNode.vue' diff --git a/src/components/graph/GraphCanvasMenu.vue b/src/components/graph/GraphCanvasMenu.vue index d4ec414d6..c4050d30c 100644 --- a/src/components/graph/GraphCanvasMenu.vue +++ b/src/components/graph/GraphCanvasMenu.vue @@ -124,11 +124,11 @@ import ButtonGroup from 'primevue/buttongroup' import { computed, onBeforeUnmount, onMounted } from 'vue' import { useI18n } from 'vue-i18n' -import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions' import { useZoomControls } from '@/composables/useZoomControls' import { LiteGraph } from '@/lib/litegraph/src/litegraph' import { useSettingStore } from '@/platform/settings/settingStore' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' +import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions' import { useMinimap } from '@/renderer/extensions/minimap/composables/useMinimap' import { useCommandStore } from '@/stores/commandStore' import { useWorkspaceStore } from '@/stores/workspaceStore' diff --git a/src/components/graph/SelectionToolbox.spec.ts b/src/components/graph/SelectionToolbox.spec.ts index 7b78190e5..85acaf6cf 100644 --- a/src/components/graph/SelectionToolbox.spec.ts +++ b/src/components/graph/SelectionToolbox.spec.ts @@ -5,12 +5,12 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { createI18n } from 'vue-i18n' import SelectionToolbox from '@/components/graph/SelectionToolbox.vue' -import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' +import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions' import { useExtensionService } from '@/services/extensionService' // Mock the composables and services -vi.mock('@/composables/graph/useCanvasInteractions', () => ({ +vi.mock('@/renderer/core/canvas/useCanvasInteractions', () => ({ useCanvasInteractions: vi.fn(() => ({ handleWheel: vi.fn() })) diff --git a/src/components/graph/SelectionToolbox.vue b/src/components/graph/SelectionToolbox.vue index d87083d3d..067b04346 100644 --- a/src/components/graph/SelectionToolbox.vue +++ b/src/components/graph/SelectionToolbox.vue @@ -60,9 +60,9 @@ import MaskEditorButton from '@/components/graph/selectionToolbox/MaskEditorButt import RefreshSelectionButton from '@/components/graph/selectionToolbox/RefreshSelectionButton.vue' import PublishSubgraphButton from '@/components/graph/selectionToolbox/SaveToSubgraphLibrary.vue' import { useSelectionToolboxPosition } from '@/composables/canvas/useSelectionToolboxPosition' -import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions' import { useSelectionState } from '@/composables/graph/useSelectionState' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' +import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions' import { useMinimap } from '@/renderer/extensions/minimap/composables/useMinimap' import { useExtensionService } from '@/services/extensionService' import { type ComfyCommandImpl, useCommandStore } from '@/stores/commandStore' diff --git a/src/composables/node/useNodeImage.ts b/src/composables/node/useNodeImage.ts index 83600aec2..ad0a04178 100644 --- a/src/composables/node/useNodeImage.ts +++ b/src/composables/node/useNodeImage.ts @@ -1,5 +1,5 @@ -import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions' import type { LGraphNode } from '@/lib/litegraph/src/litegraph' +import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions' import { useNodeOutputStore } from '@/stores/imagePreviewStore' import { fitDimensionsToNodeWidth } from '@/utils/imageUtil' diff --git a/src/composables/graph/useCanvasInteractions.ts b/src/renderer/core/canvas/useCanvasInteractions.ts similarity index 87% rename from src/composables/graph/useCanvasInteractions.ts rename to src/renderer/core/canvas/useCanvasInteractions.ts index be807b771..cddc50d08 100644 --- a/src/composables/graph/useCanvasInteractions.ts +++ b/src/renderer/core/canvas/useCanvasInteractions.ts @@ -11,12 +11,21 @@ import { app } from '@/scripts/app' */ export function useCanvasInteractions() { const settingStore = useSettingStore() - const { getCanvas } = useCanvasStore() + const canvasStore = useCanvasStore() + const { getCanvas } = canvasStore const isStandardNavMode = computed( () => settingStore.get('Comfy.Canvas.NavigationMode') === 'standard' ) + /** + * Whether Vue node components should handle pointer events. + * Returns false when canvas is in read-only/panning mode (e.g., space key held for panning). + */ + const shouldHandleNodePointerEvents = computed( + () => !(canvasStore.canvas?.read_only ?? false) + ) + /** * Handles wheel events from UI components that should be forwarded to canvas * when appropriate (e.g., Ctrl+wheel for zoom in standard mode) @@ -97,6 +106,7 @@ export function useCanvasInteractions() { return { handleWheel, handlePointer, - forwardEventToCanvas + forwardEventToCanvas, + shouldHandleNodePointerEvents } } diff --git a/src/renderer/extensions/vueNodes/components/LGraphNode.vue b/src/renderer/extensions/vueNodes/components/LGraphNode.vue index 808c21ca1..5d67d0de7 100644 --- a/src/renderer/extensions/vueNodes/components/LGraphNode.vue +++ b/src/renderer/extensions/vueNodes/components/LGraphNode.vue @@ -10,7 +10,9 @@ 'bg-white dark-theme:bg-charcoal-800', 'lg-node absolute rounded-2xl', 'border border-solid border-sand-100 dark-theme:border-charcoal-600', - 'hover:ring-7 ring-gray-500/50 dark-theme:ring-gray-500/20', + // hover (only when node should handle events) + shouldHandleNodePointerEvents && + 'hover:ring-7 ring-gray-500/50 dark-theme:ring-gray-500/20', 'outline-transparent -outline-offset-2 outline-2', borderClass, outlineClass, @@ -21,7 +23,9 @@ 'will-change-transform': isDragging }, lodCssClass, - 'pointer-events-auto' + shouldHandleNodePointerEvents + ? 'pointer-events-auto' + : 'pointer-events-none' ) " :style="[ @@ -34,6 +38,7 @@ @pointerdown="handlePointerDown" @pointermove="handlePointerMove" @pointerup="handlePointerUp" + @wheel="handleWheel" >