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)
|
||||
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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
|
|
|
@ -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<any>(null)
|
||||
|
||||
const rWrapper = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
const state = app.useStore()
|
||||
|
@ -464,6 +464,8 @@ const InnerTldraw = React.memo(function InnerTldraw({
|
|||
}
|
||||
}, [settings.isDarkMode])
|
||||
|
||||
useCursor(rWrapper)
|
||||
|
||||
return (
|
||||
<ContainerContext.Provider value={rWrapper}>
|
||||
<IntlProvider locale={translation.locale} messages={translation.messages}>
|
||||
|
@ -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%',
|
||||
|
|
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