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:
Judicael 2022-09-07 16:58:32 +03:00 committed by GitHub
parent bb9e7ee47c
commit e1689d678e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 170 additions and 36 deletions

View file

@ -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)

View file

@ -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)

View file

@ -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)
}, },

View file

@ -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)
}, },

View file

@ -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)
}, },

View file

@ -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%',

View 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])
}