Skip to content

Commit dff8784

Browse files
window controls overlay (windows&linux) (#1969)
Co-authored-by: julius <julius0216@outlook.com>
1 parent 96c9306 commit dff8784

10 files changed

Lines changed: 127 additions & 10 deletions

File tree

apps/desktop/src/main.ts

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as Path from "node:path";
77
import {
88
app,
99
BrowserWindow,
10+
type BrowserWindowConstructorOptions,
1011
clipboard,
1112
dialog,
1213
ipcMain,
@@ -118,6 +119,15 @@ const DESKTOP_UPDATE_CHANNEL = "latest";
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 {
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 {
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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -313,13 +313,15 @@ type ChatViewProps =
313313
environmentId: EnvironmentId;
314314
threadId: ThreadId;
315315
onDiffPanelOpen?: () => void;
316+
reserveTitleBarControlInset?: boolean;
316317
routeKind: "server";
317318
draftId?: never;
318319
}
319320
| {
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
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) {
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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ export type DiffPanelMode = "inline" | "sheet" | "sidebar";
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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,15 @@ export function NoActiveThreadState() {
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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1974,7 +1974,7 @@ const SidebarChromeHeader = memo(function SidebarChromeHeader({
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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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;
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,17 @@ import "./index.css";
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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,7 @@ function ChatThreadRouteView() {
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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ function SettingsContentLayout() {
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>

0 commit comments

Comments
 (0)