1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  *
    
  7.  * @flow
    
  8.  */
    
  9. 
    
  10. import type {NormalizedWheelDelta} from './utils/normalizeWheel';
    
  11. import type {Point} from './geometry';
    
  12. 
    
  13. import {useEffect, useRef} from 'react';
    
  14. import {normalizeWheel} from './utils/normalizeWheel';
    
  15. 
    
  16. export type ClickInteraction = {
    
  17.   type: 'click',
    
  18.   payload: {
    
  19.     event: MouseEvent,
    
  20.     location: Point,
    
  21.   },
    
  22. };
    
  23. export type DoubleClickInteraction = {
    
  24.   type: 'double-click',
    
  25.   payload: {
    
  26.     event: MouseEvent,
    
  27.     location: Point,
    
  28.   },
    
  29. };
    
  30. export type MouseDownInteraction = {
    
  31.   type: 'mousedown',
    
  32.   payload: {
    
  33.     event: MouseEvent,
    
  34.     location: Point,
    
  35.   },
    
  36. };
    
  37. export type MouseMoveInteraction = {
    
  38.   type: 'mousemove',
    
  39.   payload: {
    
  40.     event: MouseEvent,
    
  41.     location: Point,
    
  42.   },
    
  43. };
    
  44. export type MouseUpInteraction = {
    
  45.   type: 'mouseup',
    
  46.   payload: {
    
  47.     event: MouseEvent,
    
  48.     location: Point,
    
  49.   },
    
  50. };
    
  51. export type WheelPlainInteraction = {
    
  52.   type: 'wheel-plain',
    
  53.   payload: {
    
  54.     event: WheelEvent,
    
  55.     location: Point,
    
  56.     delta: NormalizedWheelDelta,
    
  57.   },
    
  58. };
    
  59. export type WheelWithShiftInteraction = {
    
  60.   type: 'wheel-shift',
    
  61.   payload: {
    
  62.     event: WheelEvent,
    
  63.     location: Point,
    
  64.     delta: NormalizedWheelDelta,
    
  65.   },
    
  66. };
    
  67. export type WheelWithControlInteraction = {
    
  68.   type: 'wheel-control',
    
  69.   payload: {
    
  70.     event: WheelEvent,
    
  71.     location: Point,
    
  72.     delta: NormalizedWheelDelta,
    
  73.   },
    
  74. };
    
  75. export type WheelWithMetaInteraction = {
    
  76.   type: 'wheel-meta',
    
  77.   payload: {
    
  78.     event: WheelEvent,
    
  79.     location: Point,
    
  80.     delta: NormalizedWheelDelta,
    
  81.   },
    
  82. };
    
  83. 
    
  84. export type Interaction =
    
  85.   | ClickInteraction
    
  86.   | DoubleClickInteraction
    
  87.   | MouseDownInteraction
    
  88.   | MouseMoveInteraction
    
  89.   | MouseUpInteraction
    
  90.   | WheelPlainInteraction
    
  91.   | WheelWithShiftInteraction
    
  92.   | WheelWithControlInteraction
    
  93.   | WheelWithMetaInteraction;
    
  94. 
    
  95. let canvasBoundingRectCache = null;
    
  96. function cacheFirstGetCanvasBoundingRect(
    
  97.   canvas: HTMLCanvasElement,
    
  98. ): ClientRect {
    
  99.   if (
    
  100.     canvasBoundingRectCache &&
    
  101.     canvas.width === canvasBoundingRectCache.width &&
    
  102.     canvas.height === canvasBoundingRectCache.height
    
  103.   ) {
    
  104.     return canvasBoundingRectCache.rect;
    
  105.   }
    
  106.   canvasBoundingRectCache = {
    
  107.     width: canvas.width,
    
  108.     height: canvas.height,
    
  109.     rect: canvas.getBoundingClientRect(),
    
  110.   };
    
  111.   return canvasBoundingRectCache.rect;
    
  112. }
    
  113. 
    
  114. export function useCanvasInteraction(
    
  115.   canvasRef: {current: HTMLCanvasElement | null},
    
  116.   interactor: (interaction: Interaction) => void,
    
  117. ) {
    
  118.   const isMouseDownRef = useRef<boolean>(false);
    
  119.   const didMouseMoveWhileDownRef = useRef<boolean>(false);
    
  120. 
    
  121.   useEffect(() => {
    
  122.     const canvas = canvasRef.current;
    
  123.     if (!canvas) {
    
  124.       return;
    
  125.     }
    
  126. 
    
  127.     function localToCanvasCoordinates(localCoordinates: Point): Point {
    
  128.       // $FlowFixMe[incompatible-call] found when upgrading Flow
    
  129.       const canvasRect = cacheFirstGetCanvasBoundingRect(canvas);
    
  130.       return {
    
  131.         x: localCoordinates.x - canvasRect.left,
    
  132.         y: localCoordinates.y - canvasRect.top,
    
  133.       };
    
  134.     }
    
  135. 
    
  136.     const onCanvasClick: MouseEventHandler = event => {
    
  137.       if (didMouseMoveWhileDownRef.current) {
    
  138.         return;
    
  139.       }
    
  140. 
    
  141.       interactor({
    
  142.         type: 'click',
    
  143.         payload: {
    
  144.           event,
    
  145.           location: localToCanvasCoordinates({x: event.x, y: event.y}),
    
  146.         },
    
  147.       });
    
  148.     };
    
  149. 
    
  150.     const onCanvasDoubleClick: MouseEventHandler = event => {
    
  151.       if (didMouseMoveWhileDownRef.current) {
    
  152.         return;
    
  153.       }
    
  154. 
    
  155.       interactor({
    
  156.         type: 'double-click',
    
  157.         payload: {
    
  158.           event,
    
  159.           location: localToCanvasCoordinates({x: event.x, y: event.y}),
    
  160.         },
    
  161.       });
    
  162.     };
    
  163. 
    
  164.     const onCanvasMouseDown: MouseEventHandler = event => {
    
  165.       didMouseMoveWhileDownRef.current = false;
    
  166.       isMouseDownRef.current = true;
    
  167. 
    
  168.       interactor({
    
  169.         type: 'mousedown',
    
  170.         payload: {
    
  171.           event,
    
  172.           location: localToCanvasCoordinates({x: event.x, y: event.y}),
    
  173.         },
    
  174.       });
    
  175.     };
    
  176. 
    
  177.     const onDocumentMouseMove: MouseEventHandler = event => {
    
  178.       if (isMouseDownRef.current) {
    
  179.         didMouseMoveWhileDownRef.current = true;
    
  180.       }
    
  181. 
    
  182.       interactor({
    
  183.         type: 'mousemove',
    
  184.         payload: {
    
  185.           event,
    
  186.           location: localToCanvasCoordinates({x: event.x, y: event.y}),
    
  187.         },
    
  188.       });
    
  189.     };
    
  190. 
    
  191.     const onDocumentMouseUp: MouseEventHandler = event => {
    
  192.       isMouseDownRef.current = false;
    
  193. 
    
  194.       interactor({
    
  195.         type: 'mouseup',
    
  196.         payload: {
    
  197.           event,
    
  198.           location: localToCanvasCoordinates({x: event.x, y: event.y}),
    
  199.         },
    
  200.       });
    
  201.     };
    
  202. 
    
  203.     const onCanvasWheel: WheelEventHandler = event => {
    
  204.       event.preventDefault();
    
  205.       event.stopPropagation();
    
  206. 
    
  207.       const location = localToCanvasCoordinates({x: event.x, y: event.y});
    
  208.       const delta = normalizeWheel(event);
    
  209. 
    
  210.       if (event.shiftKey) {
    
  211.         interactor({
    
  212.           type: 'wheel-shift',
    
  213.           payload: {event, location, delta},
    
  214.         });
    
  215.       } else if (event.ctrlKey) {
    
  216.         interactor({
    
  217.           type: 'wheel-control',
    
  218.           payload: {event, location, delta},
    
  219.         });
    
  220.       } else if (event.metaKey) {
    
  221.         interactor({
    
  222.           type: 'wheel-meta',
    
  223.           payload: {event, location, delta},
    
  224.         });
    
  225.       } else {
    
  226.         interactor({
    
  227.           type: 'wheel-plain',
    
  228.           payload: {event, location, delta},
    
  229.         });
    
  230.       }
    
  231. 
    
  232.       return false;
    
  233.     };
    
  234. 
    
  235.     const ownerDocument = canvas.ownerDocument;
    
  236.     ownerDocument.addEventListener('mousemove', onDocumentMouseMove);
    
  237.     ownerDocument.addEventListener('mouseup', onDocumentMouseUp);
    
  238. 
    
  239.     canvas.addEventListener('click', onCanvasClick);
    
  240.     canvas.addEventListener('dblclick', onCanvasDoubleClick);
    
  241.     canvas.addEventListener('mousedown', onCanvasMouseDown);
    
  242.     canvas.addEventListener('wheel', onCanvasWheel);
    
  243. 
    
  244.     return () => {
    
  245.       ownerDocument.removeEventListener('mousemove', onDocumentMouseMove);
    
  246.       ownerDocument.removeEventListener('mouseup', onDocumentMouseUp);
    
  247. 
    
  248.       canvas.removeEventListener('click', onCanvasClick);
    
  249.       canvas.removeEventListener('dblclick', onCanvasDoubleClick);
    
  250.       canvas.removeEventListener('mousedown', onCanvasMouseDown);
    
  251.       canvas.removeEventListener('wheel', onCanvasWheel);
    
  252.     };
    
  253.   }, [canvasRef, interactor]);
    
  254. }