Skip to content

Commit f349184

Browse files
tarik02juliusmarminge
authored andcommitted
window controls overlay (windows&linux) (pingdotgg#1969)
Co-authored-by: julius <julius0216@outlook.com>
1 parent 8d43271 commit f349184

10 files changed

Lines changed: 134 additions & 13 deletions

File tree

apps/desktop/src/main.ts

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ declare const __EMBEDDED_MARCODE_JIRA_TOKEN_PROXY_URL__: string;
1010
import {
1111
app,
1212
BrowserWindow,
13+
type BrowserWindowConstructorOptions,
1314
clipboard,
1415
dialog,
1516
ipcMain,
@@ -154,6 +155,16 @@ function normalizeContextMenuItems(source: readonly ContextMenuItem[]): ContextM
154155
return normalizedItems;
155156
}
156157

158+
const TITLEBAR_HEIGHT = 40;
159+
const TITLEBAR_COLOR = "#01000000"; // #00000000 does not work correctly on Linux
160+
const TITLEBAR_LIGHT_SYMBOL_COLOR = "#1f2937";
161+
const TITLEBAR_DARK_SYMBOL_COLOR = "#f8fafc";
162+
163+
type WindowTitleBarOptions = Pick<
164+
BrowserWindowConstructorOptions,
165+
"titleBarOverlay" | "titleBarStyle" | "trafficLightPosition"
166+
>;
167+
157168
type DesktopUpdateErrorContext = DesktopUpdateState["errorContext"];
158169
type LinuxDesktopNamedApp = Electron.App & {
159170
setDesktopName?: (desktopName: string) => void;
@@ -1716,6 +1727,46 @@ function saveWindowState(window: BrowserWindow): void {
17161727
writeWindowState(WINDOW_STATE_PATH, lastWindowState);
17171728
}
17181729

1730+
function getWindowTitleBarOptions(): WindowTitleBarOptions {
1731+
if (process.platform === "darwin") {
1732+
return {
1733+
titleBarStyle: "hiddenInset",
1734+
trafficLightPosition: { x: 16, y: 18 },
1735+
};
1736+
}
1737+
1738+
return {
1739+
titleBarStyle: "hidden",
1740+
titleBarOverlay: {
1741+
color: TITLEBAR_COLOR,
1742+
height: TITLEBAR_HEIGHT,
1743+
symbolColor: nativeTheme.shouldUseDarkColors
1744+
? TITLEBAR_DARK_SYMBOL_COLOR
1745+
: TITLEBAR_LIGHT_SYMBOL_COLOR,
1746+
},
1747+
};
1748+
}
1749+
1750+
function syncWindowAppearance(window: BrowserWindow): void {
1751+
if (window.isDestroyed()) {
1752+
return;
1753+
}
1754+
1755+
window.setBackgroundColor(getInitialWindowBackgroundColor());
1756+
const { titleBarOverlay } = getWindowTitleBarOptions();
1757+
if (typeof titleBarOverlay === "object") {
1758+
window.setTitleBarOverlay(titleBarOverlay);
1759+
}
1760+
}
1761+
1762+
function syncAllWindowAppearance(): void {
1763+
for (const window of BrowserWindow.getAllWindows()) {
1764+
syncWindowAppearance(window);
1765+
}
1766+
}
1767+
1768+
nativeTheme.on("updated", syncAllWindowAppearance);
1769+
17191770
function createWindow(options?: { deferLoad?: boolean }): BrowserWindow {
17201771
const restoredBounds = resolveWindowBounds(lastWindowState);
17211772

@@ -1728,8 +1779,7 @@ function createWindow(options?: { deferLoad?: boolean }): BrowserWindow {
17281779
backgroundColor: getInitialWindowBackgroundColor(),
17291780
...getIconOption(),
17301781
title: APP_DISPLAY_NAME,
1731-
titleBarStyle: "hiddenInset",
1732-
trafficLightPosition: { x: 16, y: 18 },
1782+
...getWindowTitleBarOptions(),
17331783
webPreferences: {
17341784
preload: Path.join(__dirname, "preload.js"),
17351785
contextIsolation: true,

apps/web/src/components/ChatView.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,7 @@ interface SubmitComposerTurnInput {
464464
interface ChatViewProps {
465465
threadId: ThreadId;
466466
environmentId?: EnvironmentId;
467+
reserveTitleBarControlInset?: boolean;
467468
}
468469

469470
interface TerminalLaunchContext {
@@ -723,7 +724,11 @@ function PersistentThreadTerminalDrawer({
723724
);
724725
}
725726

726-
export default function ChatView({ threadId, environmentId: environmentIdProp }: ChatViewProps) {
727+
export default function ChatView({
728+
threadId,
729+
environmentId: environmentIdProp,
730+
reserveTitleBarControlInset = true,
731+
}: ChatViewProps) {
727732
const { isMobile, state: sidebarState } = useSidebar();
728733
const sidebarVisible = !isMobile && sidebarState === "expanded";
729734
const primaryEnvironmentId = usePrimaryEnvironmentId();
@@ -4845,7 +4850,13 @@ export default function ChatView({ threadId, environmentId: environmentIdProp }:
48454850
className={cn(
48464851
"border-b border-border",
48474852
isElectron && !sidebarVisible ? "pr-3 sm:pr-5 pl-[90px]" : "px-3 sm:px-5",
4848-
isElectron ? "drag-region flex h-[52px] items-center" : "py-2 sm:py-3",
4853+
isElectron
4854+
? cn(
4855+
"drag-region flex h-[52px] items-center wco:h-[env(titlebar-area-height)]",
4856+
reserveTitleBarControlInset &&
4857+
"wco:pr-[calc(100vw-env(titlebar-area-width)-env(titlebar-area-x)+1em)]",
4858+
)
4859+
: "py-2 sm:py-3",
48494860
)}
48504861
>
48514862
<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: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ import { usePrimaryEnvironmentId } from "../environments/primary";
6464
import { isElectron } from "../env";
6565
import { APP_BASE_NAME, APP_STAGE_LABEL, APP_VERSION } from "../branding";
6666
import { isTerminalFocused } from "../lib/terminalFocus";
67-
import { isMacPlatform, newCommandId, newProjectId } from "../lib/utils";
67+
import { cn, isMacPlatform, newCommandId, newProjectId } from "../lib/utils";
6868
import {
6969
selectBootstrapCompleteForActiveEnvironment,
7070
selectProjectByRef,
@@ -2370,8 +2370,11 @@ const SidebarChromeHeader = memo(function SidebarChromeHeader({
23702370

23712371
return isElectron ? (
23722372
<SidebarHeader
2373-
className="drag-region h-[52px] flex-row items-center justify-center gap-2 py-0"
2374-
style={isDesktopFullscreen ? undefined : { paddingLeft: 58 }}
2373+
className={cn(
2374+
"drag-region h-[52px] flex-row items-center justify-center gap-2 py-0",
2375+
!isDesktopFullscreen && "pl-[58px]",
2376+
"wco:h-[env(titlebar-area-height)] wco:pl-[calc(env(titlebar-area-x)+1em)]",
2377+
)}
23752378
>
23762379
{wordmark}
23772380
</SidebarHeader>

apps/web/src/index.css

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
}
2626

2727
@custom-variant dark (&:is(.dark, .dark *));
28+
/* Window Controls Overlay: active when Electron exposes native titlebar control geometry. */
29+
@custom-variant wco (&:is(.wco, .wco *));
2830

2931
@theme inline {
3032
--font-heading: "Klaster Sans", sans-serif;
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: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,11 @@ function ChatThreadRouteView() {
334334
return (
335335
<>
336336
<SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground">
337-
<ChatView threadId={threadRef.threadId} environmentId={threadRef.environmentId} />
337+
<ChatView
338+
threadId={threadRef.threadId}
339+
environmentId={threadRef.environmentId}
340+
reserveTitleBarControlInset={!diffOpen}
341+
/>
338342
</SidebarInset>
339343
<DiffPanelInlineSidebar
340344
diffOpen={diffOpen}

apps/web/src/routes/settings.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ function SettingsContentLayout() {
5252
)}
5353

5454
{isElectron && (
55-
<div className="drag-region flex h-[52px] shrink-0 items-center border-b border-border px-5">
55+
<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)]">
5656
<span className="text-xs font-medium tracking-wide text-muted-foreground/70">
5757
Settings
5858
</span>

0 commit comments

Comments
 (0)