Add shouldPointerLock to useDrag, BasicNumberInput

This commit is contained in:
vezwork 2022-05-13 11:10:03 -04:00 committed by Cole Lawrence
parent b547282d95
commit c5ccb8c28d
3 changed files with 84 additions and 26 deletions

View file

@ -325,6 +325,7 @@ const BasicNumberInput: React.FC<{
debugName: 'form/BasicNumberInput', debugName: 'form/BasicNumberInput',
onDragStart: callbacks.transitionToDraggingMode, onDragStart: callbacks.transitionToDraggingMode,
lockCSSCursorTo: 'ew-resize', lockCSSCursorTo: 'ew-resize',
shouldPointerLock: true,
disabled: stateRef.current.mode === 'editingViaKeyboard', disabled: stateRef.current.mode === 'editingViaKeyboard',
}) })

View file

@ -0,0 +1,3 @@
export const isSafari = /^((?!chrome|android).)*safari/i.test(
navigator.userAgent,
)

View file

@ -5,6 +5,7 @@ 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'
import noop from '@theatre/shared/utils/noop' import noop from '@theatre/shared/utils/noop'
import {isSafari} from './isSafari'
export enum MouseButton { export enum MouseButton {
Left = 0, Left = 0,
@ -15,11 +16,19 @@ export enum MouseButton {
/** /**
* dx, dy: delta x/y from the start of the drag * dx, dy: delta x/y from the start of the drag
*
* Total movement since the start of the drag. This is commonly used with something like "drag keyframe" or "drag handle",
* where you might be dragging an item around in the UI.
* @param totalDragDeltaX - x moved total
* @param totalDragDeltaY - y moved total
*
* Movement from the last event / on drag call. This is commonly used with something like "prop nudge".
* @param dxFromLastEvent - x moved since last event
* @param dyFromLastEvent - y moved since last event
*/ */
type OnDragCallback = ( type OnDragCallback = (
// deltaX/Y are counted from the start of the drag totalDragDeltaX: number,
deltaX: number, totalDragDeltaY: number,
deltaY: number,
event: MouseEvent, event: MouseEvent,
dxFromLastEvent: number, dxFromLastEvent: number,
dyFromLastEvent: number, dyFromLastEvent: number,
@ -41,6 +50,18 @@ export type UseDragOpts = {
* Setting it to true will allow the mouse down events to propagate up * Setting it to true will allow the mouse down events to propagate up
*/ */
dontBlockMouseDown?: boolean dontBlockMouseDown?: boolean
/**
* Tells the browser to take control of the mouse pointer so that
* the user can drag endlessly in any direction without hitting the
* side of their screen.
*
* Note: that if we detect that the browser is
* safari then pointer lock is not used because the pointer lock
* banner annoyingly shifts the entire page down.
*
* ref: https://developer.mozilla.org/en-US/docs/Web/API/Pointer_Lock_API
*/
shouldPointerLock?: boolean
/** /**
* The css cursor property during the gesture will be locked to this value * The css cursor property during the gesture will be locked to this value
*/ */
@ -87,6 +108,12 @@ export default function useDrag(
'dragStartCalled' | 'dragging' | 'notDragging' 'dragStartCalled' | 'dragging' | 'notDragging'
>('notDragging') >('notDragging')
/**
* Safari has a gross behavior with locking the pointer changes the height of the webpage
* See {@link UseDragOpts.shouldPointerLock} for more context.
*/
const isPointerLockUsed = opts.shouldPointerLock && !isSafari
useCssCursorLock( useCssCursorLock(
mode === 'dragging' && typeof opts.lockCSSCursorTo === 'string', mode === 'dragging' && typeof opts.lockCSSCursorTo === 'string',
'dragging', 'dragging',
@ -97,11 +124,17 @@ export default function useDrag(
const stateRef = useRef<{ const stateRef = useRef<{
dragHappened: boolean 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: { startPos: {
x: number x: number
y: number y: number
} }
}>({dragHappened: false, startPos: {x: 0, y: 0}}) 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
@ -111,36 +144,42 @@ export default function useDrag(
const capturedPointerRef = useRef<undefined | CapturedPointer>() const capturedPointerRef = useRef<undefined | CapturedPointer>()
useLayoutEffect(() => { useLayoutEffect(() => {
if (!target) return if (!target) return
let lastDeltas = [0, 0]
const getDistances = (event: MouseEvent): [number, number] => {
const {startPos} = stateRef.current
return [event.screenX - startPos.x, event.screenY - startPos.y]
}
const dragHandler = (event: MouseEvent) => { const dragHandler = (event: MouseEvent) => {
if (!stateRef.current.dragHappened) stateRef.current.dragHappened = true if (!stateRef.current.dragHappened) {
stateRef.current.dragHappened = true
if (isPointerLockUsed) {
target.requestPointerLock()
}
}
modeRef.current = 'dragging' modeRef.current = 'dragging'
const deltas = getDistances(event) if (didPointerLockCauseMovement(event)) return
const [deltaFromLastX, deltaFromLastY] = [
deltas[0] - lastDeltas[0], const {totalMovement} = stateRef.current
deltas[1] - lastDeltas[1], if (isPointerLockUsed) {
] // when locked, the pointer event screen position is going to be 0s, since the pointer can't move.
lastDeltas = deltas totalMovement.x += event.movementX
totalMovement.y += event.movementY
} else {
const {startPos} = stateRef.current
totalMovement.x = event.screenX - startPos.x
totalMovement.y = event.screenY - startPos.y
}
callbacksRef.current.onDrag( callbacksRef.current.onDrag(
deltas[0], totalMovement.x,
deltas[1], totalMovement.y,
event, event,
deltaFromLastX, event.movementX,
deltaFromLastY, event.movementY,
) )
} }
const dragEndHandler = () => { const dragEndHandler = () => {
removeDragListeners() removeDragListeners()
modeRef.current = 'notDragging' modeRef.current = 'notDragging'
if (opts.shouldPointerLock && !isSafari) document.exitPointerLock()
callbacksRef.current.onDragEnd(stateRef.current.dragHappened) callbacksRef.current.onDragEnd(stateRef.current.dragHappened)
} }
@ -186,11 +225,11 @@ export default function useDrag(
if (returnOfOnDragStart === false) { if (returnOfOnDragStart === false) {
// we should ignore the gesture // we should ignore the gesture
return return
} else {
callbacksRef.current.onDrag = returnOfOnDragStart.onDrag
callbacksRef.current.onDragEnd = returnOfOnDragStart.onDragEnd ?? noop
} }
callbacksRef.current.onDrag = returnOfOnDragStart.onDrag
callbacksRef.current.onDragEnd = returnOfOnDragStart.onDragEnd ?? noop
// need to capture pointer after we know the provided handler wants to handle drag start // need to capture pointer after we know the provided handler wants to handle drag start
capturedPointerRef.current = capturePointer('Drag start') capturedPointerRef.current = capturePointer('Drag start')
@ -202,8 +241,11 @@ export default function useDrag(
modeRef.current = 'dragStartCalled' modeRef.current = 'dragStartCalled'
const {screenX, screenY} = event const {screenX, screenY} = event
stateRef.current.startPos = {x: screenX, y: screenY} stateRef.current = {
stateRef.current.dragHappened = false startPos: {x: screenX, y: screenY},
totalMovement: {x: 0, y: 0},
dragHappened: false,
}
addDragListeners() addDragListeners()
} }
@ -229,3 +271,15 @@ export default function useDrag(
return [mode === 'dragging'] return [mode === 'dragging']
} }
/**
* shouldPointerLock moves the mouse to the center of your screen in firefox, which
* can cause it to report very large movementX when the pointer lock begins. This
* function hackily detects unnaturally large movements of the mouse.
*
* @param event - MouseEvent from onDrag
* @returns
*/
function didPointerLockCauseMovement(event: MouseEvent) {
return Math.abs(event.movementX) > 100 || Math.abs(event.movementY) > 100
}