From a377ae68dc4e5901fa23947a97104b2d3efb090e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 19 Aug 2026 17:54:14 +0000 Subject: [PATCH] Use the Windows-style title bar on Linux. Linux previously kept native window decorations and the macOS-style in-app chrome, so the toolbar looked like traffic lights. The main window now uses the same custom title bar as Windows, with File/Edit/View/Help and minimize/maximize/close on the right. Co-authored-by: John Jeong --- apps/desktop/src/main/body.tsx | 16 +++---- apps/desktop/src/main/shell-frame.test.tsx | 30 ++++++++----- apps/desktop/src/main/shell-frame.tsx | 4 +- .../hooks/useWindowControlsGutter.test.tsx | 8 +++- .../shared/hooks/useWindowControlsGutter.ts | 6 +++ apps/desktop/src/shared/main/body.test.tsx | 45 +++---------------- plugins/windows/src/window/v1.rs | 12 +---- 7 files changed, 49 insertions(+), 72 deletions(-) diff --git a/apps/desktop/src/main/body.tsx b/apps/desktop/src/main/body.tsx index 52fe61d18e..5a8dedfe62 100644 --- a/apps/desktop/src/main/body.tsx +++ b/apps/desktop/src/main/body.tsx @@ -1,4 +1,3 @@ -import { platform } from "@tauri-apps/plugin-os"; import { type CSSProperties, type WheelEvent as ReactWheelEvent, @@ -37,7 +36,10 @@ import { useClassicMainShortcuts } from "./useShortcuts"; import { useShell } from "~/contexts/shell"; import { scrollElementByWheel } from "~/shared/dom/scroll-wheel"; import { useMountEffect } from "~/shared/hooks/useMountEffect"; -import { useWindowControlsGutter } from "~/shared/hooks/useWindowControlsGutter"; +import { + usesWindowsStyleTitleBar, + useWindowControlsGutter, +} from "~/shared/hooks/useWindowControlsGutter"; import { getMainContentMinWidth } from "~/shared/main/layout-widths"; import { useOpenNoteDialog } from "~/shared/open-note-dialog"; import { useNewNote } from "~/shared/useNewNote"; @@ -80,7 +82,7 @@ export function ClassicMainBody({ useState(false); const [noteFilter, setNoteFilter] = useState("mine"); const showWindowControlsGutter = useWindowControlsGutter(); - const showSidebarToggleInBody = getRuntimePlatform() !== "windows"; + const showSidebarToggleInBody = !usesWindowsStyleTitleBar(); leftSidebarPanelConstraintsRef.current = leftSidebarPanelConstraints; const isOnboarding = currentTab?.type === "onboarding"; @@ -572,11 +574,3 @@ export function ClassicMainBody({ ); } - -function getRuntimePlatform() { - try { - return platform(); - } catch { - return null; - } -} diff --git a/apps/desktop/src/main/shell-frame.test.tsx b/apps/desktop/src/main/shell-frame.test.tsx index 4aec57bf13..f4a0c93531 100644 --- a/apps/desktop/src/main/shell-frame.test.tsx +++ b/apps/desktop/src/main/shell-frame.test.tsx @@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ currentTab: { type: "empty" } as { type: string } | null, - platform: "macos" as "macos" | "windows", + platform: "macos" as "linux" | "macos" | "windows", leftsidebar: { expanded: true, }, @@ -77,18 +77,28 @@ describe("ClassicMainShellFrame", () => { mocks.leftsidebar.expanded = true; }); - it("places the custom title bar above the shell on Windows", () => { - mocks.platform = "windows"; + it.each(["windows", "linux"] as const)( + "places the custom title bar above the shell on %s", + (runtimePlatform) => { + mocks.platform = runtimePlatform; - render(); + render(); - const titleBar = screen.getByTestId("windows-title-bar"); - const scaffold = screen.getByTestId("main-shell-scaffold"); + const titleBar = screen.getByTestId("windows-title-bar"); + const scaffold = screen.getByTestId("main-shell-scaffold"); + + expect(titleBar.compareDocumentPosition(scaffold)).toBe( + Node.DOCUMENT_POSITION_FOLLOWING, + ); + expect(titleBar.parentElement?.className).toContain("flex-col"); + }, + ); + + it("keeps native macOS chrome without the custom title bar", () => { + render(); - expect(titleBar.compareDocumentPosition(scaffold)).toBe( - Node.DOCUMENT_POSITION_FOLLOWING, - ); - expect(titleBar.parentElement?.className).toContain("flex-col"); + expect(screen.queryByTestId("windows-title-bar")).toBeNull(); + expect(screen.getByTestId("main-shell-scaffold")).toBeTruthy(); }); it("uses left-edge main surface chrome while the sidebar timeline is expanded", () => { diff --git a/apps/desktop/src/main/shell-frame.tsx b/apps/desktop/src/main/shell-frame.tsx index 3f899d1c6d..de2d791096 100644 --- a/apps/desktop/src/main/shell-frame.tsx +++ b/apps/desktop/src/main/shell-frame.tsx @@ -1,4 +1,3 @@ -import { platform } from "@tauri-apps/plugin-os"; import { memo } from "react"; import { ClassicMainBody } from "./body"; @@ -6,6 +5,7 @@ import { resolveMainSurfaceChrome } from "./main-surface-chrome"; import { WindowsTitleBar } from "./windows-title-bar"; import { useShell } from "~/contexts/shell"; +import { usesWindowsStyleTitleBar } from "~/shared/hooks/useWindowControlsGutter"; import { MainShellBodyFrame, MainShellScaffold } from "~/shared/main"; import { ToastNotifications } from "~/sidebar/toast"; import { @@ -45,7 +45,7 @@ export function ClassicMainShellFrame() { ); - if (platform() !== "windows") { + if (!usesWindowsStyleTitleBar()) { return shell; } diff --git a/apps/desktop/src/shared/hooks/useWindowControlsGutter.test.tsx b/apps/desktop/src/shared/hooks/useWindowControlsGutter.test.tsx index 82fb0b8bfb..18871d0664 100644 --- a/apps/desktop/src/shared/hooks/useWindowControlsGutter.test.tsx +++ b/apps/desktop/src/shared/hooks/useWindowControlsGutter.test.tsx @@ -16,7 +16,10 @@ vi.mock("@tauri-apps/plugin-os", () => ({ platform: mocks.platform, })); -import { useWindowControlsGutter } from "./useWindowControlsGutter"; +import { + usesWindowsStyleTitleBar, + useWindowControlsGutter, +} from "./useWindowControlsGutter"; describe("useWindowControlsGutter", () => { beforeEach(() => { @@ -39,6 +42,7 @@ describe("useWindowControlsGutter", () => { const { result } = renderHook(() => useWindowControlsGutter()); expect(result.current).toBe(false); + expect(usesWindowsStyleTitleBar()).toBe(true); expect(mocks.getCurrentWindow).not.toHaveBeenCalled(); }, ); @@ -51,6 +55,7 @@ describe("useWindowControlsGutter", () => { const { result } = renderHook(() => useWindowControlsGutter()); expect(result.current).toBe(true); + expect(usesWindowsStyleTitleBar()).toBe(false); expect(mocks.getCurrentWindow).not.toHaveBeenCalled(); }); @@ -60,6 +65,7 @@ describe("useWindowControlsGutter", () => { const { result } = renderHook(() => useWindowControlsGutter()); expect(result.current).toBe(true); + expect(usesWindowsStyleTitleBar()).toBe(false); await waitFor(() => expect(mocks.isFullscreen).toHaveBeenCalledOnce()); expect(result.current).toBe(true); }); diff --git a/apps/desktop/src/shared/hooks/useWindowControlsGutter.ts b/apps/desktop/src/shared/hooks/useWindowControlsGutter.ts index 0d933dbbeb..5d4cc38c9e 100644 --- a/apps/desktop/src/shared/hooks/useWindowControlsGutter.ts +++ b/apps/desktop/src/shared/hooks/useWindowControlsGutter.ts @@ -4,6 +4,12 @@ import { useState } from "react"; import { useMountEffect } from "~/shared/hooks/useMountEffect"; +export function usesWindowsStyleTitleBar() { + const runtimePlatform = getRuntimePlatform(); + + return runtimePlatform === "windows" || runtimePlatform === "linux"; +} + export function useWindowControlsGutter() { const [visible, setVisible] = useState(() => { const runtimePlatform = getRuntimePlatform(); diff --git a/apps/desktop/src/shared/main/body.test.tsx b/apps/desktop/src/shared/main/body.test.tsx index 7e862efea8..efd21fc216 100644 --- a/apps/desktop/src/shared/main/body.test.tsx +++ b/apps/desktop/src/shared/main/body.test.tsx @@ -373,46 +373,15 @@ describe("ClassicMainBody", () => { ); it.each([ - { - expanded: true, - sidebarState: "expanded", - toggleLabel: "Hide sidebar", - }, - { - expanded: false, - sidebarState: "collapsed", - toggleLabel: "Show sidebar", - }, + ["windows", "expanded", true], + ["windows", "collapsed", false], + ["linux", "expanded", true], + ["linux", "collapsed", false], ] as const)( - "uses the 8px fallback gutter on Linux with the sidebar $sidebarState", - async ({ expanded, toggleLabel }) => { - mocks.leftSidebarExpanded = expanded; - mocks.platform = "linux"; - - render(); - - const sidebarToggle = screen.getByRole("button", { name: toggleLabel }); - const chromeFrame = expanded - ? document.querySelector("[data-sidebar-timeline-header]") - : sidebarToggle.parentElement?.parentElement?.parentElement; - - await waitFor(() => { - expect(chromeFrame?.className).toContain("pl-2"); - }); - expect(chromeFrame?.className).not.toContain("pl-[76px]"); - expect(mocks.isFullscreen).not.toHaveBeenCalled(); - expect(mocks.resizeListeners).toHaveLength(0); - }, - ); - - it.each([ - ["expanded", true], - ["collapsed", false], - ])( - "leaves the sidebar toggle to the Windows title bar while %s", - async (_state, expanded) => { + "leaves the sidebar toggle to the title bar on %s while %s", + async (runtimePlatform, _state, expanded) => { mocks.leftSidebarExpanded = expanded; - mocks.platform = "windows"; + mocks.platform = runtimePlatform; render(); diff --git a/plugins/windows/src/window/v1.rs b/plugins/windows/src/window/v1.rs index 41fe3b9cc5..237870efb2 100644 --- a/plugins/windows/src/window/v1.rs +++ b/plugins/windows/src/window/v1.rs @@ -171,16 +171,11 @@ impl AppWindow { .title_bar_style(tauri::TitleBarStyle::Overlay); } - #[cfg(target_os = "windows")] + #[cfg(any(target_os = "windows", target_os = "linux"))] { builder = builder.decorations(!matches!(self, Self::Main)); } - #[cfg(target_os = "linux")] - { - builder = builder.decorations(true); - } - builder } } @@ -238,12 +233,9 @@ impl WindowImpl for AppWindow { } }; - #[cfg(target_os = "windows")] + #[cfg(any(target_os = "windows", target_os = "linux"))] window.set_decorations(!matches!(self, Self::Main))?; - #[cfg(target_os = "linux")] - window.set_decorations(true)?; - Ok(window) }