diff --git a/knip.config.ts b/knip.config.ts index 5d975c8b4..5e2d9832b 100644 --- a/knip.config.ts +++ b/knip.config.ts @@ -23,7 +23,7 @@ const config: KnipConfig = { project: ['src/**/*.{js,ts}'] } }, - ignoreBinaries: ['python3'], + ignoreBinaries: ['python3', 'stylelint'], ignoreDependencies: [ // Weird importmap things '@iconify/json', @@ -32,7 +32,8 @@ const config: KnipConfig = { '@primeuix/utils', '@primevue/icons', // Dev - '@trivago/prettier-plugin-sort-imports' + '@trivago/prettier-plugin-sort-imports', + 'stylelint' ], ignore: [ // Auto generated manager types diff --git a/src/base/pointerUtils.ts b/src/base/pointerUtils.ts new file mode 100644 index 000000000..6bb218780 --- /dev/null +++ b/src/base/pointerUtils.ts @@ -0,0 +1,22 @@ +/** + * Utilities for pointer event handling + */ + +/** + * Checks if a pointer or mouse event is a middle button input + * @param event - The pointer or mouse event to check + * @returns true if the event is from the middle button/wheel + */ +export function isMiddlePointerInput( + event: PointerEvent | MouseEvent +): boolean { + if ('button' in event && event.button === 1) { + return true + } + + if ('buttons' in event && typeof event.buttons === 'number') { + return event.buttons === 4 + } + + return false +} diff --git a/src/renderer/core/canvas/useCanvasInteractions.ts b/src/renderer/core/canvas/useCanvasInteractions.ts index 46ce4ae46..5d9804e13 100644 --- a/src/renderer/core/canvas/useCanvasInteractions.ts +++ b/src/renderer/core/canvas/useCanvasInteractions.ts @@ -1,5 +1,6 @@ import { computed } from 'vue' +import { isMiddlePointerInput } from '@/base/pointerUtils' import { useSettingStore } from '@/platform/settings/settingStore' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import { app } from '@/scripts/app' @@ -59,6 +60,11 @@ export function useCanvasInteractions() { * be forwarded to canvas (e.g., space+drag for panning) */ const handlePointer = (event: PointerEvent) => { + if (isMiddlePointerInput(event)) { + forwardEventToCanvas(event) + return + } + // Check if canvas exists using established pattern const canvas = getCanvas() if (!canvas) return diff --git a/src/renderer/extensions/vueNodes/composables/useNodePointerInteractions.test.ts b/src/renderer/extensions/vueNodes/composables/useNodePointerInteractions.test.ts index f0c046ca0..7010d253c 100644 --- a/src/renderer/extensions/vueNodes/composables/useNodePointerInteractions.test.ts +++ b/src/renderer/extensions/vueNodes/composables/useNodePointerInteractions.test.ts @@ -4,10 +4,12 @@ import { nextTick, ref } from 'vue' import type { VueNodeData } from '@/composables/graph/useGraphNodeManager' import { useNodePointerInteractions } from '@/renderer/extensions/vueNodes/composables/useNodePointerInteractions' +const forwardEventToCanvasMock = vi.fn() + // Mock the dependencies vi.mock('@/renderer/core/canvas/useCanvasInteractions', () => ({ useCanvasInteractions: () => ({ - forwardEventToCanvas: vi.fn(), + forwardEventToCanvas: forwardEventToCanvasMock, shouldHandleNodePointerEvents: ref(true) }) })) @@ -69,6 +71,7 @@ const createMouseEvent = ( describe('useNodePointerInteractions', () => { beforeEach(() => { vi.clearAllMocks() + forwardEventToCanvasMock.mockClear() }) it('should only start drag on left-click', async () => { @@ -100,6 +103,34 @@ describe('useNodePointerInteractions', () => { ) }) + it('forwards middle mouse interactions to the canvas', () => { + const mockNodeData = createMockVueNodeData() + const mockOnPointerUp = vi.fn() + + const { pointerHandlers } = useNodePointerInteractions( + ref(mockNodeData), + mockOnPointerUp + ) + + const middlePointerDown = createPointerEvent('pointerdown', { button: 1 }) + pointerHandlers.onPointerdown(middlePointerDown) + expect(forwardEventToCanvasMock).toHaveBeenCalledWith(middlePointerDown) + + forwardEventToCanvasMock.mockClear() + + const middlePointerMove = createPointerEvent('pointermove', { buttons: 4 }) + pointerHandlers.onPointermove(middlePointerMove) + expect(forwardEventToCanvasMock).toHaveBeenCalledWith(middlePointerMove) + + forwardEventToCanvasMock.mockClear() + + const middlePointerUp = createPointerEvent('pointerup', { button: 1 }) + pointerHandlers.onPointerup(middlePointerUp) + expect(forwardEventToCanvasMock).toHaveBeenCalledWith(middlePointerUp) + + expect(mockOnPointerUp).not.toHaveBeenCalled() + }) + it('should distinguish drag from click based on distance threshold', async () => { const mockNodeData = createMockVueNodeData() const mockOnPointerUp = vi.fn() diff --git a/src/renderer/extensions/vueNodes/composables/useNodePointerInteractions.ts b/src/renderer/extensions/vueNodes/composables/useNodePointerInteractions.ts index 7602698f6..ccbf845cb 100644 --- a/src/renderer/extensions/vueNodes/composables/useNodePointerInteractions.ts +++ b/src/renderer/extensions/vueNodes/composables/useNodePointerInteractions.ts @@ -1,5 +1,6 @@ import { type MaybeRefOrGetter, computed, onUnmounted, ref, toValue } from 'vue' +import { isMiddlePointerInput } from '@/base/pointerUtils' import type { VueNodeData } from '@/composables/graph/useGraphNodeManager' import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions' import { layoutStore } from '@/renderer/core/layout/store/layoutStore' @@ -34,6 +35,12 @@ export function useNodePointerInteractions( const { forwardEventToCanvas, shouldHandleNodePointerEvents } = useCanvasInteractions() + const forwardMiddlePointerIfNeeded = (event: PointerEvent) => { + if (!isMiddlePointerInput(event)) return false + forwardEventToCanvas(event) + return true + } + // Drag state for styling const isDragging = ref(false) const dragStyle = computed(() => { @@ -52,6 +59,8 @@ export function useNodePointerInteractions( return } + if (forwardMiddlePointerIfNeeded(event)) return + const stopNodeDragTarget = event.target instanceof HTMLElement ? event.target.closest('[data-capture-node="true"]') @@ -90,6 +99,8 @@ export function useNodePointerInteractions( } const handlePointerMove = (event: PointerEvent) => { + if (forwardMiddlePointerIfNeeded(event)) return + if (isDragging.value) { void handleDrag(event) } @@ -129,6 +140,8 @@ export function useNodePointerInteractions( } const handlePointerUp = (event: PointerEvent) => { + if (forwardMiddlePointerIfNeeded(event)) return + if (isDragging.value) { handleDragTermination(event, 'drag end') }