History

window controls overlay (windows&linux) (#1969)

Taras committed 7 months ago

Commit dff87841 parent 96c930610 files · +127 −10Browse files

Unified · Split

10 files
  1. apps/desktop/src/main.ts +51 −2
  2. apps/web/src/components/ChatView.tsx +16 −2
  3. apps/web/src/components/DiffPanelShell.tsx +4 −2
  4. apps/web/src/components/NoActiveThreadState.tsx +6 −2
  5. apps/web/src/components/Sidebar.tsx +1 −1
  6. apps/web/src/index.css +2 −0
  7. apps/web/src/lib/windowControlsOverlay.ts +40 −0
  8. apps/web/src/main.tsx +5 −0
  9. apps/web/src/routes/_chat.$environmentId.$threadId.tsx +1 −0
  10. apps/web/src/routes/settings.tsx +1 −1
apps/desktop/src/main.ts+51−2
@@ -7,6 +7,7 @@ import * as Path from "node:path"; · 6 unmodified lines
77 import {
88 app,
99 BrowserWindow,
10+ type BrowserWindowConstructorOptions,
1011 clipboard,
1112 dialog,
1213 ipcMain,
@@ -118,6 +119,15 @@ const DESKTOP_UPDATE_CHANNEL = "latest"; · 105 unmodified lines
118119 const DESKTOP_UPDATE_ALLOW_PRERELEASE = false;
119120 const DESKTOP_LOOPBACK_HOST = "127.0.0.1";
120121 const DESKTOP_REQUIRED_PORT_PROBE_HOSTS = ["0.0.0.0", "::"] as const;
122+const TITLEBAR_HEIGHT = 40;
123+const TITLEBAR_COLOR = "#01000000"; // #00000000 does not work correctly on Linux
124+const TITLEBAR_LIGHT_SYMBOL_COLOR = "#1f2937";
125+const TITLEBAR_DARK_SYMBOL_COLOR = "#f8fafc";
126+
127+type WindowTitleBarOptions = Pick<
128+ BrowserWindowConstructorOptions,
129+ "titleBarOverlay" | "titleBarStyle" | "trafficLightPosition"
130+>;
121131
122132 type DesktopUpdateErrorContext = DesktopUpdateState["errorContext"];
123133 type LinuxDesktopNamedApp = Electron.App & {
@@ -1649,6 +1659,46 @@ function getInitialWindowBackgroundColor(): string { · 1525 unmodified lines
16491659 return nativeTheme.shouldUseDarkColors ? "#0a0a0a" : "#ffffff";
16501660 }
16511661
1662+function getWindowTitleBarOptions(): WindowTitleBarOptions {
1663+ if (process.platform === "darwin") {
1664+ return {
1665+ titleBarStyle: "hiddenInset",
1666+ trafficLightPosition: { x: 16, y: 18 },
1667+ };
1668+ }
1669+
1670+ return {
1671+ titleBarStyle: "hidden",
1672+ titleBarOverlay: {
1673+ color: TITLEBAR_COLOR,
1674+ height: TITLEBAR_HEIGHT,
1675+ symbolColor: nativeTheme.shouldUseDarkColors
1676+ ? TITLEBAR_DARK_SYMBOL_COLOR
1677+ : TITLEBAR_LIGHT_SYMBOL_COLOR,
1678+ },
1679+ };
1680+}
1681+
1682+function syncWindowAppearance(window: BrowserWindow): void {
1683+ if (window.isDestroyed()) {
1684+ return;
1685+ }
1686+
1687+ window.setBackgroundColor(getInitialWindowBackgroundColor());
1688+ const { titleBarOverlay } = getWindowTitleBarOptions();
1689+ if (typeof titleBarOverlay === "object") {
1690+ window.setTitleBarOverlay(titleBarOverlay);
1691+ }
1692+}
1693+
1694+function syncAllWindowAppearance(): void {
1695+ for (const window of BrowserWindow.getAllWindows()) {
1696+ syncWindowAppearance(window);
1697+ }
1698+}
1699+
1700+nativeTheme.on("updated", syncAllWindowAppearance);
1701+
16521702 function createWindow(): BrowserWindow {
16531703 const window = new BrowserWindow({
16541704 width: 1100,
@@ -1660,8 +1710,7 @@ function createWindow(): BrowserWindow { · 5 unmodified lines
16601710 backgroundColor: getInitialWindowBackgroundColor(),
16611711 ...getIconOption(),
16621712 title: APP_DISPLAY_NAME,
1663− titleBarStyle: "hiddenInset",
1664− trafficLightPosition: { x: 16, y: 18 },
1713+ ...getWindowTitleBarOptions(),
16651714 webPreferences: {
16661715 preload: Path.join(__dirname, "preload.js"),
16671716 contextIsolation: true,
apps/web/src/components/ChatView.tsx+16−2
@@ -313,6 +313,7 @@ type ChatViewProps = · 312 unmodified lines
313313 environmentId: EnvironmentId;
314314 threadId: ThreadId;
315315 onDiffPanelOpen?: () => void;
316+ reserveTitleBarControlInset?: boolean;
316317 routeKind: "server";
317318 draftId?: never;
318319 }
@@ -320,6 +321,7 @@ type ChatViewProps = · 1 unmodified line
320321 environmentId: EnvironmentId;
321322 threadId: ThreadId;
322323 onDiffPanelOpen?: () => void;
324+ reserveTitleBarControlInset?: boolean;
323325 routeKind: "draft";
324326 draftId: DraftId;
325327 };
@@ -573,7 +575,13 @@ const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDra · 247 unmodified lines
573575 });
574576
575577 export default function ChatView(props: ChatViewProps) {
576− const { environmentId, threadId, routeKind, onDiffPanelOpen } = props;
578+ const {
579+ environmentId,
580+ threadId,
581+ routeKind,
582+ onDiffPanelOpen,
583+ reserveTitleBarControlInset = true,
584+ } = props;
577585 const draftId = routeKind === "draft" ? props.draftId : null;
578586 const routeThreadRef = useMemo(
579587 () => scopeThreadRef(environmentId, threadId),
@@ -3117,7 +3125,13 @@ export default function ChatView(props: ChatViewProps) { · 2537 unmodified lines
31173125 <header
31183126 className={cn(
31193127 "border-b border-border px-3 sm:px-5",
3120− isElectron ? "drag-region flex h-[52px] items-center" : "py-2 sm:py-3",
3128+ isElectron
3129+ ? cn(
3130+ "drag-region flex h-[52px] items-center wco:h-[env(titlebar-area-height)]",
3131+ reserveTitleBarControlInset &&
3132+ "wco:pr-[calc(100vw-env(titlebar-area-width)-env(titlebar-area-x)+1em)]",
3133+ )
3134+ : "py-2 sm:py-3",
31213135 )}
31223136 >
31233137 <ChatHeader
apps/web/src/components/DiffPanelShell.tsx+4−2
@@ -10,8 +10,10 @@ export type DiffPanelMode = "inline" | "sheet" | "sidebar"; · 9 unmodified lines
1010 function getDiffPanelHeaderRowClassName(mode: DiffPanelMode) {
1111 const shouldUseDragRegion = isElectron && mode !== "sheet";
1212 return cn(
13− "flex items-center justify-between gap-2 px-4",
14− shouldUseDragRegion ? "drag-region h-[52px] border-b border-border" : "h-12",
13+ "flex items-center justify-between gap-2 px-4 wco:pr-[calc(100vw-env(titlebar-area-width)-env(titlebar-area-x)+1em)]",
14+ shouldUseDragRegion
15+ ? "drag-region h-[52px] border-b border-border wco:h-[env(titlebar-area-height)]"
16+ : "h-12 wco:max-h-[env(titlebar-area-height)]",
1517 );
1618 }
1719
apps/web/src/components/NoActiveThreadState.tsx+6−2
@@ -10,11 +10,15 @@ export function NoActiveThreadState() { · 9 unmodified lines
1010 <header
1111 className={cn(
1212 "border-b border-border px-3 sm:px-5",
13− isElectron ? "drag-region flex h-[52px] items-center" : "py-2 sm:py-3",
13+ isElectron
14+ ? "drag-region flex h-[52px] items-center wco:h-[env(titlebar-area-height)]"
15+ : "py-2 sm:py-3",
1416 )}
1517 >
1618 {isElectron ? (
17− <span className="text-xs text-muted-foreground/50">No active thread</span>
19+ <span className="text-xs text-muted-foreground/50 wco:pr-[calc(100vw-env(titlebar-area-width)-env(titlebar-area-x)+1em)]">
20+ No active thread
21+ </span>
1822 ) : (
1923 <div className="flex items-center gap-2">
2024 <SidebarTrigger className="size-7 shrink-0 md:hidden" />
apps/web/src/components/Sidebar.tsx+1−1
@@ -1974,7 +1974,7 @@ const SidebarChromeHeader = memo(function SidebarChromeHeader({ · 1973 unmodified lines
19741974 );
19751975
19761976 return isElectron ? (
1977− <SidebarHeader className="drag-region h-[52px] flex-row items-center gap-2 px-4 py-0 pl-[90px]">
1977+ <SidebarHeader className="drag-region h-[52px] flex-row items-center gap-2 px-4 py-0 pl-[90px] wco:h-[env(titlebar-area-height)] wco:pl-[calc(env(titlebar-area-x)+1em)]">
19781978 {wordmark}
19791979 </SidebarHeader>
19801980 ) : (
apps/web/src/index.css+2−0
@@ -1,6 +1,8 @@
11 @import "tailwindcss";
22
33 @custom-variant dark (&:is(.dark, .dark *));
4+/* Window Controls Overlay: active when Electron exposes native titlebar control geometry. */
5+@custom-variant wco (&:is(.wco, .wco *));
46
57 @theme inline {
68 --animate-skeleton: skeleton 2s -1s infinite linear;
apps/web/src/lib/windowControlsOverlay.ts+40−0
@@ -0,0 +1,40 @@
1+const WCO_CLASS_NAME = "wco";
2+
3+interface WindowControlsOverlayLike {
4+ readonly visible: boolean;
5+ addEventListener(type: "geometrychange", listener: EventListener): void;
6+ removeEventListener(type: "geometrychange", listener: EventListener): void;
7+}
8+
9+interface NavigatorWithWindowControlsOverlay extends Navigator {
10+ readonly windowControlsOverlay?: WindowControlsOverlayLike;
11+}
12+
13+function getWindowControlsOverlay(): WindowControlsOverlayLike | null {
14+ if (typeof navigator === "undefined") {
15+ return null;
16+ }
17+
18+ return (navigator as NavigatorWithWindowControlsOverlay).windowControlsOverlay ?? null;
19+}
20+
21+export function syncDocumentWindowControlsOverlayClass(): () => void {
22+ if (typeof document === "undefined") {
23+ return () => {};
24+ }
25+
26+ const overlay = getWindowControlsOverlay();
27+ const update = () => {
28+ document.documentElement.classList.toggle(WCO_CLASS_NAME, overlay !== null && overlay.visible);
29+ };
30+
31+ update();
32+ if (!overlay) {
33+ return () => {};
34+ }
35+
36+ overlay.addEventListener("geometrychange", update);
37+ return () => {
38+ overlay.removeEventListener("geometrychange", update);
39+ };
40+}
apps/web/src/main.tsx+5−0
@@ -9,12 +9,17 @@ import "./index.css"; · 8 unmodified lines
99 import { isElectron } from "./env";
1010 import { getRouter } from "./router";
1111 import { APP_DISPLAY_NAME } from "./branding";
12+import { syncDocumentWindowControlsOverlayClass } from "./lib/windowControlsOverlay";
1213
1314 // Electron loads the app from a file-backed shell, so hash history avoids path resolution issues.
1415 const history = isElectron ? createHashHistory() : createBrowserHistory();
1516
1617 const router = getRouter(history);
1718
19+if (isElectron) {
20+ syncDocumentWindowControlsOverlayClass();
21+}
22+
1823 document.title = APP_DISPLAY_NAME;
1924
2025 ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
apps/web/src/routes/_chat.$environmentId.$threadId.tsx+1−0
@@ -269,6 +269,7 @@ function ChatThreadRouteView() { · 268 unmodified lines
269269 environmentId={threadRef.environmentId}
270270 threadId={threadRef.threadId}
271271 onDiffPanelOpen={markDiffOpened}
272+ reserveTitleBarControlInset={!diffOpen}
272273 routeKind="server"
273274 />
274275 </SidebarInset>
apps/web/src/routes/settings.tsx+1−1
@@ -56,7 +56,7 @@ function SettingsContentLayout() { · 55 unmodified lines
5656 )}
5757
5858 {isElectron && (
59− <div className="drag-region flex h-[52px] shrink-0 items-center border-b border-border px-5">
59+ <div className="drag-region flex h-[52px] shrink-0 items-center border-b border-border px-5 wco:h-[env(titlebar-area-height)] wco:pr-[calc(100vw-env(titlebar-area-width)-env(titlebar-area-x)+1em)]">
6060 <span className="text-xs font-medium tracking-wide text-muted-foreground/70">
6161 Settings
6262 </span>