import { invoke } from "@tauri-apps/api/core"; import { WebviewWindow, getCurrentWebviewWindow, } from "@tauri-apps/api/webviewWindow"; import { currentMonitor } from "@tauri-apps/api/window"; import { OVERLAY_LABEL } from "../types/overlay"; export type OpenOptions = { url: string; opacity: number; alwaysOnTop: boolean; clickThrough: boolean; /** Called when the overlay window is destroyed by *any* path (taskbar close, * Alt+F4, our own closeOverlay, OS forced close). Use it to reconcile * control-panel state. Fires exactly once per open. */ onDestroyed: () => void; }; const FIRST_OPEN_W = 800; const FIRST_OPEN_H = 600; const FIRST_OPEN_MARGIN = 24; async function getOverlay(): Promise { return await WebviewWindow.getByLabel(OVERLAY_LABEL); } export async function openOverlay(opts: OpenOptions): Promise { const existing = await getOverlay(); if (existing) { await closeOverlay(); } // Default to top-right of primary monitor on first open; // tauri-plugin-window-state will restore prior geometry on subsequent opens. let x = 100; let y = 100; try { const monitor = await currentMonitor(); if (monitor) { const scale = monitor.scaleFactor; const logicalW = monitor.size.width / scale; x = Math.round(logicalW - FIRST_OPEN_W - FIRST_OPEN_MARGIN); y = FIRST_OPEN_MARGIN; } } catch { // best-effort placement; fall through to defaults } const w = new WebviewWindow(OVERLAY_LABEL, { url: opts.url, title: "Browserlay overlay", width: FIRST_OPEN_W, height: FIRST_OPEN_H, x, y, decorations: false, transparent: true, alwaysOnTop: opts.alwaysOnTop, resizable: true, visible: true, skipTaskbar: false, }); // Wait for the window to actually exist (or surface an OS error). Both // tauri://created and tauri://error fire at most once per window; we // unsubscribe whichever didn't win. await new Promise((resolve, reject) => { let settled = false; let unlistenCreated: (() => void) | null = null; let unlistenError: (() => void) | null = null; void w.once("tauri://created", () => { if (settled) return; settled = true; unlistenError?.(); resolve(); }).then((u) => { unlistenCreated = u; if (settled) u(); }); void w.once("tauri://error", (e) => { if (settled) return; settled = true; unlistenCreated?.(); reject(new Error(`Failed to create overlay window: ${JSON.stringify(e.payload)}`)); }).then((u) => { unlistenError = u; if (settled) u(); }); }); // Single source of truth for "overlay went away" — fires for *any* close // path (our closeOverlay, taskbar X, Alt+F4, OS-forced destruction). void w.once("tauri://destroyed", () => { opts.onDestroyed(); }); // Apply runtime-only state that's not part of the constructor. await setWindowOpacity(OVERLAY_LABEL, opts.opacity); await w.setIgnoreCursorEvents(opts.clickThrough); // Seed the Rust-side click-through cache so the global shortcut handler // knows the current value when it computes the inverse. await syncClickThroughCache(opts.clickThrough); } export async function closeOverlay(): Promise { const w = await getOverlay(); if (!w) return; await w.close(); } export async function applyOpacity(v: number): Promise { const w = await getOverlay(); if (!w) return; await setWindowOpacity(OVERLAY_LABEL, v); } export async function applyAlwaysOnTop(v: boolean): Promise { const w = await getOverlay(); if (!w) return; await w.setAlwaysOnTop(v); } export async function applyClickThrough(v: boolean): Promise { const w = await getOverlay(); if (!w) return; await w.setIgnoreCursorEvents(v); } /** Returns true if the overlay window currently exists. */ export async function isOverlayOpen(): Promise { return (await getOverlay()) !== null; } /** Convenience for the control window's own handle if needed elsewhere. */ export function getControlWindow(): WebviewWindow { return getCurrentWebviewWindow(); } /** Sets the opacity of a named window via Rust invoke. * Tauri JS API v2 does not expose setOpacity on WebviewWindow; we route * through a Rust command instead. */ async function setWindowOpacity(label: string, opacity: number): Promise { try { await invoke("set_window_opacity", { label, opacity }); } catch (err) { console.warn("Failed to set window opacity", err); } } export async function syncClickThroughCache(value: boolean): Promise { try { await invoke("sync_click_through_cache", { value }); } catch (err) { console.warn("Failed to sync click-through cache to backend", err); } }