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