1import { useSyncExternalStore } from "react";
2
3import { isElectron } from "~/env";
4
5import { isMacPlatform, isWindowsPlatform } from "./utils";
6
7const WCO_CLASS_NAME = "wco";
8const ELECTRON_CLASS_NAME = "electron";
9const ELECTRON_WINDOWS_CLASS_NAME = "electron-windows";
10// The workspace topbar the desktop app centers the traffic lights in. macOS has
11// no overlay API to measure, and the lights stay inside it at any zoom level.
12const MACOS_TITLEBAR_HEIGHT = 52;
13
14interface WindowControlsOverlayLike {
15 readonly visible: boolean;
16 getTitlebarAreaRect(): DOMRect;
17 addEventListener(type: "geometrychange", listener: EventListener): void;
18 removeEventListener(type: "geometrychange", listener: EventListener): void;
19}
20
21interface NavigatorWithWindowControlsOverlay extends Navigator {
22 readonly windowControlsOverlay?: WindowControlsOverlayLike;
23}
24
25function getWindowControlsOverlay(): WindowControlsOverlayLike | null {
26 if (typeof navigator === "undefined") {
27 return null;
28 }
29
30 return (navigator as NavigatorWithWindowControlsOverlay).windowControlsOverlay ?? null;
31}
32
33export function syncDocumentWindowControlsOverlayClass(): () => void {
34 if (typeof document === "undefined") {
35 return () => {};
36 }
37
38 const overlay = getWindowControlsOverlay();
39 const update = () => {
40 document.documentElement.classList.toggle(WCO_CLASS_NAME, overlay !== null && overlay.visible);
41 };
42
43 update();
44 if (!overlay) {
45 return () => {};
46 }
47
48 overlay.addEventListener("geometrychange", update);
49 return () => {
50 overlay.removeEventListener("geometrychange", update);
51 };
52}
53
54function subscribeTitlebarArea(listener: () => void): () => void {
55 const overlay = getWindowControlsOverlay();
56 overlay?.addEventListener("geometrychange", listener);
57 return () => overlay?.removeEventListener("geometrychange", listener);
58}
59
60function getNativeTitlebarHeight(): number {
61 if (isElectron && isMacPlatform(navigator.platform)) return MACOS_TITLEBAR_HEIGHT;
62 const overlay = getWindowControlsOverlay();
63 return overlay?.visible ? overlay.getTitlebarAreaRect().bottom : 0;
64}
65
66/** Height of the strip at the top of the window where native window controls paint over the page. */
67export function useNativeTitlebarHeight(): number {
68 return useSyncExternalStore(subscribeTitlebarArea, getNativeTitlebarHeight, () => 0);
69}
70
71function getElectronPlatformClassNames(
72 platform: string,
73):
74 | readonly [typeof ELECTRON_CLASS_NAME]
75 | readonly [typeof ELECTRON_CLASS_NAME, typeof ELECTRON_WINDOWS_CLASS_NAME] {
76 return isWindowsPlatform(platform)
77 ? [ELECTRON_CLASS_NAME, ELECTRON_WINDOWS_CLASS_NAME]
78 : [ELECTRON_CLASS_NAME];
79}
80
81export function syncDocumentElectronPlatformClasses(platform: string): () => void {
82 if (typeof document === "undefined") {
83 return () => {};
84 }
85
86 const classNames = getElectronPlatformClassNames(platform);
87 document.documentElement.classList.add(...classNames);
88 return () => {
89 document.documentElement.classList.remove(...classNames);
90 };
91}