-
Notifications
You must be signed in to change notification settings - Fork 377
make Vue nodes resizable #5936
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
make Vue nodes resizable #5936
Changes from all commits
2de6af4
438a831
560f61e
9c3cc35
f332a8b
8b0d56b
a1f272b
3e7b2b6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
import { useEventListener } from '@vueuse/core' | ||
import { ref } from 'vue' | ||
|
||
import type { TransformState } from '@/renderer/core/layout/injectionKeys' | ||
|
||
interface Size { | ||
width: number | ||
height: number | ||
} | ||
|
||
interface Position { | ||
x: number | ||
y: number | ||
} | ||
|
||
interface UseNodeResizeOptions { | ||
/** Transform state for coordinate conversion */ | ||
transformState: TransformState | ||
} | ||
|
||
/** | ||
* Composable for node resizing functionality | ||
* | ||
* Provides resize handle interaction that integrates with the layout system. | ||
* Handles pointer capture, coordinate calculations, and size constraints. | ||
*/ | ||
export function useNodeResize( | ||
resizeCallback: (size: Size, element: HTMLElement) => void, | ||
options: UseNodeResizeOptions | ||
) { | ||
const { transformState } = options | ||
|
||
const isResizing = ref(false) | ||
const resizeStartPos = ref<Position | null>(null) | ||
const resizeStartSize = ref<Size | null>(null) | ||
const intrinsicMinSize = ref<Size | null>(null) | ||
|
||
const startResize = (event: PointerEvent) => { | ||
event.preventDefault() | ||
event.stopPropagation() | ||
|
||
const target = event.currentTarget | ||
if (!(target instanceof HTMLElement)) return | ||
|
||
// Capture pointer to ensure we get all move/up events | ||
target.setPointerCapture(event.pointerId) | ||
|
||
isResizing.value = true | ||
resizeStartPos.value = { x: event.clientX, y: event.clientY } | ||
|
||
// Get current node size from the DOM and calculate intrinsic min size | ||
const nodeElement = target.closest('[data-node-id]') | ||
if (!(nodeElement instanceof HTMLElement)) return | ||
|
||
const rect = nodeElement.getBoundingClientRect() | ||
|
||
// Calculate intrinsic content size once at start | ||
const originalWidth = nodeElement.style.width | ||
const originalHeight = nodeElement.style.height | ||
nodeElement.style.width = 'auto' | ||
nodeElement.style.height = 'auto' | ||
|
||
const intrinsicRect = nodeElement.getBoundingClientRect() | ||
|
||
// Restore original size | ||
nodeElement.style.width = originalWidth | ||
nodeElement.style.height = originalHeight | ||
|
||
// Convert to canvas coordinates using transform state | ||
const scale = transformState.camera.z | ||
resizeStartSize.value = { | ||
width: rect.width / scale, | ||
height: rect.height / scale | ||
} | ||
intrinsicMinSize.value = { | ||
width: intrinsicRect.width / scale, | ||
height: intrinsicRect.height / scale | ||
} | ||
|
||
const handlePointerMove = (moveEvent: PointerEvent) => { | ||
if ( | ||
!isResizing.value || | ||
!resizeStartPos.value || | ||
!resizeStartSize.value || | ||
!intrinsicMinSize.value | ||
) | ||
return | ||
|
||
const dx = moveEvent.clientX - resizeStartPos.value.x | ||
const dy = moveEvent.clientY - resizeStartPos.value.y | ||
|
||
// Apply scale factor from transform state | ||
const scale = transformState.camera.z | ||
const scaledDx = dx / scale | ||
const scaledDy = dy / scale | ||
|
||
// Apply constraints: only minimum size based on content, no maximum | ||
const newWidth = Math.max( | ||
intrinsicMinSize.value.width, | ||
resizeStartSize.value.width + scaledDx | ||
) | ||
const newHeight = Math.max( | ||
intrinsicMinSize.value.height, | ||
resizeStartSize.value.height + scaledDy | ||
) | ||
|
||
// Get the node element to apply size directly | ||
const nodeElement = target.closest('[data-node-id]') | ||
if (nodeElement instanceof HTMLElement) { | ||
resizeCallback({ width: newWidth, height: newHeight }, nodeElement) | ||
} | ||
} | ||
|
||
const handlePointerUp = (upEvent: PointerEvent) => { | ||
if (isResizing.value) { | ||
isResizing.value = false | ||
resizeStartPos.value = null | ||
resizeStartSize.value = null | ||
intrinsicMinSize.value = null | ||
|
||
target.releasePointerCapture(upEvent.pointerId) | ||
stopMoveListen() | ||
stopUpListen() | ||
} | ||
} | ||
|
||
const stopMoveListen = useEventListener('pointermove', handlePointerMove) | ||
const stopUpListen = useEventListener('pointerup', handlePointerUp) | ||
Comment on lines
+127
to
+128
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think adding the listeners within function calls might be part of how we're leaking so many. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
What does this mean exactly?
So have the listeners active across the node lifecycle instead of just during resize? Isn't that worse? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Analyzed this approach but the current implementation is actually optimal - listeners only exist during resize operations (seconds), avoiding unnecessary event processing. useEventListener handles cleanup properly, so no leaks occur. |
||
} | ||
|
||
return { | ||
startResize, | ||
isResizing | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might be worth keeping as a
console.error
to start, then lettinguseNodeResize
degrade gracefully if it's not provided.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why? This should always exist.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a conflict between your console.error suggestion and @christian-byrne's view that TransformState should always exist. Since this is an internal component with guaranteed injection, I kept the current error throwing approach for fail-fast debugging.