From e1689d678e443cea0f8d7d4462317ebd71a5b990 Mon Sep 17 00:00:00 2001 From: Judicael <46365844+judicaelandria@users.noreply.github.com> Date: Wed, 7 Sep 2022 16:58:32 +0300 Subject: [PATCH] feat: change cursor when panning (#939) * feat: change icon when panning * add support for panning with the middle mouse * Remove state at top tldraw * logic tweaks * accept middle clicks on objects * Update useCursor.ts Co-authored-by: Steve Ruiz --- packages/core/src/hooks/useBoundsEvents.tsx | 19 ++++-- .../core/src/hooks/useBoundsHandleEvents.tsx | 24 ++++--- packages/core/src/hooks/useCanvasEvents.tsx | 6 +- packages/core/src/hooks/useHandleEvents.tsx | 24 ++++--- packages/core/src/hooks/useShapeEvents.tsx | 28 +++++--- packages/tldraw/src/Tldraw.tsx | 39 ++++++++++- packages/tldraw/src/hooks/useCursor.ts | 66 +++++++++++++++++++ 7 files changed, 170 insertions(+), 36 deletions(-) create mode 100644 packages/tldraw/src/hooks/useCursor.ts diff --git a/packages/core/src/hooks/useBoundsEvents.tsx b/packages/core/src/hooks/useBoundsEvents.tsx index f8e7e3bb7..476c4e3cb 100644 --- a/packages/core/src/hooks/useBoundsEvents.tsx +++ b/packages/core/src/hooks/useBoundsEvents.tsx @@ -14,16 +14,17 @@ export function useBoundsEvents() { callbacks.onRightPointBounds?.(inputs.pointerDown(e, 'bounds'), e) return } - if (e.button !== 0) return - e.currentTarget?.setPointerCapture(e.pointerId) const info = inputs.pointerDown(e, 'bounds') - callbacks.onPointBounds?.(info, e) + e.currentTarget?.setPointerCapture(e.pointerId) + if (e.button === 0) { + callbacks.onPointBounds?.(info, e) + } callbacks.onPointerDown?.(info, e) }, onPointerUp: (e: React.PointerEvent) => { if ((e as any).dead) return else (e as any).dead = true - if (e.button !== 0) return + if (e.button === 2) return inputs.activePointer = undefined if (!inputs.pointerIsValid(e)) return const isDoubleClick = inputs.isDoubleClick() @@ -34,15 +35,19 @@ export function useBoundsEvents() { if (isDoubleClick && !(info.altKey || info.metaKey)) { callbacks.onDoubleClickBounds?.(info, e) } - callbacks.onReleaseBounds?.(info, e) + if (e.button === 0) { + callbacks.onReleaseBounds?.(info, e) + } callbacks.onPointerUp?.(info, e) }, onPointerMove: (e: React.PointerEvent) => { if ((e as any).dead) return else (e as any).dead = true if (!inputs.pointerIsValid(e)) return - if (e.currentTarget.hasPointerCapture(e.pointerId)) { - callbacks.onDragBounds?.(inputs.pointerMove(e, 'bounds'), e) + if (e.button === 0) { + if (e.currentTarget.hasPointerCapture(e.pointerId)) { + callbacks.onDragBounds?.(inputs.pointerMove(e, 'bounds'), e) + } } const info = inputs.pointerMove(e, 'bounds') callbacks.onPointerMove?.(info, e) diff --git a/packages/core/src/hooks/useBoundsHandleEvents.tsx b/packages/core/src/hooks/useBoundsHandleEvents.tsx index fd4fe3ab7..19dea2a8a 100644 --- a/packages/core/src/hooks/useBoundsHandleEvents.tsx +++ b/packages/core/src/hooks/useBoundsHandleEvents.tsx @@ -11,13 +11,15 @@ export function useBoundsHandleEvents( (e: React.PointerEvent) => { if ((e as any).dead) return else (e as any).dead = true - if (e.button !== 0) return + if (e.button === 2) return if (!inputs.pointerIsValid(e)) return const info = inputs.pointerDown(e, id) - if (inputs.isDoubleClick() && !(info.altKey || info.metaKey)) { - callbacks.onDoubleClickBoundsHandle?.(info, e) + if (e.button === 0) { + if (inputs.isDoubleClick() && !(info.altKey || info.metaKey)) { + callbacks.onDoubleClickBoundsHandle?.(info, e) + } + callbacks.onPointBoundsHandle?.(info, e) } - callbacks.onPointBoundsHandle?.(info, e) callbacks.onPointerDown?.(info, e) }, [inputs, callbacks, id] @@ -27,10 +29,13 @@ export function useBoundsHandleEvents( (e: React.PointerEvent) => { if ((e as any).dead) return else (e as any).dead = true - if (e.button !== 0) return + if (e.button === 2) return if (!inputs.pointerIsValid(e)) return const info = inputs.pointerUp(e, id) - callbacks.onReleaseBoundsHandle?.(info, e) + + if (e.button === 0) { + callbacks.onReleaseBoundsHandle?.(info, e) + } callbacks.onPointerUp?.(info, e) }, [inputs, callbacks, id] @@ -40,9 +45,12 @@ export function useBoundsHandleEvents( (e: React.PointerEvent) => { if ((e as any).dead) return else (e as any).dead = true + if (e.button !== 0) return if (!inputs.pointerIsValid(e)) return - if (e.currentTarget.hasPointerCapture(e.pointerId)) { - callbacks.onDragBoundsHandle?.(inputs.pointerMove(e, id), e) + if (e.button === 0) { + if (e.currentTarget.hasPointerCapture(e.pointerId)) { + callbacks.onDragBoundsHandle?.(inputs.pointerMove(e, id), e) + } } const info = inputs.pointerMove(e, id) callbacks.onPointerMove?.(info, e) diff --git a/packages/core/src/hooks/useCanvasEvents.tsx b/packages/core/src/hooks/useCanvasEvents.tsx index 708d402ff..0671ff41b 100644 --- a/packages/core/src/hooks/useCanvasEvents.tsx +++ b/packages/core/src/hooks/useCanvasEvents.tsx @@ -24,8 +24,10 @@ export function useCanvasEvents() { else (e as any).dead = true if (!inputs.pointerIsValid(e)) return const info = inputs.pointerMove(e, 'canvas') - if (e.currentTarget.hasPointerCapture(e.pointerId)) { - callbacks.onDragCanvas?.(info, e) + if (e.button === 0) { + if (e.currentTarget.hasPointerCapture(e.pointerId)) { + callbacks.onDragCanvas?.(info, e) + } } callbacks.onPointerMove?.(info, e) }, diff --git a/packages/core/src/hooks/useHandleEvents.tsx b/packages/core/src/hooks/useHandleEvents.tsx index 66fdf33ca..e25e0cec2 100644 --- a/packages/core/src/hooks/useHandleEvents.tsx +++ b/packages/core/src/hooks/useHandleEvents.tsx @@ -10,26 +10,31 @@ export function useHandleEvents(id: string) { if ((e as any).dead) return else (e as any).dead = true if (!inputs.pointerIsValid(e)) return - if (e.button !== 0) return + if (e.button === 2) return if (!inputs.pointerIsValid(e)) return e.currentTarget?.setPointerCapture(e.pointerId) const info = inputs.pointerDown(e, id) - callbacks.onPointHandle?.(info, e) + + if (e.button === 0) { + callbacks.onPointHandle?.(info, e) + } callbacks.onPointerDown?.(info, e) }, onPointerUp: (e: React.PointerEvent) => { if ((e as any).dead) return else (e as any).dead = true - if (e.button !== 0) return + if (e.button === 2) return if (!inputs.pointerIsValid(e)) return const isDoubleClick = inputs.isDoubleClick() const info = inputs.pointerUp(e, id) if (e.currentTarget.hasPointerCapture(e.pointerId)) { e.currentTarget?.releasePointerCapture(e.pointerId) - if (isDoubleClick && !(info.altKey || info.metaKey)) { - callbacks.onDoubleClickHandle?.(info, e) + if (e.button === 0) { + if (isDoubleClick && !(info.altKey || info.metaKey)) { + callbacks.onDoubleClickHandle?.(info, e) + } + callbacks.onReleaseHandle?.(info, e) } - callbacks.onReleaseHandle?.(info, e) } callbacks.onPointerUp?.(info, e) }, @@ -37,9 +42,12 @@ export function useHandleEvents(id: string) { if ((e as any).dead) return else (e as any).dead = true if (!inputs.pointerIsValid(e)) return + if (e.button === 2) return const info = inputs.pointerMove(e, id) - if (e.currentTarget.hasPointerCapture(e.pointerId)) { - callbacks.onDragHandle?.(info, e) + if (e.button === 0) { + if (e.currentTarget.hasPointerCapture(e.pointerId)) { + callbacks.onDragHandle?.(info, e) + } } callbacks.onPointerMove?.(info, e) }, diff --git a/packages/core/src/hooks/useShapeEvents.tsx b/packages/core/src/hooks/useShapeEvents.tsx index 0f2b5b303..35c017609 100644 --- a/packages/core/src/hooks/useShapeEvents.tsx +++ b/packages/core/src/hooks/useShapeEvents.tsx @@ -15,7 +15,6 @@ export function useShapeEvents(id: string) { callbacks.onRightPointShape?.(inputs.pointerDown(e, id), e) return } - if (e.button !== 0) return const info = inputs.pointerDown(e, id) e.currentTarget?.setPointerCapture(e.pointerId) // If we click "through" the selection bounding box to hit a shape that isn't selected, @@ -26,18 +25,22 @@ export function useShapeEvents(id: string) { Utils.pointInBounds(info.point, rSelectionBounds.current) && !rPageState.current.selectedIds.includes(id) ) { - callbacks.onPointBounds?.(inputs.pointerDown(e, 'bounds'), e) - callbacks.onPointShape?.(info, e) + if (e.button === 0) { + callbacks.onPointBounds?.(inputs.pointerDown(e, 'bounds'), e) + callbacks.onPointShape?.(info, e) + } callbacks.onPointerDown?.(info, e) return } - callbacks.onPointShape?.(info, e) + if (e.button === 0) { + callbacks.onPointShape?.(info, e) + } callbacks.onPointerDown?.(info, e) }, onPointerUp: (e: React.PointerEvent) => { if ((e as any).dead) return else (e as any).dead = true - if (e.button !== 0) return + if (e.button === 2) return inputs.activePointer = undefined if (!inputs.pointerIsValid(e)) return const isDoubleClick = inputs.isDoubleClick() @@ -45,20 +48,25 @@ export function useShapeEvents(id: string) { if (e.currentTarget.hasPointerCapture(e.pointerId)) { e.currentTarget?.releasePointerCapture(e.pointerId) } - if (isDoubleClick && !(info.altKey || info.metaKey)) { - callbacks.onDoubleClickShape?.(info, e) + if (e.button === 0) { + if (isDoubleClick && !(info.altKey || info.metaKey)) { + callbacks.onDoubleClickShape?.(info, e) + } + callbacks.onReleaseShape?.(info, e) } - callbacks.onReleaseShape?.(info, e) callbacks.onPointerUp?.(info, e) }, onPointerMove: (e: React.PointerEvent) => { if ((e as any).dead) return else (e as any).dead = true + if (e.button === 2) return if (!inputs.pointerIsValid(e)) return if (inputs.pointer && e.pointerId !== inputs.pointer.pointerId) return const info = inputs.pointerMove(e, id) - if (e.currentTarget.hasPointerCapture(e.pointerId)) { - callbacks.onDragShape?.(info, e) + if (e.button === 0) { + if (e.currentTarget.hasPointerCapture(e.pointerId)) { + callbacks.onDragShape?.(info, e) + } } callbacks.onPointerMove?.(info, e) }, diff --git a/packages/tldraw/src/Tldraw.tsx b/packages/tldraw/src/Tldraw.tsx index 4d99cc862..c78088551 100644 --- a/packages/tldraw/src/Tldraw.tsx +++ b/packages/tldraw/src/Tldraw.tsx @@ -20,6 +20,7 @@ import { useTldrawApp, useTranslation, } from '~hooks' +import { useCursor } from '~hooks/useCursor' import { TDCallbacks, TldrawApp } from '~state' import { TLDR } from '~state/TLDR' import { shapeUtils } from '~state/shapes' @@ -368,7 +369,6 @@ const InnerTldraw = React.memo(function InnerTldraw({ }: InnerTldrawProps) { const app = useTldrawApp() const [dialogContainer, setDialogContainer] = React.useState(null) - const rWrapper = React.useRef(null) const state = app.useStore() @@ -464,6 +464,8 @@ const InnerTldraw = React.memo(function InnerTldraw({ } }, [settings.isDarkMode]) + useCursor(rWrapper) + return ( @@ -596,6 +598,41 @@ const OneOff = React.memo(function OneOff({ return null }) +const Wrapper = styled('div', { + variants: { + isForcingPanning: { + true: {}, + false: {}, + }, + isPointerDown: { + true: {}, + false: {}, + }, + }, + compoundVariants: [ + { + isForcingPanning: true, + isPointerDown: false, + css: { + cursor: 'grab', + }, + }, + { + isForcingPanning: false, + css: { + cursor: 'default', + }, + }, + { + isPointerDown: true, + isForcingPanning: true, + css: { + cursor: 'grabbing', + }, + }, + ], +}) + const StyledLayout = styled('div', { position: 'absolute', height: '100%', diff --git a/packages/tldraw/src/hooks/useCursor.ts b/packages/tldraw/src/hooks/useCursor.ts new file mode 100644 index 000000000..e8bcd7f12 --- /dev/null +++ b/packages/tldraw/src/hooks/useCursor.ts @@ -0,0 +1,66 @@ +import React, { RefObject } from 'react' + +export function useCursor(ref: RefObject) { + React.useEffect(() => { + let isPointing = false + let isSpacePanning = false + + const elm = ref.current + if (!elm) return + + const onKeyDown = (e: KeyboardEvent) => { + if (e.key == ' ') { + isSpacePanning = true + + if (isPointing) { + elm.setAttribute('style', 'cursor: grabbing !important') + } else { + elm.setAttribute('style', 'cursor: grab !important') + } + } + } + + const onKeyUp = (e: KeyboardEvent) => { + if (e.key == ' ') { + isSpacePanning = false + elm.setAttribute('style', 'cursor: initial') + } + } + + const onPointerDown = (e: PointerEvent) => { + isPointing = true + + if (e.button === 1) { + elm.setAttribute('style', 'cursor: grabbing !important') + } + + if (e.button === 0) { + if (isSpacePanning) { + elm.setAttribute('style', 'cursor: grabbing !important') + } + } + } + + const onPointerUp = () => { + isPointing = false + + if (isSpacePanning) { + elm.setAttribute('style', 'cursor: grab !important') + } else { + elm.setAttribute('style', 'cursor: initial') + } + } + + elm.addEventListener('keydown', onKeyDown) + elm.addEventListener('keyup', onKeyUp) + elm.addEventListener('pointerdown', onPointerDown) + elm.addEventListener('pointerup', onPointerUp) + + return () => { + elm.removeEventListener('keydown', onKeyDown) + elm.removeEventListener('keyup', onKeyUp) + elm.removeEventListener('pointerdown', onPointerDown) + elm.removeEventListener('pointerup', onPointerUp) + } + }, [ref.current]) +}