From 2269e443b8b456a6715d129e062a6154c37d4492 Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Fri, 8 May 2026 17:53:47 -0400 Subject: [PATCH] feat: overlay window API boundary Co-Authored-By: Claude Sonnet 4.6 --- src/lib/overlay.ts | 159 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 src/lib/overlay.ts diff --git a/src/lib/overlay.ts b/src/lib/overlay.ts new file mode 100644 index 0000000..c7fd043 --- /dev/null +++ b/src/lib/overlay.ts @@ -0,0 +1,159 @@ +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); + } +}