mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 08:52:12 +00:00
fix(window): show the window buttons in full screen on macOS (#2015)
Native full screen moves a window'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 title bar's slot for them stayed empty. The title bar now draws them there while the window fills the screen: close closes that project's tab, the green button leaves full screen, and minimize is disabled.
This commit is contained in:
1 parent
257fb0f50f
commit
429b8f0efe
16 files changed
+205
-7
No files matched your search
@@ -15,6 +15,8 @@ use std::sync::Arc;
|
||||
pub enum IdeEvent {
|
||||
/// Project window membership or selection changed.
|
||||
WindowGroupsChanged,
|
||||
/// A window entered or left full screen, itself or as a member of a full-screen window group.
|
||||
WindowFullscreenChanged,
|
||||
ProjectTrustChanged {
|
||||
root: crate::IpcPath,
|
||||
trusted: bool,
|
||||
|
||||
@@ -431,6 +431,7 @@ pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
|
||||
window::window_toggle_fullscreen,
|
||||
window::window_flash_taskbar,
|
||||
window::window_set_titlebar_height,
|
||||
window::window_is_fullscreen,
|
||||
window_drag::window_start_drag,
|
||||
window::window_take_launch,
|
||||
window::window_close_context,
|
||||
|
||||
@@ -193,6 +193,20 @@ pub fn window_open_projects(
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Whether this window fills the screen: in native full screen, or (macOS) shown in its window
|
||||
/// group's full-screen Space.
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn window_is_fullscreen(window: WebviewWindow) -> bool {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use tauri::Manager;
|
||||
crate::window_groups::in_fullscreen(window.app_handle(), &window)
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
window.is_fullscreen().unwrap_or(false)
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Type)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CloseContext {
|
||||
|
||||
@@ -135,6 +135,7 @@ pub fn set_fullscreen_auxiliary(window: &WebviewWindow, host: Option<&WebviewWin
|
||||
if let Some(frame) = frame {
|
||||
native.setFrame_display(frame, true);
|
||||
}
|
||||
target.state::<crate::state::AppState>().sink.emit(ide_core::IdeEvent::WindowFullscreenChanged);
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -12,11 +12,12 @@ use objc2::{
|
||||
runtime::ProtocolObject,
|
||||
};
|
||||
use objc2_app_kit::{
|
||||
NSToolbar, NSWindow, NSWindowDidExitFullScreenNotification, NSWindowStyleMask, NSWindowToolbarStyle,
|
||||
NSWindowWillEnterFullScreenNotification,
|
||||
NSToolbar, NSWindow, NSWindowDidEnterFullScreenNotification, NSWindowDidExitFullScreenNotification,
|
||||
NSWindowStyleMask, NSWindowToolbarStyle, NSWindowWillEnterFullScreenNotification,
|
||||
};
|
||||
use objc2_foundation::{NSNotificationCenter, NSObjectProtocol};
|
||||
use std::{cell::Cell, cell::RefCell, collections::HashMap, rc::Rc};
|
||||
use tauri::Manager;
|
||||
|
||||
/// Unified compact (38pt) and unified (52pt) toolbars; pick the one closer to the web bar.
|
||||
const COMPACT_MAX_HEIGHT: f64 = 45.0;
|
||||
@@ -55,7 +56,7 @@ impl Drop for Observer {
|
||||
|
||||
struct Tracked {
|
||||
state: Rc<Fullscreen>,
|
||||
_observers: [Observer; 2],
|
||||
_observers: [Observer; 3],
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
@@ -71,7 +72,7 @@ fn apply(window: &NSWindow, state: &Fullscreen) {
|
||||
}
|
||||
}
|
||||
|
||||
fn track(window: &NSWindow) -> Tracked {
|
||||
fn track(window: &NSWindow, app: &tauri::AppHandle) -> Tracked {
|
||||
let state = Rc::new(Fullscreen::default());
|
||||
if window.styleMask().contains(NSWindowStyleMask::FullScreen) {
|
||||
state.will_enter();
|
||||
@@ -79,11 +80,14 @@ fn track(window: &NSWindow) -> Tracked {
|
||||
let observe = |name, change: fn(&Fullscreen)| {
|
||||
let weak = Weak::new(window);
|
||||
let state = state.clone();
|
||||
let app = app.clone();
|
||||
let block = block2::RcBlock::new(move |_| {
|
||||
change(&state);
|
||||
if let Some(window) = weak.load() {
|
||||
apply(&window, &state);
|
||||
}
|
||||
// The web title bar shows the window buttons itself in full screen.
|
||||
app.state::<crate::state::AppState>().sink.emit(ide_core::IdeEvent::WindowFullscreenChanged);
|
||||
});
|
||||
let observer = unsafe {
|
||||
NSNotificationCenter::defaultCenter().addObserverForName_object_queue_usingBlock(
|
||||
@@ -98,6 +102,7 @@ fn track(window: &NSWindow) -> Tracked {
|
||||
let observers = unsafe {
|
||||
[
|
||||
observe(NSWindowWillEnterFullScreenNotification, Fullscreen::will_enter),
|
||||
observe(NSWindowDidEnterFullScreenNotification, Fullscreen::will_enter),
|
||||
observe(NSWindowDidExitFullScreenNotification, Fullscreen::did_exit),
|
||||
]
|
||||
};
|
||||
@@ -106,12 +111,13 @@ fn track(window: &NSWindow) -> Tracked {
|
||||
|
||||
pub fn set_height(window: &tauri::WebviewWindow, height: f64) -> tauri::Result<()> {
|
||||
let label = window.label().to_owned();
|
||||
let app = window.app_handle().clone();
|
||||
// with_webview runs on the AppKit thread.
|
||||
window.with_webview(move |webview| unsafe {
|
||||
let Some(mtm) = MainThreadMarker::new() else { return };
|
||||
let native = &*webview.ns_window().cast::<NSWindow>();
|
||||
let state =
|
||||
WINDOWS.with_borrow_mut(|windows| windows.entry(label).or_insert_with(|| track(native)).state.clone());
|
||||
let state = WINDOWS
|
||||
.with_borrow_mut(|windows| windows.entry(label).or_insert_with(|| track(native, &app)).state.clone());
|
||||
if native.toolbar().is_none() {
|
||||
let toolbar = NSToolbar::new(mtm);
|
||||
native.setToolbar(Some(&toolbar));
|
||||
|
||||
+8
-1
@@ -903,6 +903,11 @@ 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.
|
||||
*/
|
||||
windowIsFullscreen: () => __TAURI_INVOKE<boolean>("window_is_fullscreen"),
|
||||
/**
|
||||
* Starts dragging `window` while the primary mouse button is still down; returns whether a drag began.
|
||||
*
|
||||
@@ -4479,7 +4484,9 @@ export type IdeError = {
|
||||
|
||||
export type IdeEvent =
|
||||
/** Project window membership or selection changed. */
|
||||
{ type: "windowGroupsChanged" } | { type: "projectTrustChanged"; root: IpcPath; trusted: boolean } |
|
||||
{ type: "windowGroupsChanged" } |
|
||||
/** A window entered or left full screen, itself or as a member of a full-screen window group. */
|
||||
{ type: "windowFullscreenChanged" } | { type: "projectTrustChanged"; root: IpcPath; trusted: boolean } |
|
||||
/** Files or directories changed on disk (after debouncing, ignore rules applied). */
|
||||
{ type: "fsChanged"; created: IpcPath[]; modified: IpcPath[]; removed: IpcPath[] } |
|
||||
/** Excluded or ignored folders whose contents changed; they stay out of `FsChanged`. */
|
||||
|
||||
@@ -196,6 +196,7 @@
|
||||
"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}}",
|
||||
|
||||
@@ -196,6 +196,7 @@
|
||||
"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,6 +436,16 @@ 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 {
|
||||
--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
|
||||
|
||||
@@ -248,6 +248,9 @@ 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",
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<!-- 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>
|
||||
|
After Width: | Height: | Size: 299 B |
@@ -0,0 +1,5 @@
|
||||
<!-- 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>
|
||||
|
After Width: | Height: | Size: 319 B |
@@ -0,0 +1,4 @@
|
||||
<!-- 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>
|
||||
|
After Width: | Height: | Size: 277 B |
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* 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, 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(() => {
|
||||
let disposed = false;
|
||||
const offs: (() => void)[] = [];
|
||||
const sync = () => void commands.windowIsFullscreen().then((on) => !disposed && setFullscreen(on));
|
||||
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);
|
||||
});
|
||||
return () => {
|
||||
disposed = true;
|
||||
offs.forEach((off) => off());
|
||||
};
|
||||
}, []);
|
||||
return { fullscreen, active };
|
||||
}
|
||||
|
||||
function closeWindow() {
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -195,3 +195,52 @@
|
||||
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,6 +21,7 @@ import {
|
||||
widgetTitle,
|
||||
} from "./mainToolbar";
|
||||
import { WindowControls } from "./WindowControls";
|
||||
import { MacWindowButtons } from "./MacWindowButtons";
|
||||
import { usePlatform } from "@/platform/platformInfo";
|
||||
import { projectInitials } from "./projectColor";
|
||||
import { keymapItems, themeItems, themePreview, type ThemePreview, VIEW_MODE_ITEMS } from "./quickSwitch";
|
||||
@@ -301,6 +302,7 @@ export function TitleBar({
|
||||
if (bare)
|
||||
return (
|
||||
<header ref={setBar} className={className} {...windowDragRegion} data-titlebar="bare">
|
||||
{overlay && <MacWindowButtons />}
|
||||
{frameless && (
|
||||
<>
|
||||
<MainMenu />
|
||||
@@ -323,6 +325,7 @@ export function TitleBar({
|
||||
showContextMenu(e, menu);
|
||||
}}
|
||||
>
|
||||
{overlay && <MacWindowButtons />}
|
||||
{frameless && !minimal && <MainMenu onExpandedChange={setMenuExpanded} />}
|
||||
{title && (
|
||||
<span className={styles.windowTitle} {...windowDragRegion}>
|
||||
|
||||
Reference in new issue
Block a user