import type { WindowBounds } from '../types/popout' export interface WorkAreaBounds { x: number y: number width: number height: number } export interface StackableWindowLike { isDestroyed(): boolean isAlwaysOnTop(): boolean moveTop(): void } function getRight(bounds: WorkAreaBounds): number { return bounds.x + bounds.width } function getBottom(bounds: WorkAreaBounds): number { return bounds.y + bounds.height } function rangesOverlap(startA: number, endA: number, startB: number, endB: number): boolean { return startA < endB && startB < endA } function rectsIntersect(a: WorkAreaBounds, b: WorkAreaBounds): boolean { return rangesOverlap(a.x, getRight(a), b.x, getRight(b)) && rangesOverlap(a.y, getBottom(a), b.y, getBottom(b)) } function clamp(value: number, min: number, max: number): number { if (min > max) { return Math.round((min + max) / 2) } return Math.min(max, Math.max(min, value)) } function unionBounds(boundsList: readonly WorkAreaBounds[]): WorkAreaBounds { const left = Math.min(...boundsList.map((bounds) => bounds.x)) const top = Math.min(...boundsList.map((bounds) => bounds.y)) const right = Math.max(...boundsList.map((bounds) => getRight(bounds))) const bottom = Math.max(...boundsList.map((bounds) => getBottom(bounds))) return { x: left, y: top, width: right - left, height: bottom - top, } } function normalizeWorkAreas(workAreas: readonly WorkAreaBounds[]): WorkAreaBounds[] { return workAreas .filter((bounds) => bounds.width > 0 && bounds.height > 0) .map((bounds) => ({ x: Math.round(bounds.x), y: Math.round(bounds.y), width: Math.round(bounds.width), height: Math.round(bounds.height), })) } export function buildDisplayEnvelope( anchorBounds: WindowBounds, projectedBounds: WorkAreaBounds, workAreas: readonly WorkAreaBounds[], ): WorkAreaBounds { const normalizedWorkAreas = normalizeWorkAreas(workAreas) if (normalizedWorkAreas.length === 0) { return { ...projectedBounds } } const relevant = normalizedWorkAreas.filter((workArea) => { return rectsIntersect(workArea, projectedBounds) || rangesOverlap(workArea.x, getRight(workArea), anchorBounds.x, getRight(anchorBounds)) }) return unionBounds(relevant.length > 0 ? relevant : normalizedWorkAreas) } export function clampBoundsWithinEnvelope( bounds: WindowBounds, envelope: WorkAreaBounds, ): WindowBounds { const maxX = getRight(envelope) - bounds.width const maxY = getBottom(envelope) - bounds.height return { x: clamp(bounds.x, envelope.x, maxX), y: clamp(bounds.y, envelope.y, maxY), width: bounds.width, height: bounds.height, } } export function clampBoundsWithVisibleMargin( bounds: WindowBounds, envelope: WorkAreaBounds, visibleMargin: number, ): WindowBounds { const margin = Math.max(0, Math.round(visibleMargin)) const minX = envelope.x + margin - bounds.width const maxX = getRight(envelope) - margin const minY = envelope.y + margin - bounds.height const maxY = getBottom(envelope) - margin return { x: clamp(bounds.x, minX, maxX), y: clamp(bounds.y, minY, maxY), width: bounds.width, height: bounds.height, } } export function resolveExpandedMainWindowBounds( logicalBounds: WindowBounds, settingsHeight: number, workAreas: readonly WorkAreaBounds[], ): WindowBounds { const nextSettingsHeight = Math.max(0, Math.round(settingsHeight)) if (nextSettingsHeight === 0) { return { ...logicalBounds } } const projectedBounds: WindowBounds = { x: logicalBounds.x, y: logicalBounds.y, width: logicalBounds.width, height: logicalBounds.height + nextSettingsHeight, } const envelope = buildDisplayEnvelope(logicalBounds, projectedBounds, workAreas) return clampBoundsWithinEnvelope(projectedBounds, envelope) } export function clampDraggedMainWindowBounds( actualBounds: WindowBounds, workAreas: readonly WorkAreaBounds[], visibleMargin: number, ): WindowBounds { const envelope = buildDisplayEnvelope(actualBounds, actualBounds, workAreas) return clampBoundsWithVisibleMargin(actualBounds, envelope, visibleMargin) } export function clampRestoredWindowBounds( bounds: WindowBounds, workAreas: readonly WorkAreaBounds[], visibleMargin: number, ): WindowBounds { const envelope = buildDisplayEnvelope(bounds, bounds, workAreas) return clampBoundsWithVisibleMargin(bounds, envelope, visibleMargin) } export function raiseWindowAboveNormalPopouts( mainWindow: StackableWindowLike | null, popouts: Iterable, ): boolean { if (!mainWindow || mainWindow.isDestroyed()) { return false } for (const popout of popouts) { if (!popout.isDestroyed() && !popout.isAlwaysOnTop()) { mainWindow.moveTop() return true } } return false }