input-observer.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651
  1. /**
  2. * Copyright (c) 2018-2020 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, EPSILON } 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. }
  23. }
  24. return 0;
  25. }
  26. export function getButton(event: MouseEvent | Touch) {
  27. if (typeof event === 'object') {
  28. if ('button' in event) {
  29. const b = event.button;
  30. if (b === 1) {
  31. return 4;
  32. } else if (b === 2) {
  33. return 2;
  34. } else if (b >= 0) {
  35. return 1 << b;
  36. }
  37. }
  38. }
  39. return 0;
  40. }
  41. export function getModifiers(event: MouseEvent | Touch): ModifiersKeys {
  42. return {
  43. alt: 'altKey' in event ? event.altKey : false,
  44. shift: 'shiftKey' in event ? event.shiftKey : false,
  45. control: 'ctrlKey' in event ? event.ctrlKey : false,
  46. meta: 'metaKey' in event ? event.metaKey : false
  47. };
  48. }
  49. export const DefaultInputObserverProps = {
  50. noScroll: true,
  51. noMiddleClickScroll: true,
  52. noContextMenu: true,
  53. noPinchZoom: true,
  54. noTextSelect: true,
  55. mask: (x: number, y: number) => true,
  56. pixelScale: 1
  57. };
  58. export type InputObserverProps = Partial<typeof DefaultInputObserverProps>
  59. export type ModifiersKeys = {
  60. shift: boolean,
  61. alt: boolean,
  62. control: boolean,
  63. meta: boolean
  64. }
  65. export namespace ModifiersKeys {
  66. export const None = create();
  67. export function areEqual(a: ModifiersKeys, b: ModifiersKeys) {
  68. return a.shift === b.shift && a.alt === b.alt && a.control === b.control && a.meta === b.meta;
  69. }
  70. export function size(a?: ModifiersKeys) {
  71. if (!a) return 0;
  72. let ret = 0;
  73. if (!!a.shift) ret++;
  74. if (!!a.alt) ret++;
  75. if (!!a.control) ret++;
  76. if (!!a.meta) ret++;
  77. return ret;
  78. }
  79. export function create(modifierKeys: Partial<ModifiersKeys> = {}): ModifiersKeys {
  80. return {
  81. shift: !!modifierKeys.shift,
  82. alt: !!modifierKeys.alt,
  83. control: !!modifierKeys.control,
  84. meta: !!modifierKeys.meta
  85. };
  86. }
  87. }
  88. export type ButtonsType = BitFlags<ButtonsType.Flag>
  89. export namespace ButtonsType {
  90. export const has: (btn: ButtonsType, f: Flag) => boolean = BitFlags.has;
  91. export const create: (fs: Flag) => ButtonsType = BitFlags.create;
  92. export const enum Flag {
  93. /** No button or un-initialized */
  94. None = 0x0,
  95. /** Primary button (usually left) */
  96. Primary = 0x1,
  97. /** Secondary button (usually right) */
  98. Secondary = 0x2,
  99. /** Auxilary button (usually middle or mouse wheel button) */
  100. Auxilary = 0x4,
  101. /** 4th button (typically the "Browser Back" button) */
  102. Forth = 0x8,
  103. /** 5th button (typically the "Browser Forward" button) */
  104. Five = 0x10,
  105. }
  106. }
  107. type BaseInput = {
  108. buttons: ButtonsType
  109. button: ButtonsType.Flag
  110. modifiers: ModifiersKeys
  111. }
  112. export type DragInput = {
  113. x: number,
  114. y: number,
  115. dx: number,
  116. dy: number,
  117. pageX: number,
  118. pageY: number,
  119. isStart: boolean
  120. } & BaseInput
  121. export type WheelInput = {
  122. x: number,
  123. y: number,
  124. pageX: number,
  125. pageY: number,
  126. dx: number,
  127. dy: number,
  128. dz: number,
  129. } & BaseInput
  130. export type ClickInput = {
  131. x: number,
  132. y: number,
  133. pageX: number,
  134. pageY: number,
  135. } & BaseInput
  136. export type MoveInput = {
  137. x: number,
  138. y: number,
  139. pageX: number,
  140. pageY: number,
  141. inside: boolean,
  142. } & BaseInput
  143. export type PinchInput = {
  144. delta: number,
  145. fraction: number,
  146. distance: number,
  147. isStart: boolean
  148. } & BaseInput
  149. export type KeyInput = {
  150. key: string,
  151. modifiers: ModifiersKeys
  152. }
  153. export type ResizeInput = {
  154. }
  155. const enum DraggingState {
  156. Stopped = 0,
  157. Started = 1,
  158. Moving = 2
  159. }
  160. type PointerEvent = {
  161. clientX: number
  162. clientY: number
  163. pageX: number
  164. pageY: number
  165. preventDefault?: () => void
  166. }
  167. interface InputObserver {
  168. noScroll: boolean
  169. noContextMenu: boolean
  170. readonly width: number
  171. readonly height: number
  172. readonly pixelRatio: number
  173. readonly drag: Observable<DragInput>,
  174. // Equivalent to mouseUp and touchEnd
  175. readonly interactionEnd: Observable<undefined>,
  176. readonly wheel: Observable<WheelInput>,
  177. readonly pinch: Observable<PinchInput>,
  178. readonly click: Observable<ClickInput>,
  179. readonly move: Observable<MoveInput>,
  180. readonly leave: Observable<undefined>,
  181. readonly enter: Observable<undefined>,
  182. readonly resize: Observable<ResizeInput>,
  183. readonly modifiers: Observable<ModifiersKeys>
  184. readonly key: Observable<KeyInput>
  185. dispose: () => void
  186. }
  187. function createEvents() {
  188. return {
  189. drag: new Subject<DragInput>(),
  190. interactionEnd: new Subject<undefined>(),
  191. click: new Subject<ClickInput>(),
  192. move: new Subject<MoveInput>(),
  193. wheel: new Subject<WheelInput>(),
  194. pinch: new Subject<PinchInput>(),
  195. resize: new Subject<ResizeInput>(),
  196. leave: new Subject<undefined>(),
  197. enter: new Subject<undefined>(),
  198. modifiers: new Subject<ModifiersKeys>(),
  199. key: new Subject<KeyInput>(),
  200. };
  201. }
  202. const AllowedNonPrintableKeys = ['Backspace', 'Delete'];
  203. namespace InputObserver {
  204. export function create(props: InputObserverProps = {}): InputObserver {
  205. const { noScroll, noContextMenu } = { ...DefaultInputObserverProps, ...props };
  206. return {
  207. noScroll,
  208. noContextMenu,
  209. width: 0,
  210. height: 0,
  211. pixelRatio: 1,
  212. ...createEvents(),
  213. dispose: noop
  214. };
  215. }
  216. export function fromElement(element: Element, props: InputObserverProps = {}): InputObserver {
  217. let { noScroll, noMiddleClickScroll, noContextMenu, noPinchZoom, noTextSelect, mask, pixelScale } = { ...DefaultInputObserverProps, ...props };
  218. let width = element.clientWidth * pixelRatio();
  219. let height = element.clientHeight * pixelRatio();
  220. let lastTouchDistance = 0;
  221. const pointerDown = Vec2();
  222. const pointerStart = Vec2();
  223. const pointerEnd = Vec2();
  224. const pointerDelta = Vec2();
  225. const rectSize = Vec2();
  226. const modifierKeys: ModifiersKeys = {
  227. shift: false,
  228. alt: false,
  229. control: false,
  230. meta: false
  231. };
  232. function pixelRatio() {
  233. return window.devicePixelRatio * pixelScale;
  234. }
  235. function getModifierKeys(): ModifiersKeys {
  236. return { ...modifierKeys };
  237. }
  238. let dragging: DraggingState = DraggingState.Stopped;
  239. let disposed = false;
  240. let buttons = ButtonsType.create(ButtonsType.Flag.None);
  241. let button = ButtonsType.Flag.None;
  242. let isInside = false;
  243. const events = createEvents();
  244. const { drag, interactionEnd, wheel, pinch, click, move, leave, enter, resize, modifiers, key } = events;
  245. attach();
  246. return {
  247. get noScroll () { return noScroll; },
  248. set noScroll (value: boolean) { noScroll = value; },
  249. get noContextMenu () { return noContextMenu; },
  250. set noContextMenu (value: boolean) { noContextMenu = value; },
  251. get width () { return width; },
  252. get height () { return height; },
  253. get pixelRatio () { return pixelRatio(); },
  254. ...events,
  255. dispose
  256. };
  257. function attach() {
  258. element.addEventListener('contextmenu', onContextMenu as any, false );
  259. element.addEventListener('wheel', onMouseWheel as any, false);
  260. element.addEventListener('mousedown', onMouseDown as any, false);
  261. // for dragging to work outside canvas bounds,
  262. // mouse move/up events have to be added to a parent, i.e. window
  263. window.addEventListener('mousemove', onMouseMove as any, false);
  264. window.addEventListener('mouseup', onMouseUp as any, false);
  265. element.addEventListener('mouseenter', onMouseEnter as any, false);
  266. element.addEventListener('mouseleave', onMouseLeave as any, false);
  267. element.addEventListener('touchstart', onTouchStart as any, false);
  268. element.addEventListener('touchmove', onTouchMove as any, false);
  269. element.addEventListener('touchend', onTouchEnd as any, false);
  270. // reset buttons and modifier keys state when browser window looses focus
  271. window.addEventListener('blur', handleBlur);
  272. window.addEventListener('keyup', handleKeyUp as EventListener, false);
  273. window.addEventListener('keydown', handleKeyDown as EventListener, false);
  274. window.addEventListener('keypress', handleKeyPress as EventListener, false);
  275. window.addEventListener('resize', onResize, false);
  276. }
  277. function dispose() {
  278. if (disposed) return;
  279. disposed = true;
  280. element.removeEventListener( 'contextmenu', onContextMenu as any, false );
  281. element.removeEventListener('wheel', onMouseWheel as any, false);
  282. element.removeEventListener('mousedown', onMouseDown as any, false);
  283. window.removeEventListener('mousemove', onMouseMove as any, false);
  284. window.removeEventListener('mouseup', onMouseUp as any, false);
  285. element.removeEventListener('mouseenter', onMouseEnter as any, false);
  286. element.removeEventListener('mouseleave', onMouseLeave as any, false);
  287. element.removeEventListener('touchstart', onTouchStart as any, false);
  288. element.removeEventListener('touchmove', onTouchMove as any, false);
  289. element.removeEventListener('touchend', onTouchEnd as any, false);
  290. window.removeEventListener('blur', handleBlur);
  291. window.removeEventListener('keyup', handleKeyUp as EventListener, false);
  292. window.removeEventListener('keydown', handleKeyDown as EventListener, false);
  293. window.removeEventListener('keypress', handleKeyPress as EventListener, false);
  294. window.removeEventListener('resize', onResize, false);
  295. }
  296. function onContextMenu(event: MouseEvent) {
  297. if (!mask(event.clientX, event.clientY)) return;
  298. if (noContextMenu) {
  299. event.preventDefault();
  300. }
  301. }
  302. function updateModifierKeys(event: MouseEvent | WheelEvent | TouchEvent) {
  303. modifierKeys.alt = event.altKey;
  304. modifierKeys.shift = event.shiftKey;
  305. modifierKeys.control = event.ctrlKey;
  306. modifierKeys.meta = event.metaKey;
  307. }
  308. function handleBlur() {
  309. if (buttons || modifierKeys.shift || modifierKeys.alt || modifierKeys.meta || modifierKeys.control) {
  310. buttons = 0 as ButtonsType;
  311. modifierKeys.shift = modifierKeys.alt = modifierKeys.control = modifierKeys.meta = false;
  312. }
  313. }
  314. function handleKeyDown(event: KeyboardEvent) {
  315. let changed = false;
  316. if (!modifierKeys.alt && event.altKey) { changed = true; modifierKeys.alt = true; }
  317. if (!modifierKeys.shift && event.shiftKey) { changed = true; modifierKeys.shift = true; }
  318. if (!modifierKeys.control && event.ctrlKey) { changed = true; modifierKeys.control = true; }
  319. if (!modifierKeys.meta && event.metaKey) { changed = true; modifierKeys.meta = true; }
  320. if (changed && isInside) modifiers.next(getModifierKeys());
  321. }
  322. function handleKeyUp(event: KeyboardEvent) {
  323. let changed = false;
  324. if (modifierKeys.alt && !event.altKey) { changed = true; modifierKeys.alt = false; }
  325. if (modifierKeys.shift && !event.shiftKey) { changed = true; modifierKeys.shift = false; }
  326. if (modifierKeys.control && !event.ctrlKey) { changed = true; modifierKeys.control = false; }
  327. if (modifierKeys.meta && !event.metaKey) { changed = true; modifierKeys.meta = false; }
  328. if (changed && isInside) modifiers.next(getModifierKeys());
  329. if (AllowedNonPrintableKeys.includes(event.key)) handleKeyPress(event);
  330. }
  331. function handleKeyPress(event: KeyboardEvent) {
  332. key.next({
  333. key: event.key,
  334. modifiers: getModifierKeys()
  335. });
  336. }
  337. function getCenterTouch(ev: TouchEvent): PointerEvent {
  338. const t0 = ev.touches[0];
  339. const t1 = ev.touches[1];
  340. return {
  341. clientX: (t0.clientX + t1.clientX) / 2,
  342. clientY: (t0.clientY + t1.clientY) / 2,
  343. pageX: (t0.pageX + t1.pageX) / 2,
  344. pageY: (t0.pageY + t1.pageY) / 2
  345. };
  346. }
  347. function getTouchDistance(ev: TouchEvent) {
  348. const dx = ev.touches[0].pageX - ev.touches[1].pageX;
  349. const dy = ev.touches[0].pageY - ev.touches[1].pageY;
  350. return Math.sqrt(dx * dx + dy * dy);
  351. }
  352. function onTouchStart(ev: TouchEvent) {
  353. if (ev.touches.length === 1) {
  354. buttons = button = ButtonsType.Flag.Primary;
  355. onPointerDown(ev.touches[0]);
  356. } else if (ev.touches.length === 2) {
  357. buttons = ButtonsType.Flag.Secondary & ButtonsType.Flag.Auxilary;
  358. button = ButtonsType.Flag.Secondary;
  359. onPointerDown(getCenterTouch(ev));
  360. const touchDistance = getTouchDistance(ev);
  361. lastTouchDistance = touchDistance;
  362. pinch.next({
  363. distance: touchDistance,
  364. fraction: 1,
  365. delta: 0,
  366. isStart: true,
  367. buttons,
  368. button,
  369. modifiers: getModifierKeys()
  370. });
  371. } else if (ev.touches.length === 3) {
  372. buttons = button = ButtonsType.Flag.Forth;
  373. onPointerDown(getCenterTouch(ev));
  374. }
  375. }
  376. function onTouchEnd(ev: TouchEvent) {
  377. endDrag();
  378. }
  379. function onTouchMove(ev: TouchEvent) {
  380. button = ButtonsType.Flag.None;
  381. if (noPinchZoom) {
  382. ev.preventDefault();
  383. ev.stopPropagation();
  384. if ((ev as any).originalEvent) {
  385. (ev as any).originalEvent.preventDefault();
  386. (ev as any).originalEvent.stopPropagation();
  387. }
  388. }
  389. if (ev.touches.length === 1) {
  390. buttons = ButtonsType.Flag.Primary;
  391. onPointerMove(ev.touches[0]);
  392. } else if (ev.touches.length === 2) {
  393. const touchDistance = getTouchDistance(ev);
  394. const touchDelta = lastTouchDistance - touchDistance;
  395. if (Math.abs(touchDelta) < 4) {
  396. buttons = ButtonsType.Flag.Secondary;
  397. onPointerMove(getCenterTouch(ev));
  398. } else {
  399. buttons = ButtonsType.Flag.Auxilary;
  400. updateModifierKeys(ev);
  401. pinch.next({
  402. delta: touchDelta,
  403. fraction: lastTouchDistance / touchDistance,
  404. distance: touchDistance,
  405. isStart: false,
  406. buttons,
  407. button,
  408. modifiers: getModifierKeys()
  409. });
  410. }
  411. lastTouchDistance = touchDistance;
  412. } else if (ev.touches.length === 3) {
  413. buttons = ButtonsType.Flag.Forth;
  414. onPointerMove(getCenterTouch(ev));
  415. }
  416. }
  417. function onMouseDown(ev: MouseEvent) {
  418. updateModifierKeys(ev);
  419. buttons = getButtons(ev);
  420. button = getButton(ev);
  421. if (noMiddleClickScroll && buttons === ButtonsType.Flag.Auxilary) {
  422. ev.preventDefault;
  423. }
  424. onPointerDown(ev);
  425. }
  426. function onMouseMove(ev: MouseEvent) {
  427. updateModifierKeys(ev);
  428. buttons = getButtons(ev);
  429. button = ButtonsType.Flag.None;
  430. onPointerMove(ev);
  431. }
  432. function onMouseUp(ev: MouseEvent) {
  433. updateModifierKeys(ev);
  434. buttons = getButtons(ev);
  435. button = getButton(ev);
  436. onPointerUp(ev);
  437. endDrag();
  438. }
  439. function endDrag() {
  440. interactionEnd.next();
  441. }
  442. function onPointerDown(ev: PointerEvent) {
  443. if (!mask(ev.clientX, ev.clientY)) return;
  444. eventOffset(pointerStart, ev);
  445. Vec2.copy(pointerDown, pointerStart);
  446. if (insideBounds(pointerStart)) {
  447. dragging = DraggingState.Started;
  448. }
  449. }
  450. function onPointerUp(ev: PointerEvent) {
  451. dragging = DraggingState.Stopped;
  452. if (!mask(ev.clientX, ev.clientY)) return;
  453. eventOffset(pointerEnd, ev);
  454. if (Vec2.distance(pointerEnd, pointerDown) < 4) {
  455. const { pageX, pageY } = ev;
  456. const [ x, y ] = pointerEnd;
  457. click.next({ x, y, pageX, pageY, buttons, button, modifiers: getModifierKeys() });
  458. }
  459. }
  460. function onPointerMove(ev: PointerEvent) {
  461. eventOffset(pointerEnd, ev);
  462. const { pageX, pageY } = ev;
  463. const [ x, y ] = pointerEnd;
  464. const inside = insideBounds(pointerEnd);
  465. move.next({ x, y, pageX, pageY, buttons, button, modifiers: getModifierKeys(), inside });
  466. if (dragging === DraggingState.Stopped) return;
  467. if (noTextSelect) {
  468. ev.preventDefault?.();
  469. }
  470. Vec2.div(pointerDelta, Vec2.sub(pointerDelta, pointerEnd, pointerStart), getClientSize(rectSize));
  471. if (Vec2.magnitude(pointerDelta) < EPSILON) return;
  472. const isStart = dragging === DraggingState.Started;
  473. if (isStart && !mask(ev.clientX, ev.clientY)) return;
  474. const [ dx, dy ] = pointerDelta;
  475. drag.next({ x, y, dx, dy, pageX, pageY, buttons, button, modifiers: getModifierKeys(), isStart });
  476. Vec2.copy(pointerStart, pointerEnd);
  477. dragging = DraggingState.Moving;
  478. }
  479. function onMouseWheel(ev: WheelEvent) {
  480. if (!mask(ev.clientX, ev.clientY)) return;
  481. eventOffset(pointerEnd, ev);
  482. const { pageX, pageY } = ev;
  483. const [ x, y ] = pointerEnd;
  484. if (noScroll) {
  485. ev.preventDefault();
  486. }
  487. let scale = 1;
  488. switch (ev.deltaMode) {
  489. case 0: scale = 1; break; // pixels
  490. case 1: scale = 40; break; // lines
  491. case 2: scale = 800; break; // pages
  492. }
  493. const dx = (ev.deltaX || 0) * scale;
  494. const dy = (ev.deltaY || 0) * scale;
  495. const dz = (ev.deltaZ || 0) * scale;
  496. buttons = button = ButtonsType.Flag.Auxilary;
  497. if (dx || dy || dz) {
  498. wheel.next({ x, y, pageX, pageY, dx, dy, dz, buttons, button, modifiers: getModifierKeys() });
  499. }
  500. }
  501. function onMouseEnter(ev: Event) {
  502. isInside = true;
  503. enter.next();
  504. }
  505. function onMouseLeave(ev: Event) {
  506. isInside = false;
  507. leave.next();
  508. }
  509. function onResize(ev: Event) {
  510. resize.next();
  511. }
  512. function insideBounds(pos: Vec2) {
  513. if (element instanceof Window || element instanceof Document || element === document.body) {
  514. return true;
  515. } else {
  516. const rect = element.getBoundingClientRect();
  517. return pos[0] >= 0 && pos[1] >= 0 && pos[0] < rect.width && pos[1] < rect.height;
  518. }
  519. }
  520. function getClientSize(out: Vec2) {
  521. out[0] = element.clientWidth;
  522. out[1] = element.clientHeight;
  523. return out;
  524. }
  525. function eventOffset(out: Vec2, ev: PointerEvent) {
  526. width = element.clientWidth * pixelRatio();
  527. height = element.clientHeight * pixelRatio();
  528. const cx = ev.clientX || 0;
  529. const cy = ev.clientY || 0;
  530. const rect = element.getBoundingClientRect();
  531. out[0] = cx - rect.left;
  532. out[1] = cy - rect.top;
  533. return out;
  534. }
  535. }
  536. }
  537. export default InputObserver;