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 {Interaction} from './useCanvasInteraction';
    
  11. import type {Size} from './geometry';
    
  12. 
    
  13. import memoize from 'memoize-one';
    
  14. 
    
  15. import {View} from './View';
    
  16. import {zeroPoint} from './geometry';
    
  17. import {DPR} from '../content-views/constants';
    
  18. 
    
  19. export type ViewRefs = {
    
  20.   activeView: View | null,
    
  21.   hoveredView: View | null,
    
  22. };
    
  23. 
    
  24. // hidpi canvas: https://www.html5rocks.com/en/tutorials/canvas/hidpi/
    
  25. function configureRetinaCanvas(
    
  26.   canvas: HTMLCanvasElement,
    
  27.   height: number,
    
  28.   width: number,
    
  29. ) {
    
  30.   canvas.width = width * DPR;
    
  31.   canvas.height = height * DPR;
    
  32.   canvas.style.width = `${width}px`;
    
  33.   canvas.style.height = `${height}px`;
    
  34. }
    
  35. 
    
  36. const getCanvasContext = memoize(
    
  37.   (
    
  38.     canvas: HTMLCanvasElement,
    
  39.     height: number,
    
  40.     width: number,
    
  41.     scaleCanvas: boolean = true,
    
  42.   ): CanvasRenderingContext2D => {
    
  43.     const context = canvas.getContext('2d', {alpha: false});
    
  44.     if (scaleCanvas) {
    
  45.       configureRetinaCanvas(canvas, height, width);
    
  46. 
    
  47.       // Scale all drawing operations by the dpr, so you don't have to worry about the difference.
    
  48.       context.scale(DPR, DPR);
    
  49.     }
    
  50.     return context;
    
  51.   },
    
  52. );
    
  53. 
    
  54. type ResetHoveredEventFn = () => void;
    
  55. 
    
  56. /**
    
  57.  * Represents the canvas surface and a view heirarchy. A surface is also the
    
  58.  * place where all interactions enter the view heirarchy.
    
  59.  */
    
  60. export class Surface {
    
  61.   rootView: ?View;
    
  62. 
    
  63.   _context: ?CanvasRenderingContext2D;
    
  64.   _canvasSize: ?Size;
    
  65. 
    
  66.   _resetHoveredEvent: ResetHoveredEventFn;
    
  67. 
    
  68.   _viewRefs: ViewRefs = {
    
  69.     activeView: null,
    
  70.     hoveredView: null,
    
  71.   };
    
  72. 
    
  73.   constructor(resetHoveredEvent: ResetHoveredEventFn) {
    
  74.     this._resetHoveredEvent = resetHoveredEvent;
    
  75.   }
    
  76. 
    
  77.   hasActiveView(): boolean {
    
  78.     return this._viewRefs.activeView !== null;
    
  79.   }
    
  80. 
    
  81.   setCanvas(canvas: HTMLCanvasElement, canvasSize: Size) {
    
  82.     this._context = getCanvasContext(
    
  83.       canvas,
    
  84.       canvasSize.height,
    
  85.       canvasSize.width,
    
  86.     );
    
  87.     this._canvasSize = canvasSize;
    
  88. 
    
  89.     if (this.rootView) {
    
  90.       this.rootView.setNeedsDisplay();
    
  91.     }
    
  92.   }
    
  93. 
    
  94.   displayIfNeeded() {
    
  95.     const {rootView, _canvasSize, _context} = this;
    
  96.     if (!rootView || !_context || !_canvasSize) {
    
  97.       return;
    
  98.     }
    
  99.     rootView.setFrame({
    
  100.       origin: zeroPoint,
    
  101.       size: _canvasSize,
    
  102.     });
    
  103.     rootView.setVisibleArea({
    
  104.       origin: zeroPoint,
    
  105.       size: _canvasSize,
    
  106.     });
    
  107.     rootView.displayIfNeeded(_context, this._viewRefs);
    
  108.   }
    
  109. 
    
  110.   getCurrentCursor(): string | null {
    
  111.     const {activeView, hoveredView} = this._viewRefs;
    
  112.     if (activeView !== null) {
    
  113.       return activeView.currentCursor;
    
  114.     } else if (hoveredView !== null) {
    
  115.       return hoveredView.currentCursor;
    
  116.     } else {
    
  117.       return null;
    
  118.     }
    
  119.   }
    
  120. 
    
  121.   handleInteraction(interaction: Interaction) {
    
  122.     const rootView = this.rootView;
    
  123.     if (rootView != null) {
    
  124.       const viewRefs = this._viewRefs;
    
  125.       switch (interaction.type) {
    
  126.         case 'mousemove':
    
  127.         case 'wheel-control':
    
  128.         case 'wheel-meta':
    
  129.         case 'wheel-plain':
    
  130.         case 'wheel-shift':
    
  131.           // Clean out the hovered view before processing this type of interaction.
    
  132.           const hoveredView = viewRefs.hoveredView;
    
  133.           viewRefs.hoveredView = null;
    
  134. 
    
  135.           rootView.handleInteractionAndPropagateToSubviews(
    
  136.             interaction,
    
  137.             viewRefs,
    
  138.           );
    
  139. 
    
  140.           // If a previously hovered view is no longer hovered, update the outer state.
    
  141.           if (hoveredView !== null && viewRefs.hoveredView === null) {
    
  142.             this._resetHoveredEvent();
    
  143.           }
    
  144.           break;
    
  145.         default:
    
  146.           rootView.handleInteractionAndPropagateToSubviews(
    
  147.             interaction,
    
  148.             viewRefs,
    
  149.           );
    
  150.           break;
    
  151.       }
    
  152.     }
    
  153.   }
    
  154. }