feat: overlay window API boundary
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
159
src/lib/overlay.ts
Normal file
159
src/lib/overlay.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user