import { tileElement, type Board } from '../board.svelte'; import { windowOfHost } from './hosts'; import { find, windows } from './tree'; export type Direction = 'left' | 'right' | 'up' | 'down'; /** The windows of `board` on screen now, with where they are; a parked window, a hidden tab or what is behind a window filling the board is not. */ export function shown(board: Board): Map { const rects = new Map(); for (const host of document.querySelectorAll('.window-host')) { const window = windowOfHost(board.id, host); if (!window || host.closest('[inert]') || !host.checkVisibility({ visibilityProperty: true })) continue; const rect = host.getBoundingClientRect(); if (rect.width > 0 && rect.height > 0) rects.set(window, rect); } return rects; } /** What a window diagonal to the selected one costs on top, so any window overlapping it across the way is nearer. */ const DIAGONAL = 1e5; /** Where the selected pane is on screen, and the windows in it; a section or canvas by its tile. */ function selection(board: Board, rects: Map): { from: DOMRect; inside: Set } | null { const id = board.selected; const tile = id && !board.windows[id] ? find(board.root, id) : null; const from = tile ? tileElement(tile.id)?.getBoundingClientRect() : id ? rects.get(id) : undefined; return from?.width ? { from, inside: new Set(tile ? windows(tile) : [id!]) } : null; } /** * The window nearest the selected pane in `direction`, by where their slots * are on screen, so it crosses splits, tabs and canvases alike; never one * inside the selected section or canvas. A window overlapping the selected * pane across the way wins; one diagonal to it is chosen only when none * overlaps, and then only if it is wholly that way. * `skip` leaves windows out, and `beyond` only counts windows wholly that * way, not ones overlapping the selected one along it with a centre past it. */ export function nearest(board: Board, direction: Direction, { skip = () => false, beyond = false }: { skip?: (window: string) => boolean; beyond?: boolean } = {}): string | null { const rects = shown(board); const selected = selection(board, rects); if (!selected) return null; const { from, inside } = selected; const across = direction === 'left' || direction === 'right'; const sign = direction === 'right' || direction === 'down' ? 1 : -1; let best: { id: string; cost: number } | null = null; for (const [id, rect] of rects) { if (inside.has(id) || skip(id)) continue; // Edge to edge along the way, so a wide neighbour is as near as a narrow one. const ahead = across ? sign > 0 ? rect.left - from.right : from.left - rect.right : sign > 0 ? rect.top - from.bottom : from.top - rect.bottom; const centre = across ? rect.left + rect.width / 2 - (from.left + from.width / 2) : rect.top + rect.height / 2 - (from.top + from.height / 2); const aside = across ? Math.max(0, rect.top - from.bottom, from.top - rect.bottom) : Math.max(0, rect.left - from.right, from.left - rect.right); if (ahead < -1 && (beyond || aside > 0 || Math.sign(centre) !== sign)) continue; const cost = Math.max(0, ahead) + 2 * aside + (aside > 0 ? DIAGONAL : 0); if (!best || cost < best.cost) best = { id, cost }; } return best?.id ?? null; } /** Selects the window nearest the selected pane in `direction`, or the top left one when none is selected. */ export function focusToward(board: Board, direction: Direction): string | null { const rects = shown(board); const next = selection(board, rects) ? nearest(board, direction) : ([...rects].sort(([, a], [, b]) => a.top - b.top || a.left - b.left)[0]?.[0] ?? null); if (next) board.select(next); return next; }