mirror of
https://github.com/enso-org/enso.git
synced 2024-12-22 23:01:29 +03:00
Pan capturing (#10304)
Panning or zooming "captures" wheel events. While events are captured, further events of the same type will continue the pan/zoom action. The capture expires if no events are received for 250ms. A trackpad capture also expires if any pointer movement occurs.
This commit is contained in:
parent
ccfeac1c02
commit
3f70307a88
@ -662,6 +662,8 @@ const groupColors = computed(() => {
|
|||||||
@click="handleClick"
|
@click="handleClick"
|
||||||
@dragover.prevent
|
@dragover.prevent
|
||||||
@drop.prevent="handleFileDrop($event)"
|
@drop.prevent="handleFileDrop($event)"
|
||||||
|
@pointermove.capture="graphNavigator.pointerEventsCapture.pointermove"
|
||||||
|
@wheel.capture="graphNavigator.pointerEventsCapture.wheel"
|
||||||
>
|
>
|
||||||
<div class="layer" :style="{ transform: graphNavigator.transform }">
|
<div class="layer" :style="{ transform: graphNavigator.transform }">
|
||||||
<GraphNodes
|
<GraphNodes
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
/** @file Vue composables for listening to DOM events. */
|
/** @file Vue composables for listening to DOM events. */
|
||||||
|
|
||||||
|
import type { KeyboardComposable } from '@/composables/keyboard.ts'
|
||||||
import type { Opt } from '@/util/data/opt'
|
import type { Opt } from '@/util/data/opt'
|
||||||
import { Vec2 } from '@/util/data/vec2'
|
import { Vec2 } from '@/util/data/vec2'
|
||||||
import { type VueInstance } from '@vueuse/core'
|
import { type VueInstance } from '@vueuse/core'
|
||||||
@ -528,3 +529,85 @@ export function useArrows(
|
|||||||
|
|
||||||
return { events, moving }
|
return { events, moving }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Supports panning or zooming "capturing" wheel events.
|
||||||
|
*
|
||||||
|
* While events are captured, further events of the same type will continue the pan/zoom action.
|
||||||
|
* The capture expires if no events are received within the specified `captureDurationMs`.
|
||||||
|
* A trackpad capture also expires if any pointer movement occurs.
|
||||||
|
*/
|
||||||
|
export function useWheelActions(
|
||||||
|
keyboard: KeyboardComposable,
|
||||||
|
captureDurationMs: number,
|
||||||
|
onZoom: (e: WheelEvent, inputType: 'trackpad' | 'wheel') => boolean | void,
|
||||||
|
onPan: (e: WheelEvent) => boolean | void,
|
||||||
|
) {
|
||||||
|
let prevEventPanInfo:
|
||||||
|
| ({ expiration: number } & (
|
||||||
|
| { type: 'trackpad-zoom' }
|
||||||
|
| { type: 'wheel-zoom' }
|
||||||
|
| { type: 'pan'; trackpad: boolean }
|
||||||
|
))
|
||||||
|
| undefined = undefined
|
||||||
|
|
||||||
|
type WheelEventType = 'trackpad-zoom' | 'wheel-zoom' | 'pan'
|
||||||
|
function classifyEvent(e: WheelEvent): WheelEventType {
|
||||||
|
if (e.ctrlKey) {
|
||||||
|
// A pinch gesture is represented by setting `e.ctrlKey`. It can be distinguished from an actual Ctrl+wheel
|
||||||
|
// combination because the real Ctrl key emits keyup/keydown events.
|
||||||
|
const isGesture = !keyboard.ctrl
|
||||||
|
return isGesture ? 'trackpad-zoom' : 'wheel-zoom'
|
||||||
|
} else {
|
||||||
|
return 'pan'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleWheel(e: WheelEvent) {
|
||||||
|
const newType = classifyEvent(e)
|
||||||
|
if (e.eventPhase === e.CAPTURING_PHASE) {
|
||||||
|
if (newType !== prevEventPanInfo?.type || e.timeStamp > prevEventPanInfo.expiration) {
|
||||||
|
prevEventPanInfo = undefined
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const expiration = e.timeStamp + captureDurationMs
|
||||||
|
if (newType === 'wheel-zoom') {
|
||||||
|
prevEventPanInfo = { expiration, type: newType }
|
||||||
|
onZoom(e, 'wheel')
|
||||||
|
} else if (newType === 'trackpad-zoom') {
|
||||||
|
prevEventPanInfo = { expiration, type: newType }
|
||||||
|
onZoom(e, 'trackpad')
|
||||||
|
} else if (newType === 'pan') {
|
||||||
|
const alreadyKnownTrackpad = prevEventPanInfo?.type === 'pan' && prevEventPanInfo.trackpad
|
||||||
|
prevEventPanInfo = {
|
||||||
|
expiration,
|
||||||
|
type: newType,
|
||||||
|
// Heuristic: Trackpad panning is usually multi-axis; wheel panning is not.
|
||||||
|
trackpad: alreadyKnownTrackpad || (e.deltaX !== 0 && e.deltaY !== 0),
|
||||||
|
}
|
||||||
|
onPan(e)
|
||||||
|
}
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
}
|
||||||
|
|
||||||
|
function pointermove() {
|
||||||
|
// If a `pointermove` event occurs, any trackpad action has ended.
|
||||||
|
if (
|
||||||
|
prevEventPanInfo?.type === 'trackpad-zoom' ||
|
||||||
|
(prevEventPanInfo?.type === 'pan' && prevEventPanInfo.trackpad)
|
||||||
|
) {
|
||||||
|
prevEventPanInfo = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
events: {
|
||||||
|
wheel: handleWheel,
|
||||||
|
},
|
||||||
|
captureEvents: {
|
||||||
|
pointermove,
|
||||||
|
wheel: handleWheel,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
useEvent,
|
useEvent,
|
||||||
usePointer,
|
usePointer,
|
||||||
useResizeObserver,
|
useResizeObserver,
|
||||||
|
useWheelActions,
|
||||||
} from '@/composables/events'
|
} from '@/composables/events'
|
||||||
import type { KeyboardComposable } from '@/composables/keyboard'
|
import type { KeyboardComposable } from '@/composables/keyboard'
|
||||||
import { Rect } from '@/util/data/rect'
|
import { Rect } from '@/util/data/rect'
|
||||||
@ -24,6 +25,7 @@ const ZOOM_LEVELS_REVERSED = [...ZOOM_LEVELS].reverse()
|
|||||||
* If we are that close to next zoom level, we should choose the next one instead
|
* If we are that close to next zoom level, we should choose the next one instead
|
||||||
* to avoid small unnoticeable changes to zoom. */
|
* to avoid small unnoticeable changes to zoom. */
|
||||||
const ZOOM_SKIP_THRESHOLD = 0.05
|
const ZOOM_SKIP_THRESHOLD = 0.05
|
||||||
|
const WHEEL_CAPTURE_DURATION_MS = 250
|
||||||
|
|
||||||
function elemRect(target: Element | undefined): Rect {
|
function elemRect(target: Element | undefined): Rect {
|
||||||
if (target != null && target instanceof Element)
|
if (target != null && target instanceof Element)
|
||||||
@ -275,6 +277,24 @@ export function useNavigator(
|
|||||||
scale.skip()
|
scale.skip()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { events: wheelEvents, captureEvents: wheelEventsCapture } = useWheelActions(
|
||||||
|
keyboard,
|
||||||
|
WHEEL_CAPTURE_DURATION_MS,
|
||||||
|
(e, inputType) => {
|
||||||
|
if (inputType === 'trackpad') {
|
||||||
|
// OS X trackpad events provide usable rate-of-change information.
|
||||||
|
updateScale((oldValue: number) => oldValue * Math.exp(-e.deltaY / 100))
|
||||||
|
} else {
|
||||||
|
// Mouse wheel rate information is unreliable. We just step in the direction of the sign.
|
||||||
|
stepZoom(-Math.sign(e.deltaY))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(e) => {
|
||||||
|
const delta = new Vec2(e.deltaX, e.deltaY)
|
||||||
|
scrollTo(center.value.addScaled(delta, 1 / scale.value))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
return proxyRefs({
|
return proxyRefs({
|
||||||
pointerEvents: {
|
pointerEvents: {
|
||||||
dragover(e: DragEvent) {
|
dragover(e: DragEvent) {
|
||||||
@ -301,28 +321,12 @@ export function useNavigator(
|
|||||||
panPointer.events.pointerdown(e)
|
panPointer.events.pointerdown(e)
|
||||||
zoomPointer.events.pointerdown(e)
|
zoomPointer.events.pointerdown(e)
|
||||||
},
|
},
|
||||||
wheel(e: WheelEvent) {
|
|
||||||
e.preventDefault()
|
|
||||||
if (e.ctrlKey) {
|
|
||||||
// A pinch gesture is represented by setting `e.ctrlKey`. It can be distinguished from an actual Ctrl+wheel
|
|
||||||
// combination because the real Ctrl key emits keyup/keydown events.
|
|
||||||
const isGesture = !keyboard.ctrl
|
|
||||||
if (isGesture) {
|
|
||||||
// OS X trackpad events provide usable rate-of-change information.
|
|
||||||
updateScale((oldValue: number) => oldValue * Math.exp(-e.deltaY / 100))
|
|
||||||
} else {
|
|
||||||
// Mouse wheel rate information is unreliable. We just step in the direction of the sign.
|
|
||||||
stepZoom(-Math.sign(e.deltaY))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const delta = new Vec2(e.deltaX, e.deltaY)
|
|
||||||
scrollTo(center.value.addScaled(delta, 1 / scale.value))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
contextmenu(e: Event) {
|
contextmenu(e: Event) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
},
|
},
|
||||||
|
wheel: wheelEvents.wheel,
|
||||||
},
|
},
|
||||||
|
pointerEventsCapture: wheelEventsCapture,
|
||||||
keyboardEvents: panArrows.events,
|
keyboardEvents: panArrows.events,
|
||||||
translate,
|
translate,
|
||||||
targetCenter: readonly(targetCenter),
|
targetCenter: readonly(targetCenter),
|
||||||
|
Loading…
Reference in New Issue
Block a user