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 <steveruizok@gmail.com>
This commit is contained in:
parent
bb9e7ee47c
commit
e1689d678e
7 changed files with 170 additions and 36 deletions
|
@ -14,16 +14,17 @@ export function useBoundsEvents() {
|
||||||
callbacks.onRightPointBounds?.(inputs.pointerDown(e, 'bounds'), e)
|
callbacks.onRightPointBounds?.(inputs.pointerDown(e, 'bounds'), e)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (e.button !== 0) return
|
|
||||||
e.currentTarget?.setPointerCapture(e.pointerId)
|
|
||||||
const info = inputs.pointerDown(e, 'bounds')
|
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)
|
callbacks.onPointerDown?.(info, e)
|
||||||
},
|
},
|
||||||
onPointerUp: (e: React.PointerEvent) => {
|
onPointerUp: (e: React.PointerEvent) => {
|
||||||
if ((e as any).dead) return
|
if ((e as any).dead) return
|
||||||
else (e as any).dead = true
|
else (e as any).dead = true
|
||||||
if (e.button !== 0) return
|
if (e.button === 2) return
|
||||||
inputs.activePointer = undefined
|
inputs.activePointer = undefined
|
||||||
if (!inputs.pointerIsValid(e)) return
|
if (!inputs.pointerIsValid(e)) return
|
||||||
const isDoubleClick = inputs.isDoubleClick()
|
const isDoubleClick = inputs.isDoubleClick()
|
||||||
|
@ -34,15 +35,19 @@ export function useBoundsEvents() {
|
||||||
if (isDoubleClick && !(info.altKey || info.metaKey)) {
|
if (isDoubleClick && !(info.altKey || info.metaKey)) {
|
||||||
callbacks.onDoubleClickBounds?.(info, e)
|
callbacks.onDoubleClickBounds?.(info, e)
|
||||||
}
|
}
|
||||||
callbacks.onReleaseBounds?.(info, e)
|
if (e.button === 0) {
|
||||||
|
callbacks.onReleaseBounds?.(info, e)
|
||||||
|
}
|
||||||
callbacks.onPointerUp?.(info, e)
|
callbacks.onPointerUp?.(info, e)
|
||||||
},
|
},
|
||||||
onPointerMove: (e: React.PointerEvent) => {
|
onPointerMove: (e: React.PointerEvent) => {
|
||||||
if ((e as any).dead) return
|
if ((e as any).dead) return
|
||||||
else (e as any).dead = true
|
else (e as any).dead = true
|
||||||
if (!inputs.pointerIsValid(e)) return
|
if (!inputs.pointerIsValid(e)) return
|
||||||
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
if (e.button === 0) {
|
||||||
callbacks.onDragBounds?.(inputs.pointerMove(e, 'bounds'), e)
|
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
||||||
|
callbacks.onDragBounds?.(inputs.pointerMove(e, 'bounds'), e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const info = inputs.pointerMove(e, 'bounds')
|
const info = inputs.pointerMove(e, 'bounds')
|
||||||
callbacks.onPointerMove?.(info, e)
|
callbacks.onPointerMove?.(info, e)
|
||||||
|
|
|
@ -11,13 +11,15 @@ export function useBoundsHandleEvents(
|
||||||
(e: React.PointerEvent) => {
|
(e: React.PointerEvent) => {
|
||||||
if ((e as any).dead) return
|
if ((e as any).dead) return
|
||||||
else (e as any).dead = true
|
else (e as any).dead = true
|
||||||
if (e.button !== 0) return
|
if (e.button === 2) return
|
||||||
if (!inputs.pointerIsValid(e)) return
|
if (!inputs.pointerIsValid(e)) return
|
||||||
const info = inputs.pointerDown(e, id)
|
const info = inputs.pointerDown(e, id)
|
||||||
if (inputs.isDoubleClick() && !(info.altKey || info.metaKey)) {
|
if (e.button === 0) {
|
||||||
callbacks.onDoubleClickBoundsHandle?.(info, e)
|
if (inputs.isDoubleClick() && !(info.altKey || info.metaKey)) {
|
||||||
|
callbacks.onDoubleClickBoundsHandle?.(info, e)
|
||||||
|
}
|
||||||
|
callbacks.onPointBoundsHandle?.(info, e)
|
||||||
}
|
}
|
||||||
callbacks.onPointBoundsHandle?.(info, e)
|
|
||||||
callbacks.onPointerDown?.(info, e)
|
callbacks.onPointerDown?.(info, e)
|
||||||
},
|
},
|
||||||
[inputs, callbacks, id]
|
[inputs, callbacks, id]
|
||||||
|
@ -27,10 +29,13 @@ export function useBoundsHandleEvents(
|
||||||
(e: React.PointerEvent) => {
|
(e: React.PointerEvent) => {
|
||||||
if ((e as any).dead) return
|
if ((e as any).dead) return
|
||||||
else (e as any).dead = true
|
else (e as any).dead = true
|
||||||
if (e.button !== 0) return
|
if (e.button === 2) return
|
||||||
if (!inputs.pointerIsValid(e)) return
|
if (!inputs.pointerIsValid(e)) return
|
||||||
const info = inputs.pointerUp(e, id)
|
const info = inputs.pointerUp(e, id)
|
||||||
callbacks.onReleaseBoundsHandle?.(info, e)
|
|
||||||
|
if (e.button === 0) {
|
||||||
|
callbacks.onReleaseBoundsHandle?.(info, e)
|
||||||
|
}
|
||||||
callbacks.onPointerUp?.(info, e)
|
callbacks.onPointerUp?.(info, e)
|
||||||
},
|
},
|
||||||
[inputs, callbacks, id]
|
[inputs, callbacks, id]
|
||||||
|
@ -40,9 +45,12 @@ export function useBoundsHandleEvents(
|
||||||
(e: React.PointerEvent) => {
|
(e: React.PointerEvent) => {
|
||||||
if ((e as any).dead) return
|
if ((e as any).dead) return
|
||||||
else (e as any).dead = true
|
else (e as any).dead = true
|
||||||
|
if (e.button !== 0) return
|
||||||
if (!inputs.pointerIsValid(e)) return
|
if (!inputs.pointerIsValid(e)) return
|
||||||
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
if (e.button === 0) {
|
||||||
callbacks.onDragBoundsHandle?.(inputs.pointerMove(e, id), e)
|
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
||||||
|
callbacks.onDragBoundsHandle?.(inputs.pointerMove(e, id), e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const info = inputs.pointerMove(e, id)
|
const info = inputs.pointerMove(e, id)
|
||||||
callbacks.onPointerMove?.(info, e)
|
callbacks.onPointerMove?.(info, e)
|
||||||
|
|
|
@ -24,8 +24,10 @@ export function useCanvasEvents() {
|
||||||
else (e as any).dead = true
|
else (e as any).dead = true
|
||||||
if (!inputs.pointerIsValid(e)) return
|
if (!inputs.pointerIsValid(e)) return
|
||||||
const info = inputs.pointerMove(e, 'canvas')
|
const info = inputs.pointerMove(e, 'canvas')
|
||||||
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
if (e.button === 0) {
|
||||||
callbacks.onDragCanvas?.(info, e)
|
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
||||||
|
callbacks.onDragCanvas?.(info, e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
callbacks.onPointerMove?.(info, e)
|
callbacks.onPointerMove?.(info, e)
|
||||||
},
|
},
|
||||||
|
|
|
@ -10,26 +10,31 @@ export function useHandleEvents(id: string) {
|
||||||
if ((e as any).dead) return
|
if ((e as any).dead) return
|
||||||
else (e as any).dead = true
|
else (e as any).dead = true
|
||||||
if (!inputs.pointerIsValid(e)) return
|
if (!inputs.pointerIsValid(e)) return
|
||||||
if (e.button !== 0) return
|
if (e.button === 2) return
|
||||||
if (!inputs.pointerIsValid(e)) return
|
if (!inputs.pointerIsValid(e)) return
|
||||||
e.currentTarget?.setPointerCapture(e.pointerId)
|
e.currentTarget?.setPointerCapture(e.pointerId)
|
||||||
const info = inputs.pointerDown(e, id)
|
const info = inputs.pointerDown(e, id)
|
||||||
callbacks.onPointHandle?.(info, e)
|
|
||||||
|
if (e.button === 0) {
|
||||||
|
callbacks.onPointHandle?.(info, e)
|
||||||
|
}
|
||||||
callbacks.onPointerDown?.(info, e)
|
callbacks.onPointerDown?.(info, e)
|
||||||
},
|
},
|
||||||
onPointerUp: (e: React.PointerEvent) => {
|
onPointerUp: (e: React.PointerEvent) => {
|
||||||
if ((e as any).dead) return
|
if ((e as any).dead) return
|
||||||
else (e as any).dead = true
|
else (e as any).dead = true
|
||||||
if (e.button !== 0) return
|
if (e.button === 2) return
|
||||||
if (!inputs.pointerIsValid(e)) return
|
if (!inputs.pointerIsValid(e)) return
|
||||||
const isDoubleClick = inputs.isDoubleClick()
|
const isDoubleClick = inputs.isDoubleClick()
|
||||||
const info = inputs.pointerUp(e, id)
|
const info = inputs.pointerUp(e, id)
|
||||||
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
||||||
e.currentTarget?.releasePointerCapture(e.pointerId)
|
e.currentTarget?.releasePointerCapture(e.pointerId)
|
||||||
if (isDoubleClick && !(info.altKey || info.metaKey)) {
|
if (e.button === 0) {
|
||||||
callbacks.onDoubleClickHandle?.(info, e)
|
if (isDoubleClick && !(info.altKey || info.metaKey)) {
|
||||||
|
callbacks.onDoubleClickHandle?.(info, e)
|
||||||
|
}
|
||||||
|
callbacks.onReleaseHandle?.(info, e)
|
||||||
}
|
}
|
||||||
callbacks.onReleaseHandle?.(info, e)
|
|
||||||
}
|
}
|
||||||
callbacks.onPointerUp?.(info, e)
|
callbacks.onPointerUp?.(info, e)
|
||||||
},
|
},
|
||||||
|
@ -37,9 +42,12 @@ export function useHandleEvents(id: string) {
|
||||||
if ((e as any).dead) return
|
if ((e as any).dead) return
|
||||||
else (e as any).dead = true
|
else (e as any).dead = true
|
||||||
if (!inputs.pointerIsValid(e)) return
|
if (!inputs.pointerIsValid(e)) return
|
||||||
|
if (e.button === 2) return
|
||||||
const info = inputs.pointerMove(e, id)
|
const info = inputs.pointerMove(e, id)
|
||||||
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
if (e.button === 0) {
|
||||||
callbacks.onDragHandle?.(info, e)
|
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
||||||
|
callbacks.onDragHandle?.(info, e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
callbacks.onPointerMove?.(info, e)
|
callbacks.onPointerMove?.(info, e)
|
||||||
},
|
},
|
||||||
|
|
|
@ -15,7 +15,6 @@ export function useShapeEvents(id: string) {
|
||||||
callbacks.onRightPointShape?.(inputs.pointerDown(e, id), e)
|
callbacks.onRightPointShape?.(inputs.pointerDown(e, id), e)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (e.button !== 0) return
|
|
||||||
const info = inputs.pointerDown(e, id)
|
const info = inputs.pointerDown(e, id)
|
||||||
e.currentTarget?.setPointerCapture(e.pointerId)
|
e.currentTarget?.setPointerCapture(e.pointerId)
|
||||||
// If we click "through" the selection bounding box to hit a shape that isn't selected,
|
// 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) &&
|
Utils.pointInBounds(info.point, rSelectionBounds.current) &&
|
||||||
!rPageState.current.selectedIds.includes(id)
|
!rPageState.current.selectedIds.includes(id)
|
||||||
) {
|
) {
|
||||||
callbacks.onPointBounds?.(inputs.pointerDown(e, 'bounds'), e)
|
if (e.button === 0) {
|
||||||
callbacks.onPointShape?.(info, e)
|
callbacks.onPointBounds?.(inputs.pointerDown(e, 'bounds'), e)
|
||||||
|
callbacks.onPointShape?.(info, e)
|
||||||
|
}
|
||||||
callbacks.onPointerDown?.(info, e)
|
callbacks.onPointerDown?.(info, e)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
callbacks.onPointShape?.(info, e)
|
if (e.button === 0) {
|
||||||
|
callbacks.onPointShape?.(info, e)
|
||||||
|
}
|
||||||
callbacks.onPointerDown?.(info, e)
|
callbacks.onPointerDown?.(info, e)
|
||||||
},
|
},
|
||||||
onPointerUp: (e: React.PointerEvent) => {
|
onPointerUp: (e: React.PointerEvent) => {
|
||||||
if ((e as any).dead) return
|
if ((e as any).dead) return
|
||||||
else (e as any).dead = true
|
else (e as any).dead = true
|
||||||
if (e.button !== 0) return
|
if (e.button === 2) return
|
||||||
inputs.activePointer = undefined
|
inputs.activePointer = undefined
|
||||||
if (!inputs.pointerIsValid(e)) return
|
if (!inputs.pointerIsValid(e)) return
|
||||||
const isDoubleClick = inputs.isDoubleClick()
|
const isDoubleClick = inputs.isDoubleClick()
|
||||||
|
@ -45,20 +48,25 @@ export function useShapeEvents(id: string) {
|
||||||
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
||||||
e.currentTarget?.releasePointerCapture(e.pointerId)
|
e.currentTarget?.releasePointerCapture(e.pointerId)
|
||||||
}
|
}
|
||||||
if (isDoubleClick && !(info.altKey || info.metaKey)) {
|
if (e.button === 0) {
|
||||||
callbacks.onDoubleClickShape?.(info, e)
|
if (isDoubleClick && !(info.altKey || info.metaKey)) {
|
||||||
|
callbacks.onDoubleClickShape?.(info, e)
|
||||||
|
}
|
||||||
|
callbacks.onReleaseShape?.(info, e)
|
||||||
}
|
}
|
||||||
callbacks.onReleaseShape?.(info, e)
|
|
||||||
callbacks.onPointerUp?.(info, e)
|
callbacks.onPointerUp?.(info, e)
|
||||||
},
|
},
|
||||||
onPointerMove: (e: React.PointerEvent) => {
|
onPointerMove: (e: React.PointerEvent) => {
|
||||||
if ((e as any).dead) return
|
if ((e as any).dead) return
|
||||||
else (e as any).dead = true
|
else (e as any).dead = true
|
||||||
|
if (e.button === 2) return
|
||||||
if (!inputs.pointerIsValid(e)) return
|
if (!inputs.pointerIsValid(e)) return
|
||||||
if (inputs.pointer && e.pointerId !== inputs.pointer.pointerId) return
|
if (inputs.pointer && e.pointerId !== inputs.pointer.pointerId) return
|
||||||
const info = inputs.pointerMove(e, id)
|
const info = inputs.pointerMove(e, id)
|
||||||
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
if (e.button === 0) {
|
||||||
callbacks.onDragShape?.(info, e)
|
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
||||||
|
callbacks.onDragShape?.(info, e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
callbacks.onPointerMove?.(info, e)
|
callbacks.onPointerMove?.(info, e)
|
||||||
},
|
},
|
||||||
|
|
|
@ -20,6 +20,7 @@ import {
|
||||||
useTldrawApp,
|
useTldrawApp,
|
||||||
useTranslation,
|
useTranslation,
|
||||||
} from '~hooks'
|
} from '~hooks'
|
||||||
|
import { useCursor } from '~hooks/useCursor'
|
||||||
import { TDCallbacks, TldrawApp } from '~state'
|
import { TDCallbacks, TldrawApp } from '~state'
|
||||||
import { TLDR } from '~state/TLDR'
|
import { TLDR } from '~state/TLDR'
|
||||||
import { shapeUtils } from '~state/shapes'
|
import { shapeUtils } from '~state/shapes'
|
||||||
|
@ -368,7 +369,6 @@ const InnerTldraw = React.memo(function InnerTldraw({
|
||||||
}: InnerTldrawProps) {
|
}: InnerTldrawProps) {
|
||||||
const app = useTldrawApp()
|
const app = useTldrawApp()
|
||||||
const [dialogContainer, setDialogContainer] = React.useState<any>(null)
|
const [dialogContainer, setDialogContainer] = React.useState<any>(null)
|
||||||
|
|
||||||
const rWrapper = React.useRef<HTMLDivElement>(null)
|
const rWrapper = React.useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const state = app.useStore()
|
const state = app.useStore()
|
||||||
|
@ -464,6 +464,8 @@ const InnerTldraw = React.memo(function InnerTldraw({
|
||||||
}
|
}
|
||||||
}, [settings.isDarkMode])
|
}, [settings.isDarkMode])
|
||||||
|
|
||||||
|
useCursor(rWrapper)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContainerContext.Provider value={rWrapper}>
|
<ContainerContext.Provider value={rWrapper}>
|
||||||
<IntlProvider locale={translation.locale} messages={translation.messages}>
|
<IntlProvider locale={translation.locale} messages={translation.messages}>
|
||||||
|
@ -596,6 +598,41 @@ const OneOff = React.memo(function OneOff({
|
||||||
return null
|
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', {
|
const StyledLayout = styled('div', {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
|
|
66
packages/tldraw/src/hooks/useCursor.ts
Normal file
66
packages/tldraw/src/hooks/useCursor.ts
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
import React, { RefObject } from 'react'
|
||||||
|
|
||||||
|
export function useCursor(ref: RefObject<HTMLDivElement>) {
|
||||||
|
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])
|
||||||
|
}
|
Loading…
Reference in a new issue