188 lines
4.9 KiB
TypeScript
188 lines
4.9 KiB
TypeScript
import { useEffect, useMemo, useState } from "react";
|
|
import {
|
|
getIsElectronRuntimeMac,
|
|
getIsElectronRuntime,
|
|
DESKTOP_TRAFFIC_LIGHT_WIDTH,
|
|
DESKTOP_TRAFFIC_LIGHT_HEIGHT,
|
|
DESKTOP_WINDOW_CONTROLS_WIDTH,
|
|
DESKTOP_WINDOW_CONTROLS_HEIGHT,
|
|
} from "@/constants/layout";
|
|
import { getDesktopWindow } from "@/desktop/electron/window";
|
|
import { usePanelStore } from "@/stores/panel-store";
|
|
import { isNative } from "@/constants/platform";
|
|
|
|
interface RawWindowControlsPadding {
|
|
left: number;
|
|
right: number;
|
|
top: number;
|
|
}
|
|
|
|
type WindowControlsPaddingRole =
|
|
| "sidebar"
|
|
| "header"
|
|
| "detailHeader"
|
|
| "tabRow"
|
|
| "explorerSidebar";
|
|
|
|
// Module-level cache so hook remounts (e.g., on navigation) don't briefly
|
|
// fall back to the default `false` while the async fullscreen check resolves.
|
|
// Without this, in fullscreen the sidebar flashes with traffic-light padding
|
|
// on first frame and then snaps to 0 once the async read completes.
|
|
let cachedIsFullscreen = false;
|
|
const fullscreenSubscribers = new Set<(value: boolean) => void>();
|
|
let fullscreenSubscriptionStarted = false;
|
|
|
|
function setCachedFullscreen(value: boolean) {
|
|
if (cachedIsFullscreen === value) return;
|
|
cachedIsFullscreen = value;
|
|
for (const sub of fullscreenSubscribers) {
|
|
sub(value);
|
|
}
|
|
}
|
|
|
|
function startFullscreenSubscription() {
|
|
if (fullscreenSubscriptionStarted) return;
|
|
if (isNative || !getIsElectronRuntime()) return;
|
|
fullscreenSubscriptionStarted = true;
|
|
|
|
void (async () => {
|
|
const win = getDesktopWindow();
|
|
if (!win) return;
|
|
|
|
if (typeof win.isFullscreen === "function") {
|
|
try {
|
|
setCachedFullscreen(await win.isFullscreen());
|
|
} catch (error) {
|
|
console.warn("[DesktopWindow] Failed to read fullscreen state", error);
|
|
}
|
|
}
|
|
|
|
if (typeof win.onResized !== "function") return;
|
|
|
|
try {
|
|
await win.onResized(async () => {
|
|
if (typeof win.isFullscreen !== "function") return;
|
|
try {
|
|
setCachedFullscreen(await win.isFullscreen());
|
|
} catch (error) {
|
|
console.warn("[DesktopWindow] Failed to read fullscreen state", error);
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.warn("[DesktopWindow] Failed to subscribe to resize", error);
|
|
}
|
|
})();
|
|
}
|
|
|
|
function useRawWindowControlsPadding(): RawWindowControlsPadding {
|
|
const [isFullscreen, setIsFullscreen] = useState(cachedIsFullscreen);
|
|
|
|
useEffect(() => {
|
|
startFullscreenSubscription();
|
|
// Sync to any value that resolved between render and effect.
|
|
setIsFullscreen(cachedIsFullscreen);
|
|
fullscreenSubscribers.add(setIsFullscreen);
|
|
return () => {
|
|
fullscreenSubscribers.delete(setIsFullscreen);
|
|
};
|
|
}, []);
|
|
|
|
return resolveRawWindowControlsPadding({
|
|
isElectron: getIsElectronRuntime(),
|
|
isMac: getIsElectronRuntimeMac(),
|
|
isFullscreen,
|
|
});
|
|
}
|
|
|
|
export function resolveRawWindowControlsPadding(input: {
|
|
isElectron: boolean;
|
|
isMac: boolean;
|
|
isFullscreen: boolean;
|
|
}): RawWindowControlsPadding {
|
|
if (!input.isElectron || input.isFullscreen) {
|
|
return { left: 0, right: 0, top: 0 };
|
|
}
|
|
|
|
if (input.isMac) {
|
|
return {
|
|
left: DESKTOP_TRAFFIC_LIGHT_WIDTH,
|
|
right: 0,
|
|
top: DESKTOP_TRAFFIC_LIGHT_HEIGHT,
|
|
};
|
|
}
|
|
|
|
return {
|
|
left: 0,
|
|
right: DESKTOP_WINDOW_CONTROLS_WIDTH,
|
|
top: DESKTOP_WINDOW_CONTROLS_HEIGHT,
|
|
};
|
|
}
|
|
|
|
export function useWindowControlsPadding(role: WindowControlsPaddingRole): {
|
|
left: number;
|
|
right: number;
|
|
top: number;
|
|
} {
|
|
const sidebarOpen = usePanelStore((state) => state.desktop.agentListOpen);
|
|
const explorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen);
|
|
const focusModeEnabled = usePanelStore((state) => state.desktop.focusModeEnabled);
|
|
const rawPadding = useRawWindowControlsPadding();
|
|
const sidebarClosed = !sidebarOpen;
|
|
|
|
const { left, right, top } = resolveWindowControlsPadding({
|
|
role,
|
|
rawPadding,
|
|
sidebarClosed,
|
|
explorerOpen,
|
|
focusModeEnabled,
|
|
});
|
|
|
|
return useMemo(() => ({ left, right, top }), [left, right, top]);
|
|
}
|
|
|
|
export function resolveWindowControlsPadding(input: {
|
|
role: WindowControlsPaddingRole;
|
|
rawPadding: RawWindowControlsPadding;
|
|
sidebarClosed: boolean;
|
|
explorerOpen: boolean;
|
|
focusModeEnabled: boolean;
|
|
}): RawWindowControlsPadding {
|
|
if (input.role === "sidebar") {
|
|
return {
|
|
left: input.rawPadding.left,
|
|
right: 0,
|
|
top: input.rawPadding.top,
|
|
};
|
|
}
|
|
|
|
if (input.role === "header") {
|
|
return {
|
|
left: input.sidebarClosed ? input.rawPadding.left : 0,
|
|
right: input.explorerOpen ? 0 : input.rawPadding.right,
|
|
top: 0,
|
|
};
|
|
}
|
|
|
|
if (input.role === "detailHeader") {
|
|
return {
|
|
left: 0,
|
|
right: input.rawPadding.right,
|
|
top: 0,
|
|
};
|
|
}
|
|
|
|
if (input.role === "tabRow") {
|
|
return {
|
|
left: input.focusModeEnabled ? input.rawPadding.left : 0,
|
|
right: input.focusModeEnabled ? input.rawPadding.right : 0,
|
|
top: 0,
|
|
};
|
|
}
|
|
|
|
return {
|
|
left: 0,
|
|
right: input.rawPadding.right,
|
|
top: 0,
|
|
};
|
|
}
|