clone

a deno framework for local-first web apps that open instantly, work offline and sync when users choose

main
lofi / package / recipes / window-controls-overlay.ts
window-controls-overlay.ts 3.5K
1/**2 * Opt-in geometry observation for desktop Window Controls Overlay layouts.3 *4 * @module5 */67/** Value-only titlebar area rectangle in CSS pixels, clamped to finite non-negative numbers. */8export type TitlebarAreaRect = {9 /** Left edge of the usable titlebar area, relative to the viewport. */10 readonly x: number;11 /** Top edge of the usable titlebar area, relative to the viewport. */12 readonly y: number;13 /** Width available to application content between window controls. */14 readonly width: number;15 /** Height of the titlebar area. */16 readonly height: number;17};1819/** A value-only copy of the titlebar area available to application content. */20export type WindowControlsOverlayGeometry = {21 readonly visible: boolean;22 readonly titlebarArea: TitlebarAreaRect;23};2425/** Minimal browser surface consumed by this recipe. */26export type WindowControlsOverlayClient = {27 readonly visible: boolean;28 getTitlebarAreaRect(): Pick<DOMRectReadOnly, "x" | "y" | "width" | "height">;29 addEventListener(type: "geometrychange", listener: EventListener): void;30 removeEventListener(type: "geometrychange", listener: EventListener): void;31};3233/** Disposable result from installing the geometry observer. */34export type WindowControlsOverlayObserver = {35 readonly supported: boolean;36 dispose(): void;37};3839/** Options for observing overlay visibility and geometry changes. */40export type ObserveWindowControlsOverlayOptions = {41 /** Called initially and whenever browser geometry changes. */42 onGeometry(geometry: WindowControlsOverlayGeometry): void;43 /** Override the browser API for tests. */44 client?: WindowControlsOverlayClient;45};4647function browserClient(): WindowControlsOverlayClient | undefined {48 if (typeof navigator === "undefined") return undefined;49 const candidate = navigator as Navigator & {50 windowControlsOverlay?: WindowControlsOverlayClient;51 };52 const client = candidate.windowControlsOverlay;53 return client && typeof client.getTitlebarAreaRect === "function" &&54 typeof client.addEventListener === "function" &&55 typeof client.removeEventListener === "function"56 ? client57 : undefined;58}5960function finiteNonNegative(value: number): number {61 return Number.isFinite(value) && value >= 0 ? value : 0;62}6364function readGeometry(client: WindowControlsOverlayClient): WindowControlsOverlayGeometry {65 const rect = client.getTitlebarAreaRect();66 return {67 visible: client.visible === true,68 titlebarArea: {69 x: finiteNonNegative(rect.x),70 y: finiteNonNegative(rect.y),71 width: finiteNonNegative(rect.width),72 height: finiteNonNegative(rect.height),73 },74 };75}7677/**78 * Observe the experimental Window Controls Overlay geometry surface.79 *80 * CSS `titlebar-area-*` environment variables should own collision-free81 * layout. This observer is for state that genuinely needs geometry changes.82 * Unsupported browsers retain the ordinary standalone header and receive no83 * callback.84 */85export function observeWindowControlsOverlay(86 options: ObserveWindowControlsOverlayOptions,87): WindowControlsOverlayObserver {88 const client = options.client ?? browserClient();89 if (!client) return { supported: false, dispose() {} };90 let active = true;91 const update = () => {92 if (active) options.onGeometry(readGeometry(client));93 };94 client.addEventListener("geometrychange", update);95 update();96 return {97 supported: true,98 dispose() {99 if (!active) return;100 active = false;101 client.removeEventListener("geometrychange", update);102 },103 };104}