fix(window): make macOS project tabs native window tabs (#2123)

Project tabs in a window group are now one native AppKit tab group behind the
page's own tab bar, with AppKit's tab bar hidden. The group shares one frame and
enters full screen as a whole, so every tab shows the system window buttons in
the bar revealed with the menu bar, and tabs no longer lose their title bar
layout after leaving full screen.

This removes the full-screen auxiliary windows, the host handover on close, the
frame copying between hidden members and the window buttons drawn by the page.
The title bar releases the room kept for the window buttons in full screen.
Linux and Windows keep showing one member at a time.
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-11-08 17:20:59 +08:00
1 джерело f07627ada6
коміт 75cb45ed94
24 files changed
+454 -1075

No files matched your search

+1 -9
Переглянути файл
@@ -42,11 +42,6 @@ export const commands = {
windowGroupCloseTabs: (keep: string, labels: string[]) => typedError<null, IdeError>(__TAURI_INVOKE("window_group_close_tabs", { keep, labels })),
/** This window kept its project open (unsaved-changes prompt cancelled); stops a batch close. */
windowGroupCancelClose: () => __TAURI_INVOKE<void>("window_group_cancel_close"),
/**
* This window closes next (its prompts passed): a fullscreen group's host first hands its Space
* to the tab that takes over.
*/
windowGroupBeforeClose: () => typedError<null, IdeError>(__TAURI_INVOKE("window_group_before_close")),
windowGroupReorder: (labels: string[]) => typedError<null, IdeError>(__TAURI_INVOKE("window_group_reorder", { labels })),
windowGroupRestore: () => typedError<null, IdeError>(__TAURI_INVOKE("window_group_restore")),
/**
@@ -925,10 +920,7 @@ export const commands = {
windowFlashTaskbar: (flash: boolean) => typedError<null, IdeError>(__TAURI_INVOKE("window_flash_taskbar", { flash })),
/** Height of the web title bar in native logical pixels (CSS height × IDE zoom). */
windowSetTitlebarHeight: (height: number | null) => typedError<null, IdeError>(__TAURI_INVOKE("window_set_titlebar_height", { height })),
/**
* Whether this window fills the screen: in native full screen, or (macOS) shown in its window
* group's full-screen Space.
*/
/** Whether this window is in native full screen (on macOS, also a tab of a fullscreen group). */
windowIsFullscreen: () => __TAURI_INVOKE<boolean>("window_is_fullscreen"),
/**
* Starts dragging `window` while the primary mouse button is still down; returns whether a drag began.
-1
Переглянути файл
@@ -205,7 +205,6 @@ async function onCloseRequested(deps: CloseDeps): Promise<boolean> {
deps,
);
if (!ok) await (ctx.quitting ? commands.appCancelQuit() : commands.windowGroupCancelClose());
else if (!ctx.quitting) await commands.windowGroupBeforeClose();
return ok;
} catch (e) {
maybeEditorService()?.cancelClose?.();
+1 -1
Переглянути файл
@@ -123,7 +123,7 @@ describe("theme tokens", () => {
* on purpose: colours the user assigns (project and data source colours), the
* platform's caption close and macOS window buttons, and the tint strength.
*/
const INHERITED_FROM_BASE = /^--(project-color-|db-color-|caption-close-|fg-caption-close-|mac-window-|tint-strength$)/;
const INHERITED_FROM_BASE = /^--(project-color-|db-color-|caption-close-|fg-caption-close-|tint-strength$)/;
describe("palette themes", () => {
const ISLANDS = ["light", "dark", "dark-hc"];
-1
Переглянути файл
@@ -197,7 +197,6 @@
"window.caption.minimize": "Minimize",
"window.caption.maximize": "Maximize",
"window.caption.restore": "Restore Down",
"window.caption.exitFullScreen": "Exit Full Screen",
"window.caption.close": "Close",
"menu.help.keymapReference": "Keymap Reference",
"menu.help.showLog": "{os, select, macos {Show Log in Finder} windows {Show Log in Explorer} other {Show Log in Files}}",
-1
Переглянути файл
@@ -197,7 +197,6 @@
"window.caption.minimize": "最小化",
"window.caption.maximize": "最大化",
"window.caption.restore": "向下还原",
"window.caption.exitFullScreen": "退出全屏",
"window.caption.close": "关闭",
"menu.help.keymapReference": "快捷键参考",
"menu.help.showLog": "{os, select, macos {在访达中显示日志} windows {在资源管理器中显示日志} other {在文件管理器中显示日志}}",
-12
Переглянути файл
@@ -436,18 +436,6 @@ html {
--fg-caption-close-hover: #ffffff;
}
/* macOS window buttons drawn by the web title bar in full screen: the system colours in every theme. */
:root,
:root[data-theme="light"],
:root[data-theme="dark-hc"] {
--mac-window-close: #ff5f57;
--mac-window-minimize: #febc2e;
--mac-window-zoom: #28c840;
--mac-window-glyph: rgb(0 0 0 / 55%);
--mac-window-edge: rgb(0 0 0 / 12%);
--mac-window-inactive: color-mix(in srgb, var(--fg-strong) 22%, transparent);
}
/*
* Islands Light. Every colour token of :root is redefined here. Values are
* PyCharm's own; its secondary greys, comment / metadata colours, line numbers
-3
Переглянути файл
@@ -252,9 +252,6 @@ const ICONS = {
"window-maximize": "openidea/general/windowMaximize",
"window-restore": "openidea/general/windowRestore",
"window-close": "openidea/general/windowClose",
"mac-window-close": "openidea/general/macWindowClose",
"mac-window-minimize": "openidea/general/macWindowMinimize",
"mac-window-exit-full-screen": "openidea/general/macWindowExitFullScreen",
hide: "general/hide",
pin: "general/pin",
eye: "general/show",
-4
Переглянути файл
@@ -1,4 +0,0 @@
<!-- OpenIDEA: macOS window button glyph (close), drawn in the full-screen title bar. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.5 4.5L11.5 11.5M11.5 4.5L4.5 11.5" stroke="#6C707E" stroke-width="2" stroke-linecap="round"/>
</svg>

До

Ширина:  |  Висота:  |  Розмір: 299 B

-5
Переглянути файл
@@ -1,5 +0,0 @@
<!-- OpenIDEA: macOS window button glyph (exit full screen), drawn in the full-screen title bar. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.75 2.5V7.25H13.5L8.75 2.5Z" fill="#6C707E"/>
<path d="M7.25 13.5V8.75H2.5L7.25 13.5Z" fill="#6C707E"/>
</svg>

До

Ширина:  |  Висота:  |  Розмір: 319 B

-4
Переглянути файл
@@ -1,4 +0,0 @@
<!-- OpenIDEA: macOS window button glyph (minimize), drawn in the full-screen title bar. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.5 8H12.5" stroke="#6C707E" stroke-width="2" stroke-linecap="round"/>
</svg>

До

Ширина:  |  Висота:  |  Розмір: 277 B

-96
Переглянути файл
@@ -1,96 +0,0 @@
/**
* macOS window buttons in the title bar's slot while the window fills the screen. Native full
* screen moves the system's buttons into the bar revealed with the menu bar, and a project tab
* shown in its group's full-screen Space has none, so the slot kept for them would be empty.
* Close closes this project's tab, the green button leaves full screen, minimize is disabled.
* Like the system's, they take no keyboard focus.
*/
import { useEffect, useState } from "react";
import clsx from "clsx";
import { t } from "@/platform/i18n";
import { commands, isTauri, onIdeEvent } from "@/platform/ipc";
import { Icon } from "@/ui";
import styles from "./TitleBar.module.css";
type AppWindow = import("@tauri-apps/api/window").Window;
function useFullscreenWindow(): { fullscreen: boolean; active: boolean } {
const [fullscreen, setFullscreen] = useState(false);
const [active, setActive] = useState(() => document.hasFocus());
useEffect(() => {
// The plain-browser shell and tests have no Tauri window to ask.
if (!isTauri()) return;
let disposed = false;
const offs: (() => void)[] = [];
const sync = () =>
void commands
.windowIsFullscreen()
.then((on) => !disposed && setFullscreen(on))
.catch(() => {});
sync();
offs.push(onIdeEvent("windowFullscreenChanged", sync));
void import("@tauri-apps/api/window")
.then(async ({ getCurrentWindow }) => {
const w: AppWindow = getCurrentWindow();
const listeners = await Promise.all([
w.onResized(sync),
w.onFocusChanged(({ payload }) => {
setActive(payload);
sync();
}),
]);
if (disposed) listeners.forEach((off) => off());
else offs.push(...listeners);
})
.catch(() => {});
return () => {
disposed = true;
offs.forEach((off) => off());
};
}, []);
return { fullscreen, active };
}
function closeWindow() {
if (!isTauri()) return;
void import("@tauri-apps/api/window").then(({ getCurrentWindow }) => getCurrentWindow().close());
}
export function MacWindowButtons() {
const { fullscreen, active } = useFullscreenWindow();
if (!fullscreen) return null;
return (
<div
className={clsx(styles.macButtons, !active && styles.macButtonsInactive)}
onMouseDown={(e) => e.preventDefault()}
>
<button
type="button"
tabIndex={-1}
className={clsx(styles.macButton, styles.macClose)}
aria-label={t("window.caption.close")}
onClick={closeWindow}
>
<Icon name="mac-window-close" size={8} />
</button>
<button
type="button"
tabIndex={-1}
className={clsx(styles.macButton, styles.macMinimize)}
aria-label={t("window.caption.minimize")}
disabled
>
<Icon name="mac-window-minimize" size={8} />
</button>
<button
type="button"
tabIndex={-1}
className={clsx(styles.macButton, styles.macZoom)}
aria-label={t("window.caption.exitFullScreen")}
onClick={() => void commands.windowToggleFullscreen()}
>
<Icon name="mac-window-exit-full-screen" size={8} />
</button>
</div>
);
}
+4 -49
Переглянути файл
@@ -16,6 +16,10 @@
/* Keep the gear on the icon-only right stripe's centre line, including compact mode. */
padding-right: calc(var(--stripe-outer-padding) + (var(--stripe-icon-button) - var(--titlebar-tool-button)) / 2);
}
/* Native full screen moves the window buttons out of the bar. */
.overlayFullscreen {
padding-left: 10px;
}
/* Welcome window: plain title on the frame colour with a hairline, like IntelliJ's welcome frame. */
.minimal {
background: var(--bg-elev);
@@ -195,52 +199,3 @@
background: var(--caption-close-pressed);
color: var(--fg-caption-close-hover);
}
/* macOS window buttons drawn in the traffic-light slot while the window fills the screen. */
.macButtons {
position: fixed;
top: 0;
left: 0;
height: var(--h-titlebar);
display: flex;
align-items: center;
gap: 8px;
padding-left: 12px;
zoom: calc(1 / var(--titlebar-zoom, 1));
z-index: 1;
}
.macButton {
width: 12px;
height: 12px;
padding: 0;
border: none;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: var(--mac-window-glyph);
box-shadow: inset 0 0 0 0.5px var(--mac-window-edge);
}
.macButton > * {
opacity: 0;
}
.macButtons:hover .macButton:not(:disabled) > * {
opacity: 1;
}
.macClose {
background: var(--mac-window-close);
}
.macMinimize {
background: var(--mac-window-minimize);
}
.macZoom {
background: var(--mac-window-zoom);
}
.macButton:active:not(:disabled) {
filter: brightness(0.8);
}
/* Disabled in full screen, and every button of an inactive window, are grey as AppKit draws them. */
.macButton:disabled,
.macButtonsInactive:not(:hover) .macButton {
background: var(--mac-window-inactive);
}
+8 -4
Переглянути файл
@@ -21,7 +21,7 @@ import {
widgetTitle,
} from "./mainToolbar";
import { WindowControls } from "./WindowControls";
import { MacWindowButtons } from "./MacWindowButtons";
import { useFullscreen } from "./useFullscreen";
import { usePlatform } from "@/platform/platformInfo";
import { projectInitials } from "./projectColor";
import { keymapItems, themeItems, themePreview, type ThemePreview, VIEW_MODE_ITEMS } from "./quickSwitch";
@@ -291,18 +291,23 @@ export function TitleBar({
title?: string;
}) {
const overlay = usePlatform((s) => s.overlayTitleBar);
const fullscreen = useFullscreen();
const frameless = usePlatform((s) => s.framelessTitleBar);
const { map, shown, menu } = useToolbar();
const [menuExpanded, setMenuExpanded] = useState(false);
const [bar, setBar] = useState<HTMLElement | null>(null);
useNativeTitlebar(bar);
const className = clsx(styles.bar, overlay && styles.overlay, frameless && styles.frameless);
const className = clsx(
styles.bar,
overlay && styles.overlay,
overlay && fullscreen && styles.overlayFullscreen,
frameless && styles.frameless,
);
// Distraction Free Mode: the window frame only. Where the title bar carries the
// main menu (Windows, Linux) it stays, or nothing would lead back out of the mode.
if (bare)
return (
<header ref={setBar} className={className} {...windowDragRegion} data-titlebar="bare">
{overlay && <MacWindowButtons />}
{frameless && (
<>
<MainMenu />
@@ -325,7 +330,6 @@ export function TitleBar({
showContextMenu(e, menu);
}}
>
{overlay && <MacWindowButtons />}
{frameless && !minimal && <MainMenu onExpandedChange={setMenuExpanded} />}
{title && (
<span className={styles.windowTitle} {...windowDragRegion}>
+28
Переглянути файл
@@ -0,0 +1,28 @@
/**
* Whether this window fills the screen: native full screen, or a project tab shown in its
* group's full-screen Space. Native full screen moves the window buttons out of the title bar,
* so the room kept for them is released.
*/
import { useEffect, useState } from "react";
import { commands, isTauri, onIdeEvent } from "@/platform/ipc";
export function useFullscreen(): boolean {
const [fullscreen, setFullscreen] = useState(false);
useEffect(() => {
// The plain-browser shell and tests have no Tauri window to ask.
if (!isTauri()) return;
let disposed = false;
const sync = () =>
void commands
.windowIsFullscreen()
.then((on) => !disposed && setFullscreen(on))
.catch(() => {});
sync();
const off = onIdeEvent("windowFullscreenChanged", sync);
return () => {
disposed = true;
off();
};
}, []);
return fullscreen;
}