File size: 3,435 Bytes
7e3630c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
import type { DOMElement } from "src/ink";
import {
  type RefObject,
  useCallback,
  useEffect,
  useRef,
  useState,
} from "react";
import { useInkPictureConfig } from "../InkPictureProvider.js";

export type Position = {
  /**
   * Column position (x-coordinate) in terminal cells.
   */
  col: number;
  /**
   * Row position (y-coordinate) in terminal cells.
   */
  row: number;
  /**
   * Element width in terminal cells.
   */
  width: number;
  /**
   * Element height in terminal cells.
   */
  height: number;
  /**
   * Application width in terminal cells.
   */
  appWidth: number;
  /**
   * Application height in terminal cells.
   */
  appHeight: number;
};

/**
 * Get the current position and dimensions of a mounted component in terminal cells.
 *
 * @param ref - React ref pointing to a Box component
 * @returns Position object with col, row, width, and height
 */
const usePosition = (
  ref: RefObject<DOMElement | null>,
): Position | undefined => {
  const [position, setPosition] = useState<Position | undefined>(undefined);

  const updatePosition = useCallback(() => {
    const node = ref.current;
    if (!node?.yogaNode) {
      return;
    }

    const { yogaNode } = node;

    // Calculate absolute position by traversing up the tree
    let absoluteCol = 0;
    let absoluteRow = 0;
    let appWidth = 0;
    let appHeight = 0;
    let currentNode: DOMElement | undefined = node;

    while (currentNode) {
      if (currentNode.yogaNode) {
        absoluteCol += currentNode.yogaNode.getComputedLeft();
        absoluteRow += currentNode.yogaNode.getComputedTop();
        appWidth = currentNode.yogaNode.getComputedWidth();
        appHeight = currentNode.yogaNode.getComputedHeight();
      }

      currentNode = currentNode.parentNode;
    }

    const newPosition: Position = {
      col: absoluteCol,
      row: absoluteRow,
      width: yogaNode.getComputedWidth(),
      height: yogaNode.getComputedHeight(),
      appWidth,
      appHeight,
    };

    setPosition((previousPosition) => {
      // Only update if position actually changed to avoid unnecessary re-renders
      if (
        !previousPosition ||
        previousPosition.col !== newPosition.col ||
        previousPosition.row !== newPosition.row ||
        previousPosition.width !== newPosition.width ||
        previousPosition.height !== newPosition.height ||
        previousPosition.appWidth !== newPosition.appWidth ||
        previousPosition.appHeight !== newPosition.appHeight
      ) {
        return newPosition;
      }

      return previousPosition;
    });
  }, [ref]);

  // Calculate position after every render
  // This captures layout changes caused by the component's own re-renders
  useEffect(() => {
    updatePosition();
  });

  // Poll to detect layout changes that happen without re-rendering
  // e.g. parent layout changes, where React bails out from re-rendering children
  const config = useInkPictureConfig();
  const configRef = useRef(config);
  configRef.current = config;

  useEffect(() => {
    let timeout: ReturnType<typeof setTimeout>;

    function schedule() {
      timeout = setTimeout(tick, configRef.current.pollIntervalMs);
      timeout.unref();
    }

    function tick() {
      updatePosition();
      schedule();
    }

    schedule();

    return () => {
      clearTimeout(timeout);
    };
  }, [updatePosition]);

  return position;
};

export default usePosition;