import type { Placement, Rect } from '../core/types'; type TooltipPosition = { x: number; y: number; placement: 'top' | 'bottom' | 'left' | 'right'; }; export type TooltipFrameInsets = { top: number; right: number; bottom: number; left: number; }; const EDGE_MARGIN = 12; const NO_INSETS: TooltipFrameInsets = { top: 0, right: 0, bottom: 0, left: 0, }; /** * Computes the optimal position for a tooltip relative to a target element. * * This function calculates the best position for a tooltip based on available screen space, * attempting to place it according to the specified placement preference. If the preferred * placement doesn't fit, it will try alternative placements (when using 'auto' mode) or * fallback to bottom placement with adjustments to fit within screen bounds. * * @param screenW - The width of the screen in pixels * @param screenH - The height of the screen in pixels * @param target - The rectangle dimensions and position of the target element * @param placement - The preferred placement direction ('auto', 'top', 'bottom', 'left', 'right') * @param tooltipSize - An object containing the width (w) and height (h) of the tooltip * @param gap - The spacing between the tooltip and target element in pixels (default: 12) * @param insets - Safe area insets excluded from the usable frame. Defaults to zero, * which keeps the tooltip inside the raw window. * * @returns An object containing: * - x: The horizontal position of the tooltip * - y: The vertical position of the tooltip * - placement: The final placement direction used ('top' | 'bottom' | 'left' | 'right') * * @remarks * - When placement is 'auto', the function tries placements in order: bottom, top, right, left * - The tooltip is automatically adjusted to stay within the safe frame with a 12px margin * - If no placement fits perfectly, defaults to bottom placement with position adjustments */ export function computeTooltipPosition( screenW: number, screenH: number, target: Rect, placement: Placement, tooltipSize: { w: number; h: number }, gap = 12, insets: TooltipFrameInsets = NO_INSETS ): TooltipPosition { const cx = target.x + target.width / 2; const cy = target.y + target.height / 2; const minX = insets.left; const minY = insets.top; const maxX = screenW - insets.right; const maxY = screenH - insets.bottom; const centeredX = Math.min( Math.max(cx - tooltipSize.w / 2, minX + EDGE_MARGIN), maxX - tooltipSize.w - EDGE_MARGIN ); const pref: Placement[] = placement === 'auto' ? ['bottom', 'top', 'right', 'left'] : [placement]; for (const p of pref) { if ( p === 'bottom' && target.y + target.height + gap + tooltipSize.h <= maxY ) { return { x: centeredX, y: target.y + target.height + gap, placement: 'bottom' as const, }; } if (p === 'top' && target.y - gap - tooltipSize.h >= minY) { return { x: centeredX, y: Math.max(target.y - gap - tooltipSize.h, minY + EDGE_MARGIN), placement: 'top' as const, }; } if ( p === 'right' && target.x + target.width + gap + tooltipSize.w <= maxX ) { return { x: target.x + target.width + gap, y: Math.min( Math.max(cy - tooltipSize.h / 2, minY + EDGE_MARGIN), maxY - tooltipSize.h - EDGE_MARGIN ), placement: 'right' as const, }; } if (p === 'left' && target.x - gap - tooltipSize.w >= minX) { return { x: target.x - gap - tooltipSize.w, y: Math.min( Math.max(cy - tooltipSize.h / 2, minY + EDGE_MARGIN), maxY - tooltipSize.h - EDGE_MARGIN ), placement: 'left' as const, }; } } return { x: centeredX, y: Math.min( target.y + target.height + gap, maxY - tooltipSize.h - EDGE_MARGIN ), placement: 'bottom' as const, }; }