feat: overlay window API boundary

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Michael Chihlas
2026-05-08 17:53:47 -04:00
parent ce88841209
commit 2269e443b8

159
src/lib/overlay.ts Normal file
View File

@@ -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<WebviewWindow | null> {
return await WebviewWindow.getByLabel(OVERLAY_LABEL);
}
export async function openOverlay(opts: OpenOptions): Promise<void> {
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<void>((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<void> {
const w = await getOverlay();
if (!w) return;
await w.close();
}
export async function applyOpacity(v: number): Promise<void> {
const w = await getOverlay();
if (!w) return;
await setWindowOpacity(OVERLAY_LABEL, v);
}
export async function applyAlwaysOnTop(v: boolean): Promise<void> {
const w = await getOverlay();
if (!w) return;
await w.setAlwaysOnTop(v);
}
export async function applyClickThrough(v: boolean): Promise<void> {
const w = await getOverlay();
if (!w) return;
await w.setIgnoreCursorEvents(v);
}
/** Returns true if the overlay window currently exists. */
export async function isOverlayOpen(): Promise<boolean> {
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<void> {
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<void> {
try {
await invoke("sync_click_through_cache", { value });
} catch (err) {
console.warn("Failed to sync click-through cache to backend", err);
}
}