codev / src /ink-picture /hooks /usePosition.ts
chenbhao's picture
refactor: integrate ink-picture as source directory
7e3630c
Raw
History Blame Contribute Delete
3.44 kB
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;