дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02:00
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.
Цей коміт міститься в:
1 джерело
f07627ada6
коміт
75cb45ed94
24 files changed
+454
-1075
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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?.();
|
||||
|
||||
@@ -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"];
|
||||
|
||||
@@ -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}}",
|
||||
|
||||
@@ -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 {在文件管理器中显示日志}}",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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}>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Заблокувати користувача