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 {UserTimingMark} from '../types';
    
  11. import type {
    
  12.   Interaction,
    
  13.   MouseMoveInteraction,
    
  14.   Rect,
    
  15.   Size,
    
  16.   ViewRefs,
    
  17. } from '../view-base';
    
  18. 
    
  19. import {
    
  20.   positioningScaleFactor,
    
  21.   timestampToPosition,
    
  22.   positionToTimestamp,
    
  23.   widthToDuration,
    
  24. } from './utils/positioning';
    
  25. import {
    
  26.   View,
    
  27.   Surface,
    
  28.   rectContainsPoint,
    
  29.   rectIntersectsRect,
    
  30.   intersectionOfRects,
    
  31. } from '../view-base';
    
  32. import {
    
  33.   COLORS,
    
  34.   TOP_ROW_PADDING,
    
  35.   USER_TIMING_MARK_SIZE,
    
  36.   BORDER_SIZE,
    
  37. } from './constants';
    
  38. 
    
  39. const ROW_HEIGHT_FIXED =
    
  40.   TOP_ROW_PADDING + USER_TIMING_MARK_SIZE + TOP_ROW_PADDING;
    
  41. 
    
  42. export class UserTimingMarksView extends View {
    
  43.   _marks: UserTimingMark[];
    
  44.   _intrinsicSize: Size;
    
  45. 
    
  46.   _hoveredMark: UserTimingMark | null = null;
    
  47.   onHover: ((mark: UserTimingMark | null) => void) | null = null;
    
  48. 
    
  49.   constructor(
    
  50.     surface: Surface,
    
  51.     frame: Rect,
    
  52.     marks: UserTimingMark[],
    
  53.     duration: number,
    
  54.   ) {
    
  55.     super(surface, frame);
    
  56.     this._marks = marks;
    
  57. 
    
  58.     this._intrinsicSize = {
    
  59.       width: duration,
    
  60.       height: ROW_HEIGHT_FIXED,
    
  61.     };
    
  62.   }
    
  63. 
    
  64.   desiredSize(): Size {
    
  65.     return this._intrinsicSize;
    
  66.   }
    
  67. 
    
  68.   setHoveredMark(hoveredMark: UserTimingMark | null) {
    
  69.     if (this._hoveredMark === hoveredMark) {
    
  70.       return;
    
  71.     }
    
  72.     this._hoveredMark = hoveredMark;
    
  73.     this.setNeedsDisplay();
    
  74.   }
    
  75. 
    
  76.   /**
    
  77.    * Draw a single `UserTimingMark` as a circle in the canvas.
    
  78.    */
    
  79.   _drawSingleMark(
    
  80.     context: CanvasRenderingContext2D,
    
  81.     rect: Rect,
    
  82.     mark: UserTimingMark,
    
  83.     baseY: number,
    
  84.     scaleFactor: number,
    
  85.     showHoverHighlight: boolean,
    
  86.   ) {
    
  87.     const {frame} = this;
    
  88.     const {timestamp} = mark;
    
  89. 
    
  90.     const x = timestampToPosition(timestamp, scaleFactor, frame);
    
  91.     const size = USER_TIMING_MARK_SIZE;
    
  92.     const halfSize = size / 2;
    
  93. 
    
  94.     const markRect: Rect = {
    
  95.       origin: {
    
  96.         x: x - halfSize,
    
  97.         y: baseY,
    
  98.       },
    
  99.       size: {width: size, height: size},
    
  100.     };
    
  101.     if (!rectIntersectsRect(markRect, rect)) {
    
  102.       return; // Not in view
    
  103.     }
    
  104. 
    
  105.     const fillStyle = showHoverHighlight
    
  106.       ? COLORS.USER_TIMING_HOVER
    
  107.       : COLORS.USER_TIMING;
    
  108. 
    
  109.     if (fillStyle !== null) {
    
  110.       const y = baseY + halfSize;
    
  111. 
    
  112.       context.beginPath();
    
  113.       context.fillStyle = fillStyle;
    
  114.       context.moveTo(x, y - halfSize);
    
  115.       context.lineTo(x + halfSize, y);
    
  116.       context.lineTo(x, y + halfSize);
    
  117.       context.lineTo(x - halfSize, y);
    
  118.       context.fill();
    
  119.     }
    
  120.   }
    
  121. 
    
  122.   draw(context: CanvasRenderingContext2D) {
    
  123.     const {frame, _marks, _hoveredMark, visibleArea} = this;
    
  124. 
    
  125.     context.fillStyle = COLORS.BACKGROUND;
    
  126.     context.fillRect(
    
  127.       visibleArea.origin.x,
    
  128.       visibleArea.origin.y,
    
  129.       visibleArea.size.width,
    
  130.       visibleArea.size.height,
    
  131.     );
    
  132. 
    
  133.     // Draw marks
    
  134.     const baseY = frame.origin.y + TOP_ROW_PADDING;
    
  135.     const scaleFactor = positioningScaleFactor(
    
  136.       this._intrinsicSize.width,
    
  137.       frame,
    
  138.     );
    
  139. 
    
  140.     _marks.forEach(mark => {
    
  141.       if (mark === _hoveredMark) {
    
  142.         return;
    
  143.       }
    
  144.       this._drawSingleMark(
    
  145.         context,
    
  146.         visibleArea,
    
  147.         mark,
    
  148.         baseY,
    
  149.         scaleFactor,
    
  150.         false,
    
  151.       );
    
  152.     });
    
  153. 
    
  154.     // Draw the hovered and/or selected items on top so they stand out.
    
  155.     // This is helpful if there are multiple (overlapping) items close to each other.
    
  156.     if (_hoveredMark !== null) {
    
  157.       this._drawSingleMark(
    
  158.         context,
    
  159.         visibleArea,
    
  160.         _hoveredMark,
    
  161.         baseY,
    
  162.         scaleFactor,
    
  163.         true,
    
  164.       );
    
  165.     }
    
  166. 
    
  167.     // Render bottom border.
    
  168.     // Propose border rect, check if intersects with `rect`, draw intersection.
    
  169.     const borderFrame: Rect = {
    
  170.       origin: {
    
  171.         x: frame.origin.x,
    
  172.         y: frame.origin.y + ROW_HEIGHT_FIXED - BORDER_SIZE,
    
  173.       },
    
  174.       size: {
    
  175.         width: frame.size.width,
    
  176.         height: BORDER_SIZE,
    
  177.       },
    
  178.     };
    
  179.     if (rectIntersectsRect(borderFrame, visibleArea)) {
    
  180.       const borderDrawableRect = intersectionOfRects(borderFrame, visibleArea);
    
  181.       context.fillStyle = COLORS.PRIORITY_BORDER;
    
  182.       context.fillRect(
    
  183.         borderDrawableRect.origin.x,
    
  184.         borderDrawableRect.origin.y,
    
  185.         borderDrawableRect.size.width,
    
  186.         borderDrawableRect.size.height,
    
  187.       );
    
  188.     }
    
  189.   }
    
  190. 
    
  191.   /**
    
  192.    * @private
    
  193.    */
    
  194.   _handleMouseMove(interaction: MouseMoveInteraction, viewRefs: ViewRefs) {
    
  195.     const {frame, onHover, visibleArea} = this;
    
  196.     if (!onHover) {
    
  197.       return;
    
  198.     }
    
  199. 
    
  200.     const {location} = interaction.payload;
    
  201.     if (!rectContainsPoint(location, visibleArea)) {
    
  202.       onHover(null);
    
  203.       return;
    
  204.     }
    
  205. 
    
  206.     const {_marks} = this;
    
  207.     const scaleFactor = positioningScaleFactor(
    
  208.       this._intrinsicSize.width,
    
  209.       frame,
    
  210.     );
    
  211.     const hoverTimestamp = positionToTimestamp(location.x, scaleFactor, frame);
    
  212.     const timestampAllowance = widthToDuration(
    
  213.       USER_TIMING_MARK_SIZE / 2,
    
  214.       scaleFactor,
    
  215.     );
    
  216. 
    
  217.     // Because data ranges may overlap, we want to find the last intersecting item.
    
  218.     // This will always be the one on "top" (the one the user is hovering over).
    
  219.     for (let index = _marks.length - 1; index >= 0; index--) {
    
  220.       const mark = _marks[index];
    
  221.       const {timestamp} = mark;
    
  222. 
    
  223.       if (
    
  224.         timestamp - timestampAllowance <= hoverTimestamp &&
    
  225.         hoverTimestamp <= timestamp + timestampAllowance
    
  226.       ) {
    
  227.         viewRefs.hoveredView = this;
    
  228.         onHover(mark);
    
  229.         return;
    
  230.       }
    
  231.     }
    
  232. 
    
  233.     onHover(null);
    
  234.   }
    
  235. 
    
  236.   handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
    
  237.     switch (interaction.type) {
    
  238.       case 'mousemove':
    
  239.         this._handleMouseMove(interaction, viewRefs);
    
  240.         break;
    
  241.     }
    
  242.   }
    
  243. }