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;
|