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 {IntrinsicSize, Rect, Size} from './geometry';
    
  12. import type {Layouter} from './layouter';
    
  13. import type {ViewRefs} from './Surface';
    
  14. 
    
  15. import {Surface} from './Surface';
    
  16. import {
    
  17.   rectEqualToRect,
    
  18.   intersectionOfRects,
    
  19.   rectIntersectsRect,
    
  20.   sizeIsEmpty,
    
  21.   sizeIsValid,
    
  22.   unionOfRects,
    
  23.   zeroRect,
    
  24. } from './geometry';
    
  25. import {noopLayout, viewsToLayout, collapseLayoutIntoViews} from './layouter';
    
  26. 
    
  27. /**
    
  28.  * Base view class that can be subclassed to draw custom content or manage
    
  29.  * subclasses.
    
  30.  */
    
  31. export class View {
    
  32.   _backgroundColor: string | null;
    
  33. 
    
  34.   currentCursor: string | null = null;
    
  35. 
    
  36.   surface: Surface;
    
  37. 
    
  38.   frame: Rect;
    
  39.   visibleArea: Rect;
    
  40. 
    
  41.   superview: ?View;
    
  42.   subviews: View[] = [];
    
  43. 
    
  44.   /**
    
  45.    * An injected function that lays out our subviews.
    
  46.    * @private
    
  47.    */
    
  48.   _layouter: Layouter;
    
  49. 
    
  50.   /**
    
  51.    * Whether this view needs to be drawn.
    
  52.    *
    
  53.    * NOTE: Do not set directly! Use `setNeedsDisplay`.
    
  54.    *
    
  55.    * @see setNeedsDisplay
    
  56.    * @private
    
  57.    */
    
  58.   _needsDisplay: boolean = true;
    
  59. 
    
  60.   /**
    
  61.    * Whether the hierarchy below this view has subviews that need display.
    
  62.    *
    
  63.    * NOTE: Do not set directly! Use `setSubviewsNeedDisplay`.
    
  64.    *
    
  65.    * @see setSubviewsNeedDisplay
    
  66.    * @private
    
  67.    */
    
  68.   _subviewsNeedDisplay: boolean = false;
    
  69. 
    
  70.   constructor(
    
  71.     surface: Surface,
    
  72.     frame: Rect,
    
  73.     layouter: Layouter = noopLayout,
    
  74.     visibleArea: Rect = frame,
    
  75.     backgroundColor?: string | null = null,
    
  76.   ) {
    
  77.     this._backgroundColor = backgroundColor || null;
    
  78.     this.surface = surface;
    
  79.     this.frame = frame;
    
  80.     this._layouter = layouter;
    
  81.     this.visibleArea = visibleArea;
    
  82.   }
    
  83. 
    
  84.   /**
    
  85.    * Invalidates view's contents.
    
  86.    *
    
  87.    * Downward propagating; once called, all subviews of this view should also
    
  88.    * be invalidated.
    
  89.    */
    
  90.   setNeedsDisplay() {
    
  91.     this._needsDisplay = true;
    
  92.     if (this.superview) {
    
  93.       this.superview._setSubviewsNeedDisplay();
    
  94.     }
    
  95.     this.subviews.forEach(subview => subview.setNeedsDisplay());
    
  96.   }
    
  97. 
    
  98.   /**
    
  99.    * Informs superview that it has subviews that need to be drawn.
    
  100.    *
    
  101.    * Upward propagating; once called, all superviews of this view should also
    
  102.    * have `subviewsNeedDisplay` = true.
    
  103.    *
    
  104.    * @private
    
  105.    */
    
  106.   _setSubviewsNeedDisplay() {
    
  107.     this._subviewsNeedDisplay = true;
    
  108.     if (this.superview) {
    
  109.       this.superview._setSubviewsNeedDisplay();
    
  110.     }
    
  111.   }
    
  112. 
    
  113.   setFrame(newFrame: Rect) {
    
  114.     if (!rectEqualToRect(this.frame, newFrame)) {
    
  115.       this.frame = newFrame;
    
  116.       if (sizeIsValid(newFrame.size)) {
    
  117.         this.frame = newFrame;
    
  118.       } else {
    
  119.         this.frame = zeroRect;
    
  120.       }
    
  121.       this.setNeedsDisplay();
    
  122.     }
    
  123.   }
    
  124. 
    
  125.   setVisibleArea(newVisibleArea: Rect) {
    
  126.     if (!rectEqualToRect(this.visibleArea, newVisibleArea)) {
    
  127.       if (sizeIsValid(newVisibleArea.size)) {
    
  128.         this.visibleArea = newVisibleArea;
    
  129.       } else {
    
  130.         this.visibleArea = zeroRect;
    
  131.       }
    
  132.       this.setNeedsDisplay();
    
  133.     }
    
  134.   }
    
  135. 
    
  136.   /**
    
  137.    * A size that can be used as a hint by layout functions.
    
  138.    *
    
  139.    * Implementations should typically return the intrinsic content size or a
    
  140.    * size that fits all the view's content.
    
  141.    *
    
  142.    * The default implementation returns a size that fits all the view's
    
  143.    * subviews.
    
  144.    *
    
  145.    * Can be overridden by subclasses.
    
  146.    */
    
  147.   desiredSize(): Size | IntrinsicSize {
    
  148.     if (this._needsDisplay) {
    
  149.       this.layoutSubviews();
    
  150.     }
    
  151.     const frames = this.subviews.map(subview => subview.frame);
    
  152.     return unionOfRects(...frames).size;
    
  153.   }
    
  154. 
    
  155.   /**
    
  156.    * Appends `view` to the list of this view's `subviews`.
    
  157.    */
    
  158.   addSubview(view: View) {
    
  159.     if (this.subviews.includes(view)) {
    
  160.       return;
    
  161.     }
    
  162.     this.subviews.push(view);
    
  163.     view.superview = this;
    
  164.   }
    
  165. 
    
  166.   /**
    
  167.    * Breaks the subview-superview relationship between `view` and this view, if
    
  168.    * `view` is a subview of this view.
    
  169.    */
    
  170.   removeSubview(view: View) {
    
  171.     const subviewIndex = this.subviews.indexOf(view);
    
  172.     if (subviewIndex === -1) {
    
  173.       return;
    
  174.     }
    
  175.     view.superview = undefined;
    
  176.     this.subviews.splice(subviewIndex, 1);
    
  177.   }
    
  178. 
    
  179.   /**
    
  180.    * Removes all subviews from this view.
    
  181.    */
    
  182.   removeAllSubviews() {
    
  183.     this.subviews.forEach(subview => (subview.superview = undefined));
    
  184.     this.subviews = [];
    
  185.   }
    
  186. 
    
  187.   /**
    
  188.    * Executes the display flow if this view needs to be drawn.
    
  189.    *
    
  190.    * 1. Lays out subviews with `layoutSubviews`.
    
  191.    * 2. Draws content with `draw`.
    
  192.    */
    
  193.   displayIfNeeded(context: CanvasRenderingContext2D, viewRefs: ViewRefs) {
    
  194.     if (
    
  195.       (this._needsDisplay || this._subviewsNeedDisplay) &&
    
  196.       rectIntersectsRect(this.frame, this.visibleArea) &&
    
  197.       !sizeIsEmpty(this.visibleArea.size)
    
  198.     ) {
    
  199.       this.layoutSubviews();
    
  200.       if (this._needsDisplay) {
    
  201.         this._needsDisplay = false;
    
  202.       }
    
  203.       if (this._subviewsNeedDisplay) this._subviewsNeedDisplay = false;
    
  204. 
    
  205.       // Clip anything drawn by the view to prevent it from overflowing its visible area.
    
  206.       const visibleArea = this.visibleArea;
    
  207.       const region = new Path2D();
    
  208.       region.rect(
    
  209.         visibleArea.origin.x,
    
  210.         visibleArea.origin.y,
    
  211.         visibleArea.size.width,
    
  212.         visibleArea.size.height,
    
  213.       );
    
  214.       context.save();
    
  215.       context.clip(region);
    
  216.       context.beginPath();
    
  217. 
    
  218.       this.draw(context, viewRefs);
    
  219. 
    
  220.       // Stop clipping
    
  221.       context.restore();
    
  222.     }
    
  223.   }
    
  224. 
    
  225.   /**
    
  226.    * Layout self and subviews.
    
  227.    *
    
  228.    * Implementations should call `setNeedsDisplay` if a draw is required.
    
  229.    *
    
  230.    * The default implementation uses `this.layouter` to lay out subviews.
    
  231.    *
    
  232.    * Can be overwritten by subclasses that wish to manually manage their
    
  233.    * subviews' layout.
    
  234.    *
    
  235.    * NOTE: Do not call directly! Use `displayIfNeeded`.
    
  236.    *
    
  237.    * @see displayIfNeeded
    
  238.    */
    
  239.   layoutSubviews() {
    
  240.     const {frame, _layouter, subviews, visibleArea} = this;
    
  241.     const existingLayout = viewsToLayout(subviews);
    
  242.     const newLayout = _layouter(existingLayout, frame);
    
  243.     collapseLayoutIntoViews(newLayout);
    
  244. 
    
  245.     subviews.forEach((subview, subviewIndex) => {
    
  246.       if (rectIntersectsRect(visibleArea, subview.frame)) {
    
  247.         subview.setVisibleArea(intersectionOfRects(visibleArea, subview.frame));
    
  248.       } else {
    
  249.         subview.setVisibleArea(zeroRect);
    
  250.       }
    
  251.     });
    
  252.   }
    
  253. 
    
  254.   /**
    
  255.    * Draw the contents of this view in the given canvas `context`.
    
  256.    *
    
  257.    * Defaults to drawing this view's `subviews`.
    
  258.    *
    
  259.    * To be overwritten by subclasses that wish to draw custom content.
    
  260.    *
    
  261.    * NOTE: Do not call directly! Use `displayIfNeeded`.
    
  262.    *
    
  263.    * @see displayIfNeeded
    
  264.    */
    
  265.   draw(context: CanvasRenderingContext2D, viewRefs: ViewRefs) {
    
  266.     const {subviews, visibleArea} = this;
    
  267.     subviews.forEach(subview => {
    
  268.       if (rectIntersectsRect(visibleArea, subview.visibleArea)) {
    
  269.         subview.displayIfNeeded(context, viewRefs);
    
  270.       }
    
  271.     });
    
  272. 
    
  273.     const backgroundColor = this._backgroundColor;
    
  274.     if (backgroundColor !== null) {
    
  275.       const desiredSize = this.desiredSize();
    
  276.       if (visibleArea.size.height > desiredSize.height) {
    
  277.         context.fillStyle = backgroundColor;
    
  278.         context.fillRect(
    
  279.           visibleArea.origin.x,
    
  280.           visibleArea.origin.y + desiredSize.height,
    
  281.           visibleArea.size.width,
    
  282.           visibleArea.size.height - desiredSize.height,
    
  283.         );
    
  284.       }
    
  285.     }
    
  286.   }
    
  287. 
    
  288.   /**
    
  289.    * Handle an `interaction`.
    
  290.    *
    
  291.    * To be overwritten by subclasses that wish to handle interactions.
    
  292.    *
    
  293.    * NOTE: Do not call directly! Use `handleInteractionAndPropagateToSubviews`
    
  294.    */
    
  295.   handleInteraction(interaction: Interaction, viewRefs: ViewRefs): ?boolean {}
    
  296. 
    
  297.   /**
    
  298.    * Handle an `interaction` and propagates it to all of this view's
    
  299.    * `subviews`.
    
  300.    *
    
  301.    * NOTE: Should not be overridden! Subclasses should override
    
  302.    * `handleInteraction` instead.
    
  303.    *
    
  304.    * @see handleInteraction
    
  305.    * @protected
    
  306.    */
    
  307.   handleInteractionAndPropagateToSubviews(
    
  308.     interaction: Interaction,
    
  309.     viewRefs: ViewRefs,
    
  310.   ): boolean {
    
  311.     const {subviews, visibleArea} = this;
    
  312. 
    
  313.     if (visibleArea.size.height === 0) {
    
  314.       return false;
    
  315.     }
    
  316. 
    
  317.     // Pass the interaction to subviews first,
    
  318.     // so they have the opportunity to claim it before it bubbles.
    
  319.     //
    
  320.     // Views are painted first to last,
    
  321.     // so they should process interactions last to first,
    
  322.     // so views in front (on top) can claim the interaction first.
    
  323.     for (let i = subviews.length - 1; i >= 0; i--) {
    
  324.       const subview = subviews[i];
    
  325.       if (rectIntersectsRect(visibleArea, subview.visibleArea)) {
    
  326.         const didSubviewHandle =
    
  327.           subview.handleInteractionAndPropagateToSubviews(
    
  328.             interaction,
    
  329.             viewRefs,
    
  330.           ) === true;
    
  331.         if (didSubviewHandle) {
    
  332.           return true;
    
  333.         }
    
  334.       }
    
  335.     }
    
  336. 
    
  337.     const didSelfHandle =
    
  338.       this.handleInteraction(interaction, viewRefs) === true;
    
  339.     if (didSelfHandle) {
    
  340.       return true;
    
  341.     }
    
  342. 
    
  343.     return false;
    
  344.   }
    
  345. }