返回 DeepSeek-Reasonix
useBrowserSurfaceLayout.ts
根目录 / desktop / frontend / src / lib / useBrowserSurfaceLayout.ts
1 import { useEffect } from "react";
2
3 import type { BrowserLayoutRect, DesktopBrowserHost } from "./browserHost";
4
5 export type BrowserSurfaceHost = Pick<DesktopBrowserHost, "setLayout" | "setOverlay">;
6 type View = Window & typeof globalThis;
7
8 /** Marks a portaled application overlay root; while one exists every native website view is hidden. */
9 export const APP_OVERLAY_SELECTOR = "[data-app-overlay]";
10
11 const sameRect = (a: BrowserLayoutRect, b: BrowserLayoutRect) =>
12 a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
13
14 /** Reports the surface's viewport rectangle to the shell until detached, then reports null. */
15 export function attachBrowserSurfaceLayout(host: Pick<DesktopBrowserHost, "setLayout">, surface: HTMLElement, view: View = window): () => void {
16 let frame = 0;
17 let last: BrowserLayoutRect | null = null;
18 const measure = () => {
19 frame = 0;
20 const box = surface.getBoundingClientRect();
21 const rect = { x: Math.round(box.left), y: Math.round(box.top), width: Math.round(box.width), height: Math.round(box.height) };
22 if (last && sameRect(last, rect)) return;
23 last = rect;
24 host.setLayout(rect);
25 };
26 const schedule = () => {
27 if (!frame) frame = view.requestAnimationFrame(measure);
28 };
29 const observer = typeof view.ResizeObserver === "function" ? new view.ResizeObserver(schedule) : null;
30 observer?.observe(surface);
31 const scrollers: EventTarget[] = [view];
32 for (let node = surface.parentElement; node; node = node.parentElement) scrollers.push(node);
33 for (const scroller of scrollers) scroller.addEventListener("scroll", schedule, { passive: true });
34 view.addEventListener("resize", schedule);
35 view.visualViewport?.addEventListener("resize", schedule);
36 view.visualViewport?.addEventListener("scroll", schedule);
37 measure();
38 return () => {
39 if (frame) view.cancelAnimationFrame(frame);
40 observer?.disconnect();
41 for (const scroller of scrollers) scroller.removeEventListener("scroll", schedule);
42 view.removeEventListener("resize", schedule);
43 view.visualViewport?.removeEventListener("resize", schedule);
44 view.visualViewport?.removeEventListener("scroll", schedule);
45 host.setLayout(null);
46 };
47 }
48
49 const carriesOverlay = (node: Node) =>
50 node.nodeType === 1 && ((node as Element).matches(APP_OVERLAY_SELECTOR) || (node as Element).querySelector(APP_OVERLAY_SELECTOR) !== null);
51
52 /** Mirrors the presence of any `[data-app-overlay]` element into the shell's overlay state. */
53 export function attachAppOverlayGate(host: Pick<DesktopBrowserHost, "setOverlay">, view: View = window): () => void {
54 const document = view.document;
55 let frame = 0;
56 let last: boolean | null = null;
57 const check = () => {
58 frame = 0;
59 const active = document.querySelector(APP_OVERLAY_SELECTOR) !== null;
60 if (active === last) return;
61 last = active;
62 host.setOverlay(active);
63 };
64 const schedule = () => {
65 if (!frame) frame = view.requestAnimationFrame(check);
66 };
67 // Transcript streaming mutates the DOM constantly; only records that can
68 // change overlay presence are allowed to schedule the document query.
69 const observer = new view.MutationObserver((records) => {
70 if (records.some((record) => record.type === "attributes"
71 || [...record.addedNodes, ...record.removedNodes].some(carriesOverlay))) schedule();
72 });
73 observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["data-app-overlay"] });
74 check();
75 return () => {
76 if (frame) view.cancelAnimationFrame(frame);
77 observer.disconnect();
78 if (last) host.setOverlay(false);
79 };
80 }
81
82 export function useBrowserSurfaceLayout(host: BrowserSurfaceHost | undefined, surface: HTMLElement | null): void {
83 useEffect(() => (host && surface ? attachBrowserSurfaceLayout(host, surface) : undefined), [host, surface]);
84 useEffect(() => (host ? attachAppOverlayGate(host) : undefined), [host]);
85 }
86
86 lines TYPESCRIPT