| 1 | // useWindowStatePersistence polls the Wails runtime for window geometry and |
| 2 | // persists it via SaveWindowState so the next launch restores the same size and |
| 3 | // position. No-op in browser dev (no window.runtime). |
| 4 | // |
| 5 | // The frontend is the sole source of geometry: resize (debounced), a 5s poll, |
| 6 | // and beforeunload. Go never reads WindowGetSize/Position/IsMaximised during |
| 7 | // beforeClose or shutdown (those paths can panic on Windows when DPI reports |
| 8 | // 0). The Go shutdown hook only re-persists the last frontend-reported state. |
| 9 | |
| 10 | import { useEffect } from "react"; |
| 11 | import { app } from "./bridge"; |
| 12 | |
| 13 | export interface WindowStateSnapshot { |
| 14 | width: number; |
| 15 | height: number; |
| 16 | x: number; |
| 17 | y: number; |
| 18 | maximised: boolean; |
| 19 | } |
| 20 | |
| 21 | interface WindowStateRuntime { |
| 22 | WindowGetSize(): Promise<{ w: number; h: number }>; |
| 23 | WindowGetPosition(): Promise<{ x: number; y: number }>; |
| 24 | WindowIsMaximised(): Promise<boolean>; |
| 25 | } |
| 26 | |
| 27 | // Serialize capture + persistence as one operation. Resize, polling, and |
| 28 | // beforeunload can all request a save while an earlier Wails call is still in |
| 29 | // flight; a queue prevents an older completion from overwriting a newer |
| 30 | // observation. A queued request captures geometry only when it reaches the |
| 31 | // front, so it observes the latest native state instead of replaying a stale |
| 32 | // snapshot. |
| 33 | export function createWindowStateSaver( |
| 34 | runtime: WindowStateRuntime, |
| 35 | persist: (state: WindowStateSnapshot) => Promise<void>, |
| 36 | ): () => Promise<void> { |
| 37 | let lastState = ""; |
| 38 | let queue = Promise.resolve(); |
| 39 | |
| 40 | return () => { |
| 41 | queue = queue.then(async () => { |
| 42 | try { |
| 43 | const size = await runtime.WindowGetSize(); |
| 44 | const pos = await runtime.WindowGetPosition(); |
| 45 | const maximised = await runtime.WindowIsMaximised(); |
| 46 | const state = { width: size.w, height: size.h, x: pos.x, y: pos.y, maximised }; |
| 47 | const json = JSON.stringify(state); |
| 48 | if (json === lastState) return; |
| 49 | await persist(state); |
| 50 | lastState = json; |
| 51 | } catch { |
| 52 | /* runtime not ready yet — a later request will retry */ |
| 53 | } |
| 54 | }); |
| 55 | return queue; |
| 56 | }; |
| 57 | } |
| 58 | |
| 59 | export function useWindowStatePersistence() { |
| 60 | useEffect(() => { |
| 61 | const runtime = typeof window !== "undefined" ? window.runtime : undefined; |
| 62 | if (!runtime?.WindowGetSize || !runtime.WindowGetPosition || !runtime.WindowIsMaximised) return; |
| 63 | const { WindowGetSize, WindowGetPosition, WindowIsMaximised } = runtime; |
| 64 | |
| 65 | let timer: ReturnType<typeof setInterval>; |
| 66 | const save = createWindowStateSaver( |
| 67 | { WindowGetSize, WindowGetPosition, WindowIsMaximised }, |
| 68 | (state) => app.SaveWindowState(state), |
| 69 | ); |
| 70 | |
| 71 | // Debounced save on resize (500ms after the last resize event). |
| 72 | let debounce: ReturnType<typeof setTimeout>; |
| 73 | const onResize = () => { |
| 74 | clearTimeout(debounce); |
| 75 | debounce = setTimeout(save, 500); |
| 76 | }; |
| 77 | window.addEventListener("resize", onResize); |
| 78 | |
| 79 | // Periodic poll every 5s for moves/maximise that don't trigger resize. |
| 80 | timer = setInterval(save, 5000); |
| 81 | |
| 82 | // Best-effort save before the page unloads. Go re-persists the last |
| 83 | // accepted report during shutdown without querying the native window. |
| 84 | const onBeforeUnload = () => { void save(); }; |
| 85 | window.addEventListener("beforeunload", onBeforeUnload); |
| 86 | |
| 87 | return () => { |
| 88 | clearInterval(timer); |
| 89 | clearTimeout(debounce); |
| 90 | window.removeEventListener("resize", onResize); |
| 91 | window.removeEventListener("beforeunload", onBeforeUnload); |
| 92 | }; |
| 93 | }, []); |
| 94 | } |
| 95 | |
| 96 | export function useViewportHeightVar() { |
| 97 | useEffect(() => { |
| 98 | if (typeof window === "undefined" || typeof document === "undefined") return; |
| 99 | |
| 100 | let frame = 0; |
| 101 | const root = document.documentElement; |
| 102 | const setHeight = () => { |
| 103 | frame = 0; |
| 104 | const height = Math.round(window.visualViewport?.height ?? window.innerHeight); |
| 105 | if (height > 0) root.style.setProperty("--app-viewport-height", `${height}px`); |
| 106 | }; |
| 107 | const schedule = () => { |
| 108 | if (frame) window.cancelAnimationFrame(frame); |
| 109 | frame = window.requestAnimationFrame(setHeight); |
| 110 | }; |
| 111 | |
| 112 | schedule(); |
| 113 | window.addEventListener("resize", schedule); |
| 114 | window.addEventListener("orientationchange", schedule); |
| 115 | document.addEventListener("fullscreenchange", schedule); |
| 116 | window.visualViewport?.addEventListener("resize", schedule); |
| 117 | |
| 118 | return () => { |
| 119 | if (frame) window.cancelAnimationFrame(frame); |
| 120 | window.removeEventListener("resize", schedule); |
| 121 | window.removeEventListener("orientationchange", schedule); |
| 122 | document.removeEventListener("fullscreenchange", schedule); |
| 123 | window.visualViewport?.removeEventListener("resize", schedule); |
| 124 | }; |
| 125 | }, []); |
| 126 | } |
| 127 |