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 {clamp} from './clamp';
    
  11. 
    
  12. /**
    
  13.  * Single-axis offset and length state.
    
  14.  *
    
  15.  * ```
    
  16.  * contentStart   containerStart  containerEnd   contentEnd
    
  17.  *     |<----------offset|              |             |
    
  18.  *     |<-------------------length------------------->|
    
  19.  * ```
    
  20.  */
    
  21. export type ScrollState = {
    
  22.   offset: number,
    
  23.   length: number,
    
  24. };
    
  25. 
    
  26. function clampOffset(state: ScrollState, containerLength: number): ScrollState {
    
  27.   return {
    
  28.     offset: clamp(-(state.length - containerLength), 0, state.offset),
    
  29.     length: state.length,
    
  30.   };
    
  31. }
    
  32. 
    
  33. function clampLength({
    
  34.   state,
    
  35.   minContentLength,
    
  36.   maxContentLength,
    
  37.   containerLength,
    
  38. }: {
    
  39.   state: ScrollState,
    
  40.   minContentLength: number,
    
  41.   maxContentLength: number,
    
  42.   containerLength: number,
    
  43. }): ScrollState {
    
  44.   return {
    
  45.     offset: state.offset,
    
  46.     length: clamp(
    
  47.       Math.max(minContentLength, containerLength),
    
  48.       Math.max(containerLength, maxContentLength),
    
  49.       state.length,
    
  50.     ),
    
  51.   };
    
  52. }
    
  53. 
    
  54. /**
    
  55.  * Returns `state` clamped such that:
    
  56.  * - `length`: you won't be able to zoom in/out such that the content is
    
  57.  *   shorter than the `containerLength`.
    
  58.  * - `offset`: content remains in `containerLength`.
    
  59.  */
    
  60. export function clampState({
    
  61.   state,
    
  62.   minContentLength,
    
  63.   maxContentLength,
    
  64.   containerLength,
    
  65. }: {
    
  66.   state: ScrollState,
    
  67.   minContentLength: number,
    
  68.   maxContentLength: number,
    
  69.   containerLength: number,
    
  70. }): ScrollState {
    
  71.   return clampOffset(
    
  72.     clampLength({
    
  73.       state,
    
  74.       minContentLength,
    
  75.       maxContentLength,
    
  76.       containerLength,
    
  77.     }),
    
  78.     containerLength,
    
  79.   );
    
  80. }
    
  81. 
    
  82. export function translateState({
    
  83.   state,
    
  84.   delta,
    
  85.   containerLength,
    
  86. }: {
    
  87.   state: ScrollState,
    
  88.   delta: number,
    
  89.   containerLength: number,
    
  90. }): ScrollState {
    
  91.   return clampOffset(
    
  92.     {
    
  93.       offset: state.offset + delta,
    
  94.       length: state.length,
    
  95.     },
    
  96.     containerLength,
    
  97.   );
    
  98. }
    
  99. 
    
  100. /**
    
  101.  * Returns a new clamped `state` zoomed by `multiplier`.
    
  102.  *
    
  103.  * The provided fixed point will also remain stationary relative to
    
  104.  * `containerStart`.
    
  105.  *
    
  106.  * ```
    
  107.  * contentStart   containerStart                fixedPoint containerEnd
    
  108.  *     |<---------offset-|                          x           |
    
  109.  *     |-fixedPoint-------------------------------->x           |
    
  110.  *                       |-fixedPointFromContainer->x           |
    
  111.  *                       |<----------containerLength----------->|
    
  112.  * ```
    
  113.  */
    
  114. export function zoomState({
    
  115.   state,
    
  116.   multiplier,
    
  117.   fixedPoint,
    
  118. 
    
  119.   minContentLength,
    
  120.   maxContentLength,
    
  121.   containerLength,
    
  122. }: {
    
  123.   state: ScrollState,
    
  124.   multiplier: number,
    
  125.   fixedPoint: number,
    
  126. 
    
  127.   minContentLength: number,
    
  128.   maxContentLength: number,
    
  129.   containerLength: number,
    
  130. }): ScrollState {
    
  131.   // Length and offset must be computed separately, so that if the length is
    
  132.   // clamped the offset will still be correct (unless it gets clamped too).
    
  133. 
    
  134.   const zoomedState = clampLength({
    
  135.     state: {
    
  136.       offset: state.offset,
    
  137.       length: state.length * multiplier,
    
  138.     },
    
  139.     minContentLength,
    
  140.     maxContentLength,
    
  141.     containerLength,
    
  142.   });
    
  143. 
    
  144.   // Adjust offset so that distance between containerStart<->fixedPoint is fixed
    
  145.   const fixedPointFromContainer = fixedPoint + state.offset;
    
  146.   const scaledFixedPoint = fixedPoint * (zoomedState.length / state.length);
    
  147.   const offsetAdjustedState = clampOffset(
    
  148.     {
    
  149.       offset: fixedPointFromContainer - scaledFixedPoint,
    
  150.       length: zoomedState.length,
    
  151.     },
    
  152.     containerLength,
    
  153.   );
    
  154. 
    
  155.   return offsetAdjustedState;
    
  156. }
    
  157. 
    
  158. export function moveStateToRange({
    
  159.   state,
    
  160.   rangeStart,
    
  161.   rangeEnd,
    
  162.   contentLength,
    
  163. 
    
  164.   minContentLength,
    
  165.   maxContentLength,
    
  166.   containerLength,
    
  167. }: {
    
  168.   state: ScrollState,
    
  169.   rangeStart: number,
    
  170.   rangeEnd: number,
    
  171.   contentLength: number,
    
  172. 
    
  173.   minContentLength: number,
    
  174.   maxContentLength: number,
    
  175.   containerLength: number,
    
  176. }): ScrollState {
    
  177.   // Length and offset must be computed separately, so that if the length is
    
  178.   // clamped the offset will still be correct (unless it gets clamped too).
    
  179. 
    
  180.   const lengthClampedState = clampLength({
    
  181.     state: {
    
  182.       offset: state.offset,
    
  183.       length: contentLength * (containerLength / (rangeEnd - rangeStart)),
    
  184.     },
    
  185.     minContentLength,
    
  186.     maxContentLength,
    
  187.     containerLength,
    
  188.   });
    
  189. 
    
  190.   const offsetAdjustedState = clampOffset(
    
  191.     {
    
  192.       offset: -rangeStart * (lengthClampedState.length / contentLength),
    
  193.       length: lengthClampedState.length,
    
  194.     },
    
  195.     containerLength,
    
  196.   );
    
  197. 
    
  198.   return offsetAdjustedState;
    
  199. }
    
  200. 
    
  201. export function areScrollStatesEqual(
    
  202.   state1: ScrollState,
    
  203.   state2: ScrollState,
    
  204. ): boolean {
    
  205.   return state1.offset === state2.offset && state1.length === state2.length;
    
  206. }