input-observer.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  1. /**
  2. * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  5. */
  6. import { Subject, Observable } from 'rxjs';
  7. import { Vec2 } from '../../mol-math/linear-algebra';
  8. import { BitFlags, noop } from '../../mol-util';
  9. export function getButtons(event: MouseEvent | Touch) {
  10. if (typeof event === 'object') {
  11. if ('buttons' in event) {
  12. return event.buttons
  13. } else if ('which' in event) {
  14. const b = (event as any).which // 'any' to support older browsers
  15. if (b === 2) {
  16. return 4
  17. } else if (b === 3) {
  18. return 2
  19. } else if (b > 0) {
  20. return 1 << (b - 1)
  21. }
  22. } else if ('button' in event) {
  23. const b = (event as any).button // 'any' to support older browsers
  24. if (b === 1) {
  25. return 4
  26. } else if (b === 2) {
  27. return 2
  28. } else if (b >= 0) {
  29. return 1 << b
  30. }
  31. }
  32. }
  33. return 0
  34. }
  35. export function getModifiers(event: MouseEvent | Touch) {
  36. return {
  37. alt: 'altKey' in event ? event.altKey : false,
  38. shift: 'shiftKey' in event ? event.shiftKey : false,
  39. control: 'ctrlKey' in event ? event.ctrlKey : false,
  40. meta: 'metaKey' in event ? event.metaKey : false
  41. }
  42. }
  43. export const DefaultInputObserverProps = {
  44. noScroll: true,
  45. noContextMenu: true,
  46. noPinchZoom: true
  47. }
  48. export type InputObserverProps = Partial<typeof DefaultInputObserverProps>
  49. export type ModifiersKeys = {
  50. shift: boolean,
  51. alt: boolean,
  52. control: boolean,
  53. meta: boolean
  54. }
  55. export namespace ModifiersKeys {
  56. export const None: ModifiersKeys = { shift: false, alt: false, control: false, meta: false };
  57. export function areEqual(a: ModifiersKeys, b: ModifiersKeys) {
  58. return a.shift === b.shift && a.alt === b.alt && a.control === b.control && a.meta === b.meta;
  59. }
  60. }
  61. export type ButtonsType = BitFlags<ButtonsType.Flag>
  62. export namespace ButtonsType {
  63. export const has: (btn: ButtonsType, f: Flag) => boolean = BitFlags.has
  64. export const create: (fs: Flag) => ButtonsType = BitFlags.create
  65. export const enum Flag {
  66. /** No button or un-initialized */
  67. None = 0x0,
  68. /** Primary button (usually left) */
  69. Primary = 0x1,
  70. /** Secondary button (usually right) */
  71. Secondary = 0x2,
  72. /** Auxilary button (usually middle or mouse wheel button) */
  73. Auxilary = 0x4,
  74. /** 4th button (typically the "Browser Back" button) */
  75. Forth = 0x8,
  76. /** 5th button (typically the "Browser Forward" button) */
  77. Five = 0x10,
  78. }
  79. }
  80. type BaseInput = {
  81. buttons: ButtonsType
  82. modifiers: ModifiersKeys
  83. }
  84. export type DragInput = {
  85. x: number,
  86. y: number,
  87. dx: number,
  88. dy: number,
  89. pageX: number,
  90. pageY: number,
  91. isStart: boolean
  92. } & BaseInput
  93. export type WheelInput = {
  94. dx: number,
  95. dy: number,
  96. dz: number,
  97. } & BaseInput
  98. export type ClickInput = {
  99. x: number,
  100. y: number,
  101. pageX: number,
  102. pageY: number,
  103. } & BaseInput
  104. export type MoveInput = {
  105. x: number,
  106. y: number,
  107. pageX: number,
  108. pageY: number,
  109. inside: boolean,
  110. } & BaseInput
  111. export type PinchInput = {
  112. delta: number,
  113. fraction: number,
  114. distance: number,
  115. isStart: boolean
  116. }
  117. export type ResizeInput = {
  118. }
  119. const enum DraggingState {
  120. Stopped = 0,
  121. Started = 1,
  122. Moving = 2
  123. }
  124. type PointerEvent = {
  125. clientX: number
  126. clientY: number
  127. pageX: number
  128. pageY: number
  129. }
  130. interface InputObserver {
  131. noScroll: boolean
  132. noContextMenu: boolean
  133. readonly drag: Observable<DragInput>,
  134. // Equivalent to mouseUp and touchEnd
  135. readonly interactionEnd: Observable<undefined>,
  136. readonly wheel: Observable<WheelInput>,
  137. readonly pinch: Observable<PinchInput>,
  138. readonly click: Observable<ClickInput>,
  139. readonly move: Observable<MoveInput>,
  140. readonly leave: Observable<undefined>,
  141. readonly enter: Observable<undefined>,
  142. readonly resize: Observable<ResizeInput>,
  143. readonly modifiers: Observable<ModifiersKeys>
  144. dispose: () => void
  145. }
  146. function createEvents() {
  147. return {
  148. drag: new Subject<DragInput>(),
  149. interactionEnd: new Subject<undefined>(),
  150. click: new Subject<ClickInput>(),
  151. move: new Subject<MoveInput>(),
  152. wheel: new Subject<WheelInput>(),
  153. pinch: new Subject<PinchInput>(),
  154. resize: new Subject<ResizeInput>(),
  155. leave: new Subject<undefined>(),
  156. enter: new Subject<undefined>(),
  157. modifiers: new Subject<ModifiersKeys>(),
  158. }
  159. }
  160. namespace InputObserver {
  161. export function create(props: InputObserverProps = {}): InputObserver {
  162. const { noScroll, noContextMenu } = { ...DefaultInputObserverProps, ...props }
  163. return {
  164. noScroll,
  165. noContextMenu,
  166. ...createEvents(),
  167. dispose: noop
  168. }
  169. }
  170. export function fromElement(element: Element, props: InputObserverProps = {}): InputObserver {
  171. let { noScroll, noContextMenu, noPinchZoom } = { ...DefaultInputObserverProps, ...props }
  172. let lastTouchDistance = 0
  173. const pointerDown = Vec2.zero()
  174. const pointerStart = Vec2.zero()
  175. const pointerEnd = Vec2.zero()
  176. const pointerDelta = Vec2.zero()
  177. const rectSize = Vec2.zero()
  178. const modifierKeys: ModifiersKeys = {
  179. shift: false,
  180. alt: false,
  181. control: false,
  182. meta: false
  183. }
  184. function getModifierKeys(): ModifiersKeys {
  185. return { ...modifierKeys };
  186. }
  187. let dragging: DraggingState = DraggingState.Stopped
  188. let disposed = false
  189. let buttons = 0 as ButtonsType
  190. let isInside = false
  191. const events = createEvents()
  192. const { drag, interactionEnd, wheel, pinch, click, move, leave, enter, resize, modifiers } = events
  193. attach()
  194. return {
  195. get noScroll () { return noScroll },
  196. set noScroll (value: boolean) { noScroll = value },
  197. get noContextMenu () { return noContextMenu },
  198. set noContextMenu (value: boolean) { noContextMenu = value },
  199. ...events,
  200. dispose
  201. }
  202. function attach () {
  203. element.addEventListener('contextmenu', onContextMenu, false )
  204. element.addEventListener('wheel', onMouseWheel as any, false)
  205. element.addEventListener('mousedown', onMouseDown as any, false)
  206. // for dragging to work outside canvas bounds,
  207. // mouse move/up events have to be added to a parent, i.e. window
  208. window.addEventListener('mousemove', onMouseMove as any, false)
  209. window.addEventListener('mouseup', onMouseUp as any, false)
  210. element.addEventListener('mouseenter', onMouseEnter as any, false)
  211. element.addEventListener('mouseleave', onMouseLeave as any, false)
  212. element.addEventListener('touchstart', onTouchStart as any, false)
  213. element.addEventListener('touchmove', onTouchMove as any, false)
  214. element.addEventListener('touchend', onTouchEnd as any, false)
  215. element.addEventListener('blur', handleBlur)
  216. window.addEventListener('keyup', handleKeyUp as EventListener, false)
  217. window.addEventListener('keydown', handleKeyDown as EventListener, false)
  218. window.addEventListener('resize', onResize, false)
  219. }
  220. function dispose () {
  221. if (disposed) return
  222. disposed = true
  223. element.removeEventListener( 'contextmenu', onContextMenu, false )
  224. element.removeEventListener('wheel', onMouseWheel as any, false)
  225. element.removeEventListener('mousedown', onMouseDown as any, false)
  226. window.removeEventListener('mousemove', onMouseMove as any, false)
  227. window.removeEventListener('mouseup', onMouseUp as any, false)
  228. element.removeEventListener('mouseenter', onMouseEnter as any, false)
  229. element.removeEventListener('mouseleave', onMouseLeave as any, false)
  230. element.removeEventListener('touchstart', onTouchStart as any, false)
  231. element.removeEventListener('touchmove', onTouchMove as any, false)
  232. element.removeEventListener('touchend', onTouchEnd as any, false)
  233. element.removeEventListener('blur', handleBlur)
  234. window.removeEventListener('keyup', handleKeyUp as EventListener, false)
  235. window.removeEventListener('keydown', handleKeyDown as EventListener, false)
  236. window.removeEventListener('resize', onResize, false)
  237. }
  238. function onContextMenu(event: Event) {
  239. if (noContextMenu) {
  240. event.preventDefault()
  241. }
  242. }
  243. function handleBlur () {
  244. if (buttons || modifierKeys.shift || modifierKeys.alt || modifierKeys.meta || modifierKeys.control) {
  245. buttons = 0 as ButtonsType
  246. modifierKeys.shift = modifierKeys.alt = modifierKeys.control = modifierKeys.meta = false
  247. }
  248. }
  249. function handleKeyDown (event: KeyboardEvent) {
  250. let changed = false;
  251. if (!modifierKeys.alt && event.altKey) { changed = true; modifierKeys.alt = true; }
  252. if (!modifierKeys.shift && event.shiftKey) { changed = true; modifierKeys.shift = true; }
  253. if (!modifierKeys.control && event.ctrlKey) { changed = true; modifierKeys.control = true; }
  254. if (!modifierKeys.meta && event.metaKey) { changed = true; modifierKeys.meta = true; }
  255. if (changed && isInside) modifiers.next(getModifierKeys());
  256. }
  257. function handleKeyUp (event: KeyboardEvent) {
  258. let changed = false;
  259. if (modifierKeys.alt && !event.altKey) { changed = true; modifierKeys.alt = false; }
  260. if (modifierKeys.shift && !event.shiftKey) { changed = true; modifierKeys.shift = false; }
  261. if (modifierKeys.control && !event.ctrlKey) { changed = true; modifierKeys.control = false; }
  262. if (modifierKeys.meta && !event.metaKey) { changed = true; modifierKeys.meta = false; }
  263. if (changed && isInside) modifiers.next(getModifierKeys());
  264. }
  265. function getCenterTouch (ev: TouchEvent): PointerEvent {
  266. const t0 = ev.touches[0]
  267. const t1 = ev.touches[1]
  268. return {
  269. clientX: (t0.clientX + t1.clientX) / 2,
  270. clientY: (t0.clientY + t1.clientY) / 2,
  271. pageX: (t0.pageX + t1.pageX) / 2,
  272. pageY: (t0.pageY + t1.pageY) / 2
  273. }
  274. }
  275. function getTouchDistance (ev: TouchEvent) {
  276. const dx = ev.touches[0].pageX - ev.touches[1].pageX;
  277. const dy = ev.touches[0].pageY - ev.touches[1].pageY;
  278. return Math.sqrt(dx * dx + dy * dy);
  279. }
  280. function onTouchStart (ev: TouchEvent) {
  281. if (ev.touches.length === 1) {
  282. buttons = ButtonsType.Flag.Primary
  283. onPointerDown(ev.touches[0])
  284. } else if (ev.touches.length >= 2) {
  285. buttons = ButtonsType.Flag.Secondary
  286. onPointerDown(getCenterTouch(ev))
  287. const touchDistance = getTouchDistance(ev)
  288. lastTouchDistance = touchDistance
  289. pinch.next({ distance: touchDistance, fraction: 1, delta: 0, isStart: true })
  290. }
  291. }
  292. function onTouchEnd (ev: TouchEvent) {
  293. endDrag()
  294. }
  295. function onTouchMove (ev: TouchEvent) {
  296. if (noPinchZoom) {
  297. ev.preventDefault();
  298. ev.stopPropagation();
  299. if ((ev as any).originalEvent) {
  300. (ev as any).originalEvent.preventDefault();
  301. (ev as any).originalEvent.stopPropagation();
  302. }
  303. }
  304. if (ev.touches.length === 1) {
  305. buttons = ButtonsType.Flag.Primary
  306. onPointerMove(ev.touches[0])
  307. } else if (ev.touches.length >= 2) {
  308. const touchDistance = getTouchDistance(ev)
  309. const touchDelta = lastTouchDistance - touchDistance
  310. if (Math.abs(touchDelta) < 4) {
  311. buttons = ButtonsType.Flag.Secondary
  312. onPointerMove(getCenterTouch(ev))
  313. } else {
  314. pinch.next({
  315. delta: touchDelta,
  316. fraction: lastTouchDistance / touchDistance,
  317. distance: touchDistance,
  318. isStart: false
  319. })
  320. }
  321. lastTouchDistance = touchDistance
  322. }
  323. }
  324. function onMouseDown (ev: MouseEvent) {
  325. buttons = getButtons(ev)
  326. onPointerDown(ev)
  327. }
  328. function onMouseMove (ev: MouseEvent) {
  329. buttons = getButtons(ev)
  330. onPointerMove(ev)
  331. }
  332. function onMouseUp (ev: MouseEvent) {
  333. onPointerUp(ev)
  334. endDrag()
  335. }
  336. function endDrag() {
  337. interactionEnd.next()
  338. }
  339. function onPointerDown (ev: PointerEvent) {
  340. eventOffset(pointerStart, ev)
  341. Vec2.copy(pointerDown, pointerStart)
  342. if (insideBounds(pointerStart)) {
  343. dragging = DraggingState.Started
  344. }
  345. }
  346. function onPointerUp (ev: PointerEvent) {
  347. dragging = DraggingState.Stopped
  348. eventOffset(pointerEnd, ev);
  349. if (Vec2.distance(pointerEnd, pointerDown) < 4) {
  350. const { pageX, pageY } = ev
  351. const [ x, y ] = pointerEnd
  352. click.next({ x, y, pageX, pageY, buttons, modifiers: getModifierKeys() })
  353. }
  354. }
  355. function onPointerMove (ev: PointerEvent) {
  356. eventOffset(pointerEnd, ev)
  357. const { pageX, pageY } = ev
  358. const [ x, y ] = pointerEnd
  359. const inside = insideBounds(pointerEnd)
  360. move.next({ x, y, pageX, pageY, buttons, modifiers: getModifierKeys(), inside })
  361. if (dragging === DraggingState.Stopped) return
  362. Vec2.div(pointerDelta, Vec2.sub(pointerDelta, pointerEnd, pointerStart), getClientSize(rectSize))
  363. const isStart = dragging === DraggingState.Started
  364. const [ dx, dy ] = pointerDelta
  365. drag.next({ x, y, dx, dy, pageX, pageY, buttons, modifiers: getModifierKeys(), isStart })
  366. Vec2.copy(pointerStart, pointerEnd)
  367. dragging = DraggingState.Moving
  368. }
  369. function onMouseWheel(ev: WheelEvent) {
  370. if (noScroll) {
  371. ev.preventDefault()
  372. }
  373. let scale = 1
  374. switch (ev.deltaMode) {
  375. case 0: scale = 1; break // pixels
  376. case 1: scale = 40; break // lines
  377. case 2: scale = 800; break // pages
  378. }
  379. const dx = (ev.deltaX || 0) * scale
  380. const dy = (ev.deltaY || 0) * scale
  381. const dz = (ev.deltaZ || 0) * scale
  382. if (dx || dy || dz) {
  383. wheel.next({ dx, dy, dz, buttons, modifiers: getModifierKeys() })
  384. }
  385. }
  386. function onMouseEnter (ev: Event) {
  387. isInside = true;
  388. enter.next();
  389. }
  390. function onMouseLeave (ev: Event) {
  391. isInside = false;
  392. leave.next();
  393. }
  394. function onResize (ev: Event) {
  395. resize.next()
  396. }
  397. function insideBounds (pos: Vec2) {
  398. if (element instanceof Window || element instanceof Document || element === document.body) {
  399. return true
  400. } else {
  401. const rect = element.getBoundingClientRect()
  402. return pos[0] >= 0 && pos[1] >= 0 && pos[0] < rect.width && pos[1] < rect.height
  403. }
  404. }
  405. function getClientSize (out: Vec2) {
  406. out[0] = element.clientWidth
  407. out[1] = element.clientHeight
  408. return out
  409. }
  410. function eventOffset (out: Vec2, ev: PointerEvent) {
  411. const cx = ev.clientX || 0
  412. const cy = ev.clientY || 0
  413. const rect = element.getBoundingClientRect()
  414. out[0] = cx - rect.left
  415. out[1] = cy - rect.top
  416. return out
  417. }
  418. }
  419. }
  420. export default InputObserver