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 {
    
  11.   ClickInteraction,
    
  12.   DoubleClickInteraction,
    
  13.   Interaction,
    
  14.   MouseDownInteraction,
    
  15.   MouseMoveInteraction,
    
  16.   MouseUpInteraction,
    
  17. } from '../useCanvasInteraction';
    
  18. import type {Rect} from '../geometry';
    
  19. import type {ViewRefs} from '../Surface';
    
  20. import type {ViewState} from '../../types';
    
  21. 
    
  22. import {ResizeBarView} from './ResizeBarView';
    
  23. import {Surface} from '../Surface';
    
  24. import {View} from '../View';
    
  25. import {rectContainsPoint} from '../geometry';
    
  26. import {noopLayout} from '../layouter';
    
  27. import {clamp} from '../utils/clamp';
    
  28. 
    
  29. type ResizingState = $ReadOnly<{
    
  30.   /** Distance between top of resize bar and mouseY */
    
  31.   cursorOffsetInBarFrame: number,
    
  32.   /** Mouse's vertical coordinates relative to canvas */
    
  33.   mouseY: number,
    
  34. }>;
    
  35. 
    
  36. type LayoutState = {
    
  37.   /** Resize bar's vertical position relative to resize view's frame.origin.y */
    
  38.   barOffsetY: number,
    
  39. };
    
  40. 
    
  41. const RESIZE_BAR_HEIGHT = 8;
    
  42. const RESIZE_BAR_WITH_LABEL_HEIGHT = 16;
    
  43. 
    
  44. const HIDDEN_RECT = {
    
  45.   origin: {x: 0, y: 0},
    
  46.   size: {width: 0, height: 0},
    
  47. };
    
  48. 
    
  49. export class ResizableView extends View {
    
  50.   _canvasRef: {current: HTMLCanvasElement | null};
    
  51.   _layoutState: LayoutState;
    
  52.   _mutableViewStateKey: string;
    
  53.   _resizeBar: ResizeBarView;
    
  54.   _resizingState: ResizingState | null = null;
    
  55.   _subview: View;
    
  56.   _viewState: ViewState;
    
  57. 
    
  58.   constructor(
    
  59.     surface: Surface,
    
  60.     frame: Rect,
    
  61.     subview: View,
    
  62.     viewState: ViewState,
    
  63.     canvasRef: {current: HTMLCanvasElement | null},
    
  64.     label: string,
    
  65.   ) {
    
  66.     super(surface, frame, noopLayout);
    
  67. 
    
  68.     this._canvasRef = canvasRef;
    
  69.     this._layoutState = {barOffsetY: 0};
    
  70.     this._mutableViewStateKey = label + ':ResizableView';
    
  71.     this._subview = subview;
    
  72.     this._resizeBar = new ResizeBarView(surface, frame, label);
    
  73.     this._viewState = viewState;
    
  74. 
    
  75.     this.addSubview(this._subview);
    
  76.     this.addSubview(this._resizeBar);
    
  77. 
    
  78.     this._restoreMutableViewState();
    
  79.   }
    
  80. 
    
  81.   desiredSize(): {+height: number, +width: number} {
    
  82.     const subviewDesiredSize = this._subview.desiredSize();
    
  83. 
    
  84.     if (this._shouldRenderResizeBar()) {
    
  85.       const resizeBarDesiredSize = this._resizeBar.desiredSize();
    
  86. 
    
  87.       return {
    
  88.         width: this.frame.size.width,
    
  89.         height: this._layoutState.barOffsetY + resizeBarDesiredSize.height,
    
  90.       };
    
  91.     } else {
    
  92.       return {
    
  93.         width: this.frame.size.width,
    
  94.         height: subviewDesiredSize.height,
    
  95.       };
    
  96.     }
    
  97.   }
    
  98. 
    
  99.   layoutSubviews() {
    
  100.     this._updateLayoutState();
    
  101.     this._updateSubviewFrames();
    
  102. 
    
  103.     super.layoutSubviews();
    
  104.   }
    
  105. 
    
  106.   _restoreMutableViewState() {
    
  107.     if (
    
  108.       this._viewState.viewToMutableViewStateMap.has(this._mutableViewStateKey)
    
  109.     ) {
    
  110.       this._layoutState = ((this._viewState.viewToMutableViewStateMap.get(
    
  111.         this._mutableViewStateKey,
    
  112.       ): any): LayoutState);
    
  113. 
    
  114.       this._updateLayoutStateAndResizeBar(this._layoutState.barOffsetY);
    
  115.     } else {
    
  116.       this._viewState.viewToMutableViewStateMap.set(
    
  117.         this._mutableViewStateKey,
    
  118.         this._layoutState,
    
  119.       );
    
  120. 
    
  121.       const subviewDesiredSize = this._subview.desiredSize();
    
  122.       this._updateLayoutStateAndResizeBar(
    
  123.         subviewDesiredSize.maxInitialHeight != null
    
  124.           ? Math.min(
    
  125.               subviewDesiredSize.maxInitialHeight,
    
  126.               subviewDesiredSize.height,
    
  127.             )
    
  128.           : subviewDesiredSize.height,
    
  129.       );
    
  130.     }
    
  131. 
    
  132.     this.setNeedsDisplay();
    
  133.   }
    
  134. 
    
  135.   _shouldRenderResizeBar(): boolean {
    
  136.     const subviewDesiredSize = this._subview.desiredSize();
    
  137.     return subviewDesiredSize.hideScrollBarIfLessThanHeight != null
    
  138.       ? subviewDesiredSize.height >
    
  139.           subviewDesiredSize.hideScrollBarIfLessThanHeight
    
  140.       : true;
    
  141.   }
    
  142. 
    
  143.   _updateLayoutStateAndResizeBar(barOffsetY: number) {
    
  144.     if (barOffsetY <= RESIZE_BAR_WITH_LABEL_HEIGHT - RESIZE_BAR_HEIGHT) {
    
  145.       barOffsetY = 0;
    
  146.     }
    
  147. 
    
  148.     this._layoutState.barOffsetY = barOffsetY;
    
  149. 
    
  150.     this._resizeBar.showLabel = barOffsetY === 0;
    
  151.   }
    
  152. 
    
  153.   _updateLayoutState() {
    
  154.     const {frame, _resizingState} = this;
    
  155. 
    
  156.     // Allow bar to travel to bottom of the visible area of this view but no further
    
  157.     const subviewDesiredSize = this._subview.desiredSize();
    
  158.     const maxBarOffset = subviewDesiredSize.height;
    
  159. 
    
  160.     let proposedBarOffsetY = this._layoutState.barOffsetY;
    
  161.     // Update bar offset if dragging bar
    
  162.     if (_resizingState) {
    
  163.       const {mouseY, cursorOffsetInBarFrame} = _resizingState;
    
  164.       proposedBarOffsetY = mouseY - frame.origin.y - cursorOffsetInBarFrame;
    
  165.     }
    
  166. 
    
  167.     this._updateLayoutStateAndResizeBar(
    
  168.       clamp(0, maxBarOffset, proposedBarOffsetY),
    
  169.     );
    
  170.   }
    
  171. 
    
  172.   _updateSubviewFrames() {
    
  173.     const {
    
  174.       frame: {
    
  175.         origin: {x, y},
    
  176.         size: {width},
    
  177.       },
    
  178.       _layoutState: {barOffsetY},
    
  179.     } = this;
    
  180. 
    
  181.     const resizeBarDesiredSize = this._resizeBar.desiredSize();
    
  182. 
    
  183.     if (barOffsetY === 0) {
    
  184.       this._subview.setFrame(HIDDEN_RECT);
    
  185.     } else {
    
  186.       this._subview.setFrame({
    
  187.         origin: {x, y},
    
  188.         size: {width, height: barOffsetY},
    
  189.       });
    
  190.     }
    
  191. 
    
  192.     this._resizeBar.setFrame({
    
  193.       origin: {x, y: y + barOffsetY},
    
  194.       size: {width, height: resizeBarDesiredSize.height},
    
  195.     });
    
  196.   }
    
  197. 
    
  198.   _handleClick(interaction: ClickInteraction): void | boolean {
    
  199.     if (!this._shouldRenderResizeBar()) {
    
  200.       return;
    
  201.     }
    
  202. 
    
  203.     const cursorInView = rectContainsPoint(
    
  204.       interaction.payload.location,
    
  205.       this.frame,
    
  206.     );
    
  207.     if (cursorInView) {
    
  208.       if (this._layoutState.barOffsetY === 0) {
    
  209.         // Clicking on the collapsed label should expand.
    
  210.         const subviewDesiredSize = this._subview.desiredSize();
    
  211.         this._updateLayoutStateAndResizeBar(subviewDesiredSize.height);
    
  212.         this.setNeedsDisplay();
    
  213. 
    
  214.         return true;
    
  215.       }
    
  216.     }
    
  217.   }
    
  218. 
    
  219.   _handleDoubleClick(interaction: DoubleClickInteraction): void | boolean {
    
  220.     if (!this._shouldRenderResizeBar()) {
    
  221.       return;
    
  222.     }
    
  223. 
    
  224.     const cursorInView = rectContainsPoint(
    
  225.       interaction.payload.location,
    
  226.       this.frame,
    
  227.     );
    
  228.     if (cursorInView) {
    
  229.       if (this._layoutState.barOffsetY > 0) {
    
  230.         // Double clicking on the expanded view should collapse.
    
  231.         this._updateLayoutStateAndResizeBar(0);
    
  232.         this.setNeedsDisplay();
    
  233. 
    
  234.         return true;
    
  235.       }
    
  236.     }
    
  237.   }
    
  238. 
    
  239.   _handleMouseDown(interaction: MouseDownInteraction): void | boolean {
    
  240.     const cursorLocation = interaction.payload.location;
    
  241.     const resizeBarFrame = this._resizeBar.frame;
    
  242.     if (rectContainsPoint(cursorLocation, resizeBarFrame)) {
    
  243.       const mouseY = cursorLocation.y;
    
  244.       this._resizingState = {
    
  245.         cursorOffsetInBarFrame: mouseY - resizeBarFrame.origin.y,
    
  246.         mouseY,
    
  247.       };
    
  248. 
    
  249.       return true;
    
  250.     }
    
  251.   }
    
  252. 
    
  253.   _handleMouseMove(interaction: MouseMoveInteraction): void | boolean {
    
  254.     const {_resizingState} = this;
    
  255.     if (_resizingState) {
    
  256.       this._resizingState = {
    
  257.         ..._resizingState,
    
  258.         mouseY: interaction.payload.location.y,
    
  259.       };
    
  260.       this.setNeedsDisplay();
    
  261. 
    
  262.       return true;
    
  263.     }
    
  264.   }
    
  265. 
    
  266.   _handleMouseUp(interaction: MouseUpInteraction) {
    
  267.     if (this._resizingState) {
    
  268.       this._resizingState = null;
    
  269.     }
    
  270.   }
    
  271. 
    
  272.   getCursorActiveSubView(interaction: Interaction): View | null {
    
  273.     const cursorLocation = interaction.payload.location;
    
  274.     const resizeBarFrame = this._resizeBar.frame;
    
  275.     if (rectContainsPoint(cursorLocation, resizeBarFrame)) {
    
  276.       return this;
    
  277.     } else {
    
  278.       return null;
    
  279.     }
    
  280.   }
    
  281. 
    
  282.   handleInteraction(
    
  283.     interaction: Interaction,
    
  284.     viewRefs: ViewRefs,
    
  285.   ): void | boolean {
    
  286.     switch (interaction.type) {
    
  287.       case 'click':
    
  288.         return this._handleClick(interaction);
    
  289.       case 'double-click':
    
  290.         return this._handleDoubleClick(interaction);
    
  291.       case 'mousedown':
    
  292.         return this._handleMouseDown(interaction);
    
  293.       case 'mousemove':
    
  294.         return this._handleMouseMove(interaction);
    
  295.       case 'mouseup':
    
  296.         return this._handleMouseUp(interaction);
    
  297.     }
    
  298.   }
    
  299. }