fix(useDrag): Refactor to separate detection
* Remove unecessary `modeRef` * Use "Domain modeling" principles to enforce state
This commit is contained in:
parent
1f7206a66f
commit
5d61060828
1 changed files with 143 additions and 69 deletions
|
@ -1,6 +1,5 @@
|
||||||
import type {$FixMe} from '@theatre/shared/utils/types'
|
import type {$FixMe} from '@theatre/shared/utils/types'
|
||||||
import {useLayoutEffect, useRef} from 'react'
|
import {useLayoutEffect, useRef} from 'react'
|
||||||
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
|
||||||
import {useCssCursorLock} from './PointerEventsHandler'
|
import {useCssCursorLock} from './PointerEventsHandler'
|
||||||
import type {CapturedPointer} from '@theatre/studio/UIRoot/PointerCapturing'
|
import type {CapturedPointer} from '@theatre/studio/UIRoot/PointerCapturing'
|
||||||
import {usePointerCapturing} from '@theatre/studio/UIRoot/PointerCapturing'
|
import {usePointerCapturing} from '@theatre/studio/UIRoot/PointerCapturing'
|
||||||
|
@ -97,45 +96,76 @@ export type UseDragOpts = {
|
||||||
| [MouseButton | MouseButton | MouseButton]
|
| [MouseButton | MouseButton | MouseButton]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** How far in total does the cursor have to move before we decide that the user is dragging */
|
||||||
|
const DRAG_DETECTION_DISTANCE_THRESHOLD = 3
|
||||||
|
const DRAG_DETECTION_WAS_POINTER_LOCK_MOVEMENT = 100
|
||||||
|
|
||||||
|
type IUseDragStateRef = IUseDragState_NotStarted | IUseDragState_Started
|
||||||
|
|
||||||
|
type IUseDragState_NotStarted = {
|
||||||
|
/** We have not yet encountered a `"dragstart"` event. */
|
||||||
|
domDragStarted: false
|
||||||
|
}
|
||||||
|
|
||||||
|
type IUseDragState_Started = {
|
||||||
|
/** We have encountered a `"dragstart"` event. */
|
||||||
|
domDragStarted: true
|
||||||
|
detection:
|
||||||
|
| IUseDragStateDetection_Detected
|
||||||
|
| IUseDragStateDetection_NotDetected
|
||||||
|
/**
|
||||||
|
* Used when `isPointerLockUsed` is false, so we can calculate
|
||||||
|
* dx / dy based on the difference of the moved pointer from the start position of the pointer.
|
||||||
|
*
|
||||||
|
* This is generally going to give us a much more accurate estimation than accumulating
|
||||||
|
* movementX & movementY values.
|
||||||
|
*/
|
||||||
|
startPos: {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type IUseDragStateDetection_NotDetected = {
|
||||||
|
detected: false
|
||||||
|
// Used for detection thresholds
|
||||||
|
/** Accumulated in all directions */
|
||||||
|
totalDistanceMoved: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type IUseDragStateDetection_Detected = {
|
||||||
|
detected: true
|
||||||
|
dragMovement: {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Number of drag events since we started guessing this was a drag
|
||||||
|
* This is used to determine if requesting pointer lock causes a
|
||||||
|
* large change to mouse movement (since on at least FF, requesting
|
||||||
|
* pointer lock will move the pointer to the center of the screen)
|
||||||
|
*/
|
||||||
|
dragEventCount: number
|
||||||
|
}
|
||||||
|
|
||||||
export default function useDrag(
|
export default function useDrag(
|
||||||
target: HTMLElement | SVGElement | undefined | null,
|
target: HTMLElement | SVGElement | undefined | null,
|
||||||
opts: UseDragOpts,
|
opts: UseDragOpts,
|
||||||
): [isDragging: boolean] {
|
): [isDragging: boolean] {
|
||||||
const optsRef = useRef<typeof opts>(opts)
|
const optsRef = useRef<UseDragOpts>(opts)
|
||||||
optsRef.current = opts
|
optsRef.current = opts
|
||||||
|
|
||||||
const [modeRef, mode] = useRefAndState<
|
|
||||||
'dragStartCalled' | 'dragging' | 'notDragging'
|
|
||||||
>('notDragging')
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Safari has a gross behavior with locking the pointer changes the height of the webpage
|
* Safari has a gross behavior with locking the pointer changes the height of the webpage
|
||||||
* See {@link UseDragOpts.shouldPointerLock} for more context.
|
* See {@link UseDragOpts.shouldPointerLock} for more context.
|
||||||
*/
|
*/
|
||||||
const isPointerLockUsed = opts.shouldPointerLock && !isSafari
|
const isPointerLockUsed = opts.shouldPointerLock && !isSafari
|
||||||
|
|
||||||
useCssCursorLock(
|
const stateRef = useRef<IUseDragStateRef>({
|
||||||
mode === 'dragging' && typeof opts.lockCSSCursorTo === 'string',
|
domDragStarted: false,
|
||||||
'dragging',
|
})
|
||||||
opts.lockCSSCursorTo,
|
|
||||||
)
|
|
||||||
|
|
||||||
const {capturePointer} = usePointerCapturing(`useDrag for ${opts.debugName}`)
|
const {capturePointer} = usePointerCapturing(`useDrag for ${opts.debugName}`)
|
||||||
|
|
||||||
const stateRef = useRef<{
|
|
||||||
dragHappened: boolean
|
|
||||||
// used when `isPointerLockUsed` is false, so we can calculate
|
|
||||||
// dx / dy based on the difference of the moved pointer from the start position of the pointer.
|
|
||||||
startPos: {
|
|
||||||
x: number
|
|
||||||
y: number
|
|
||||||
}
|
|
||||||
totalMovement: {
|
|
||||||
x: number
|
|
||||||
y: number
|
|
||||||
}
|
|
||||||
}>({dragHappened: false, startPos: {x: 0, y: 0}, totalMovement: {x: 0, y: 0}})
|
|
||||||
|
|
||||||
const callbacksRef = useRef<{
|
const callbacksRef = useRef<{
|
||||||
onDrag: OnDragCallback
|
onDrag: OnDragCallback
|
||||||
onDragEnd: OnDragEndCallback
|
onDragEnd: OnDragEndCallback
|
||||||
|
@ -146,42 +176,64 @@ export default function useDrag(
|
||||||
if (!target) return
|
if (!target) return
|
||||||
|
|
||||||
const dragHandler = (event: MouseEvent) => {
|
const dragHandler = (event: MouseEvent) => {
|
||||||
if (!stateRef.current.dragHappened) {
|
if (!stateRef.current.domDragStarted) return
|
||||||
stateRef.current.dragHappened = true
|
|
||||||
if (isPointerLockUsed) {
|
const stateStarted = stateRef.current
|
||||||
target.requestPointerLock()
|
|
||||||
|
if (didPointerLockCauseMovement(event, stateStarted)) return
|
||||||
|
|
||||||
|
if (!stateStarted.detection.detected) {
|
||||||
|
stateStarted.detection.totalDistanceMoved +=
|
||||||
|
Math.abs(event.movementY) + Math.abs(event.movementX)
|
||||||
|
|
||||||
|
if (
|
||||||
|
stateStarted.detection.totalDistanceMoved >
|
||||||
|
DRAG_DETECTION_DISTANCE_THRESHOLD
|
||||||
|
) {
|
||||||
|
if (isPointerLockUsed) {
|
||||||
|
target.requestPointerLock()
|
||||||
|
}
|
||||||
|
|
||||||
|
stateStarted.detection = {
|
||||||
|
detected: true,
|
||||||
|
dragMovement: {x: 0, y: 0},
|
||||||
|
dragEventCount: 0,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
modeRef.current = 'dragging'
|
|
||||||
|
|
||||||
if (didPointerLockCauseMovement(event)) return
|
// drag detection threshold checking
|
||||||
|
if (stateStarted.detection.detected) {
|
||||||
|
stateStarted.detection.dragEventCount += 1
|
||||||
|
const {dragMovement} = stateStarted.detection
|
||||||
|
if (isPointerLockUsed) {
|
||||||
|
// when locked, the pointer event screen position is going to be 0s, since the pointer can't move.
|
||||||
|
// So, we use the movement on the event
|
||||||
|
dragMovement.x += event.movementX
|
||||||
|
dragMovement.y += event.movementY
|
||||||
|
} else {
|
||||||
|
const {startPos} = stateStarted
|
||||||
|
dragMovement.x = event.screenX - startPos.x
|
||||||
|
dragMovement.y = event.screenY - startPos.y
|
||||||
|
}
|
||||||
|
|
||||||
const {totalMovement} = stateRef.current
|
callbacksRef.current.onDrag(
|
||||||
if (isPointerLockUsed) {
|
dragMovement.x,
|
||||||
// when locked, the pointer event screen position is going to be 0s, since the pointer can't move.
|
dragMovement.y,
|
||||||
totalMovement.x += event.movementX
|
event,
|
||||||
totalMovement.y += event.movementY
|
event.movementX,
|
||||||
} else {
|
event.movementY,
|
||||||
const {startPos} = stateRef.current
|
)
|
||||||
totalMovement.x = event.screenX - startPos.x
|
|
||||||
totalMovement.y = event.screenY - startPos.y
|
|
||||||
}
|
}
|
||||||
|
|
||||||
callbacksRef.current.onDrag(
|
|
||||||
totalMovement.x,
|
|
||||||
totalMovement.y,
|
|
||||||
event,
|
|
||||||
event.movementX,
|
|
||||||
event.movementY,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const dragEndHandler = () => {
|
const dragEndHandler = () => {
|
||||||
removeDragListeners()
|
removeDragListeners()
|
||||||
modeRef.current = 'notDragging'
|
if (!stateRef.current.domDragStarted) return
|
||||||
|
const dragHappened = stateRef.current.detection.detected
|
||||||
|
stateRef.current = {domDragStarted: false}
|
||||||
if (opts.shouldPointerLock && !isSafari) document.exitPointerLock()
|
if (opts.shouldPointerLock && !isSafari) document.exitPointerLock()
|
||||||
|
callbacksRef.current.onDragEnd(dragHappened)
|
||||||
callbacksRef.current.onDragEnd(stateRef.current.dragHappened)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const addDragListeners = () => {
|
const addDragListeners = () => {
|
||||||
|
@ -197,15 +249,16 @@ export default function useDrag(
|
||||||
|
|
||||||
const preventUnwantedClick = (event: MouseEvent) => {
|
const preventUnwantedClick = (event: MouseEvent) => {
|
||||||
if (optsRef.current.disabled) return
|
if (optsRef.current.disabled) return
|
||||||
if (stateRef.current.dragHappened) {
|
if (!stateRef.current.domDragStarted) return
|
||||||
if (
|
if (stateRef.current.detection.detected) {
|
||||||
!optsRef.current.dontBlockMouseDown &&
|
if (!optsRef.current.dontBlockMouseDown) {
|
||||||
modeRef.current !== 'notDragging'
|
|
||||||
) {
|
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
stateRef.current.dragHappened = false
|
stateRef.current.detection = {
|
||||||
|
detected: false,
|
||||||
|
totalDistanceMoved: 0,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,13 +291,13 @@ export default function useDrag(
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
modeRef.current = 'dragStartCalled'
|
|
||||||
|
|
||||||
const {screenX, screenY} = event
|
|
||||||
stateRef.current = {
|
stateRef.current = {
|
||||||
startPos: {x: screenX, y: screenY},
|
domDragStarted: true,
|
||||||
totalMovement: {x: 0, y: 0},
|
startPos: {x: event.screenX, y: event.screenY},
|
||||||
dragHappened: false,
|
detection: {
|
||||||
|
detected: false,
|
||||||
|
totalDistanceMoved: 0,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
addDragListeners()
|
addDragListeners()
|
||||||
|
@ -262,14 +315,23 @@ export default function useDrag(
|
||||||
target.removeEventListener('mousedown', onMouseDown as $FixMe)
|
target.removeEventListener('mousedown', onMouseDown as $FixMe)
|
||||||
target.removeEventListener('click', preventUnwantedClick as $FixMe)
|
target.removeEventListener('click', preventUnwantedClick as $FixMe)
|
||||||
|
|
||||||
if (modeRef.current !== 'notDragging') {
|
if (stateRef.current.domDragStarted) {
|
||||||
callbacksRef.current.onDragEnd?.(modeRef.current === 'dragging')
|
callbacksRef.current.onDragEnd?.(stateRef.current.detection.detected)
|
||||||
}
|
}
|
||||||
modeRef.current = 'notDragging'
|
stateRef.current = {domDragStarted: false}
|
||||||
}
|
}
|
||||||
}, [target])
|
}, [target])
|
||||||
|
|
||||||
return [mode === 'dragging']
|
const isDragging =
|
||||||
|
stateRef.current.domDragStarted && stateRef.current.detection.detected
|
||||||
|
|
||||||
|
useCssCursorLock(
|
||||||
|
isDragging && !!opts.lockCSSCursorTo,
|
||||||
|
'dragging',
|
||||||
|
opts.lockCSSCursorTo,
|
||||||
|
)
|
||||||
|
|
||||||
|
return [isDragging]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -280,6 +342,18 @@ export default function useDrag(
|
||||||
* @param event - MouseEvent from onDrag
|
* @param event - MouseEvent from onDrag
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
function didPointerLockCauseMovement(event: MouseEvent) {
|
function didPointerLockCauseMovement(
|
||||||
return Math.abs(event.movementX) > 100 || Math.abs(event.movementY) > 100
|
event: MouseEvent,
|
||||||
|
state: IUseDragState_Started,
|
||||||
|
) {
|
||||||
|
const isEarlyInDragging =
|
||||||
|
!state.detection.detected ||
|
||||||
|
(state.detection.detected && state.detection.dragEventCount < 3)
|
||||||
|
|
||||||
|
return (
|
||||||
|
isEarlyInDragging &&
|
||||||
|
// sudden movement
|
||||||
|
(Math.abs(event.movementX) > DRAG_DETECTION_WAS_POINTER_LOCK_MOVEMENT ||
|
||||||
|
Math.abs(event.movementY) > DRAG_DETECTION_WAS_POINTER_LOCK_MOVEMENT)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue