packages/core/src/events/synthetic-event-manager.ts (244 lines of code) (raw):
import { assert } from '@canvas-ui/assert'
import { Point } from '../math'
import {
SyntheticEvent,
SyntheticPointerEvent,
SyntheticWheelEvent
} from './synthetic-event'
import { SyntheticEventDispatcher } from './synthetic-event-dispatcher'
import type {
HitTestRoot,
NativeEventBinding,
NativePointerEvents,
SyntheticEventTarget
} from './types'
class PointerState {
/**
* 指针位置
*/
position = Point.zero
/**
* 命中路径
*/
path?: SyntheticEventTarget[]
/**
* 指针位于 target (path[0]) 内的位置
*/
offset?: Point
/**
* 指示是否已 pointerdown
*/
isPointerDown = false
}
export class SyntheticEventManager {
private _rootNode?: HitTestRoot
get rootNode() {
return this._rootNode
}
set rootNode(value) {
if (this._rootNode !== value) {
if (this._rootNode) {
SyntheticEventManager.instances.delete(this._rootNode)
}
this._rootNode = value
if (this._rootNode) {
SyntheticEventManager.instances.set(this._rootNode, this)
}
}
}
binding?: NativeEventBinding
/**
* 主指针状态
*/
private primaryPointerState = new PointerState()
/**
* 滚轮状态
*/
private wheelState = new PointerState()
flushNativeEvents() {
assert(this.binding)
const wheelEvent = this.binding.flushWheelEvent()
// 如果有滚动事件则只处理滚动事件
if (wheelEvent) {
this.handleWheelEvent(wheelEvent)
} else {
this.handlePointerEvents(this.binding.flushPointerEvents())
}
}
private handleWheelEvent(wheelEvent: WheelEvent) {
const { offsetX, offsetY } = wheelEvent
const { wheelState } = this
wheelState.position = Point.fromXY(offsetX, offsetY)
assert(this.rootNode)
const result = this.rootNode.hitTestFromRoot(wheelState.position)
wheelState.path = result.path.map(it => it.target)
wheelState.offset = result.path[0].position
assert(wheelState.path)
assert(wheelState.offset)
const event = new SyntheticWheelEvent('wheel', {
nativeEvent: wheelEvent,
bubbles: wheelEvent.bubbles,
cancelable: wheelEvent.cancelable,
path: wheelState.path,
target: wheelState.path[0],
deltaMode: wheelEvent.deltaMode,
deltaX: wheelEvent.deltaX,
deltaY: wheelEvent.deltaY,
deltaZ: wheelEvent.deltaZ,
offsetX: wheelState.offset.x,
offsetY: wheelState.offset.y,
screenX: wheelState.position.x,
screenY: wheelState.position.y,
})
this.dispatchEvent(event)
}
private handlePointerEvents(pointerEvents: NativePointerEvents) {
for (const pointerId in pointerEvents) {
const {
pointermove,
pointerdown,
pointerup,
pointerleave,
} = pointerEvents[pointerId]
const { primaryPointerState } = this
if (pointerleave) {
const lastPath = primaryPointerState.path
// 当 pointerleave 时不需要检测,清空 path 即可
primaryPointerState.path = undefined
const lastState = {
path: lastPath,
offset: primaryPointerState.offset,
position: primaryPointerState.position,
}
if (lastPath) {
this.dispatchEvent(this.createSyntheticPointerEvent(
'pointerout',
pointerleave,
lastState,
))
this.dispatchEventAtTarget(this.createSyntheticPointerEvent(
'pointerleave',
pointerleave,
lastState,
))
}
}
if (pointermove) {
primaryPointerState.position = Point.fromXY(pointermove.offsetX, pointermove.offsetY)
assert(this.rootNode)
const lastPath = primaryPointerState.path
const result = this.rootNode.hitTestFromRoot(primaryPointerState.position)
primaryPointerState.path = result.path.map(it => it.target)
primaryPointerState.offset = result.path[0].position
const event = this.createSyntheticPointerEvent(
'pointermove',
pointermove,
primaryPointerState,
)
this.dispatchEvent(event)
// 处理 out 和 leave
if (lastPath && lastPath[0] !== primaryPointerState.path[0]) {
const lastState = {
path: lastPath,
offset: primaryPointerState.offset,
position: primaryPointerState.position,
}
this.dispatchEvent(this.createSyntheticPointerEvent(
'pointerout',
pointermove,
lastState,
))
// todo(haocong): pointerleave 实现有误
this.dispatchEventAtTarget(this.createSyntheticPointerEvent(
'pointerleave',
pointermove,
lastState,
))
this.dispatchEvent(this.createSyntheticPointerEvent(
'pointerover',
pointermove,
primaryPointerState,
))
this.dispatchEventAtTarget(this.createSyntheticPointerEvent(
'pointerenter',
pointermove,
primaryPointerState,
))
}
}
if (pointerdown) {
primaryPointerState.isPointerDown = true
primaryPointerState.position = Point.fromXY(pointerdown.offsetX, pointerdown.offsetY)
assert(this.rootNode)
const result = this.rootNode.hitTestFromRoot(primaryPointerState.position)
primaryPointerState.path = result.path.map(it => it.target)
primaryPointerState.offset = result.path[0].position
const event = this.createSyntheticPointerEvent(
'pointerdown',
pointerdown,
primaryPointerState,
)
this.dispatchEvent(event)
}
if (pointerup) {
primaryPointerState.isPointerDown = false
primaryPointerState.position = Point.fromXY(pointerup.offsetX, pointerup.offsetY)
assert(this.rootNode)
const result = this.rootNode.hitTestFromRoot(primaryPointerState.position)
primaryPointerState.path = result.path.map(it => it.target)
primaryPointerState.offset = result.path[0].position
const event = this.createSyntheticPointerEvent(
'pointerup',
pointerup,
primaryPointerState,
)
this.dispatchEvent(event)
}
}
}
private dispatchEventAtTarget(event: SyntheticEvent<SyntheticEventTarget, Event>): boolean {
event.eventPhase = SyntheticEvent.AT_TARGET
const currentTarget = event.composedPath()[0]
event.currentTarget = currentTarget
currentTarget.getDispatcher()?.emitEvent(SyntheticEventDispatcher.getEventKey(event.type), event)
return !event.isDefaultPrevented()
}
dispatchEvent(event: SyntheticEvent<SyntheticEventTarget, Event>): boolean {
assert(event.target, 'event.target 不能是 null')
// composedPath 的第一个节点等于 target,最后一个节点是根节点 (一般是 RenderCanvas)
const composedPath = event.composedPath()
assert(composedPath[0] === event.target, 'event.composedPath()[0] !== event.target')
const n = composedPath.length
// CAPTURING_PHASE: 从尾部开始遍历
event.eventPhase = SyntheticEvent.CAPTURING_PHASE
let key = SyntheticEventDispatcher.getEventKey(event.type, true)
for (let i = n - 1; i > 0 && this.shouldPropagate(event); i--) {
const currentTarget = composedPath[i]
event.currentTarget = currentTarget
currentTarget.getDispatcher()?.emitEvent(key, event)
}
// AT_TARGET
if (this.shouldPropagate(event)) {
event.eventPhase = SyntheticEvent.AT_TARGET
const currentTarget = composedPath[0]
event.currentTarget = currentTarget
key = SyntheticEventDispatcher.getEventKey(event.type)
currentTarget.getDispatcher()?.emitEvent(key, event)
}
// BUBBLING_PHASE: 从头部开始遍历
event.eventPhase = SyntheticEvent.BUBBLING_PHASE
for (let i = 1; i < n && this.shouldPropagate(event); i++) {
const currentTarget = composedPath[i]
event.currentTarget = currentTarget
currentTarget.getDispatcher()?.emitEvent(key, event)
}
// 流程走完后的清理
event.currentTarget = null
return !event.isDefaultPrevented()
}
private shouldPropagate(event: Pick<SyntheticEvent<SyntheticEventTarget, Event>, 'isPropagationStopped' | 'isImmediatePropagationStopped'>) {
return !(event.isPropagationStopped() || event.isImmediatePropagationStopped())
}
private createSyntheticPointerEvent(
type: string,
nativeEvent: PointerEvent,
pointerState: Pick<PointerState, 'path' | 'offset' | 'position'>,
) {
assert(pointerState.path)
assert(pointerState.offset)
const event = new SyntheticPointerEvent(type, {
nativeEvent,
bubbles: nativeEvent.bubbles,
cancelable: nativeEvent.cancelable,
path: pointerState.path,
target: pointerState.path[0],
clientX: nativeEvent.clientX,
clientY: nativeEvent.clientY,
movementX: nativeEvent.movementX,
movementY: nativeEvent.movementY,
relatedTarget: null,
offsetX: pointerState.offset.x,
offsetY: pointerState.offset.y,
screenX: pointerState.position.x,
screenY: pointerState.position.y,
})
return event
}
private static instances = new WeakMap<HitTestRoot, SyntheticEventManager>()
static findInstance(root: HitTestRoot) {
return SyntheticEventManager.instances.get(root)
}
}