From a852bcc09410b47dcabbe9b089725777024d125e Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Mon, 1 Sep 2025 02:24:00 +0200 Subject: [PATCH] Improve system window tabs visibility (#37244) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow up of https://github.com/zed-industries/zed/pull/33334 After chatting with @MrSubidubi we found out that he had an old defaults setting (most likely from when he encountered a previous window tabbing bug): ``` ❯ defaults read dev.zed.Zed-Nightly { NSNavPanelExpandedSizeForOpenMode = "{800, 448}"; NSNavPanelExpandedSizeForSaveMode = "{800, 448}"; NSNavPanelExpandedStateForSaveMode = 1; NSOSPLastRootDirectory = {length = 828, bytes = 0x626f6f6b 3c030000 00000410 30000000 ... dc010000 00000000 }; "NSWindow Frame NSNavPanelAutosaveName" = "557 1726 800 448 -323 982 2560 1440 "; "NSWindowTabbingShoudShowTabBarKey-GPUIWindow-GPUIWindow-(null)-HT-FS" = 1; } ``` > That suffix is AppKit’s fallback autosave name when no tabbing identifier is set. It encodes the NSWindow subclass (GPUIWindow), plus traits like HT (hidden titlebar) and FS (fullscreen). Which explains why it only happened on the Nightly build, since each bundle has it's own defaults. It also explains why the tabbar would disappear when he activated the `use_system_window_tabs` setting, because with that setting activated, the tabbing identifier becomes "zed" (instead of the default one when omitted) for which he didn't have the `NSWindowTabbingShoudShowTabBarKey` default. The original implementation was perhaps a bit naive and relied fully on macOS to determine if the tabbar should be shown. I've updated the code to always hide the tabbar, if the setting is turned off and there is only 1 tab entry. While testing, I also noticed that the menu's like 'merge all windows' wouldn't become active when the setting was turned on, only after a full workspace reload. So I added a setting observer as well, to immediately set the correct window properties to enable all the features without a reload. Release Notes: - N/A --- crates/gpui/src/platform.rs | 1 + crates/gpui/src/platform/mac/window.rs | 21 ++++++++++ crates/gpui/src/window.rs | 7 ++++ crates/title_bar/src/system_window_tabs.rs | 49 ++++++++++++++++++++-- crates/zed/src/main.rs | 2 +- 5 files changed, 76 insertions(+), 4 deletions(-) diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index eb1d73814388a26503e9ada782bc358dc712b53c..d3425c8835bb474ffbed6bc79371340d569d1bfb 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -522,6 +522,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle { fn merge_all_windows(&self) {} fn move_tab_to_new_window(&self) {} fn toggle_window_tab_overview(&self) {} + fn set_tabbing_identifier(&self, _identifier: Option) {} #[cfg(target_os = "windows")] fn get_raw_handle(&self) -> windows::HWND; diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 0262cbb1213ca670cece780959c740f292764630..686cfb314e58c4e10e916a07931fb5f4248ea54e 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -781,6 +781,8 @@ impl MacWindow { if let Some(tabbing_identifier) = tabbing_identifier { let tabbing_id = NSString::alloc(nil).init_str(tabbing_identifier.as_str()); let _: () = msg_send![native_window, setTabbingIdentifier: tabbing_id]; + } else { + let _: () = msg_send![native_window, setTabbingIdentifier:nil]; } } WindowKind::PopUp => { @@ -1018,6 +1020,25 @@ impl PlatformWindow for MacWindow { } } + fn set_tabbing_identifier(&self, tabbing_identifier: Option) { + let native_window = self.0.lock().native_window; + unsafe { + let allows_automatic_window_tabbing = tabbing_identifier.is_some(); + if allows_automatic_window_tabbing { + let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: YES]; + } else { + let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: NO]; + } + + if let Some(tabbing_identifier) = tabbing_identifier { + let tabbing_id = NSString::alloc(nil).init_str(tabbing_identifier.as_str()); + let _: () = msg_send![native_window, setTabbingIdentifier: tabbing_id]; + } else { + let _: () = msg_send![native_window, setTabbingIdentifier:nil]; + } + } + } + fn scale_factor(&self) -> f32 { self.0.as_ref().lock().scale_factor() } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 4504f512551b678b9304a4c180f54b15c34af956..c2719665d423a4431184d56a9b6bff16f8ad443b 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -4390,6 +4390,13 @@ impl Window { self.platform_window.toggle_window_tab_overview() } + /// Sets the tabbing identifier for the window. + /// This is macOS specific. + pub fn set_tabbing_identifier(&self, tabbing_identifier: Option) { + self.platform_window + .set_tabbing_identifier(tabbing_identifier) + } + /// Toggles the inspector mode on this window. #[cfg(any(feature = "inspector", debug_assertions))] pub fn toggle_inspector(&mut self, cx: &mut App) { diff --git a/crates/title_bar/src/system_window_tabs.rs b/crates/title_bar/src/system_window_tabs.rs index cc50fbc2b99b56c2d8dab95e0c56deb33da2bb4b..ba898da716f042573840f8f9c9f375747ac5cc04 100644 --- a/crates/title_bar/src/system_window_tabs.rs +++ b/crates/title_bar/src/system_window_tabs.rs @@ -1,4 +1,4 @@ -use settings::Settings; +use settings::{Settings, SettingsStore}; use gpui::{ AnyWindowHandle, Context, Hsla, InteractiveElement, MouseButton, ParentElement, ScrollHandle, @@ -11,7 +11,7 @@ use ui::{ LabelSize, Tab, h_flex, prelude::*, right_click_menu, }; use workspace::{ - CloseWindow, ItemSettings, Workspace, + CloseWindow, ItemSettings, Workspace, WorkspaceSettings, item::{ClosePosition, ShowCloseButton}, }; @@ -53,6 +53,46 @@ impl SystemWindowTabs { } pub fn init(cx: &mut App) { + let mut was_use_system_window_tabs = + WorkspaceSettings::get_global(cx).use_system_window_tabs; + + cx.observe_global::(move |cx| { + let use_system_window_tabs = WorkspaceSettings::get_global(cx).use_system_window_tabs; + if use_system_window_tabs == was_use_system_window_tabs { + return; + } + was_use_system_window_tabs = use_system_window_tabs; + + let tabbing_identifier = if use_system_window_tabs { + Some(String::from("zed")) + } else { + None + }; + + if use_system_window_tabs { + SystemWindowTabController::init(cx); + } + + cx.windows().iter().for_each(|handle| { + let _ = handle.update(cx, |_, window, cx| { + window.set_tabbing_identifier(tabbing_identifier.clone()); + if use_system_window_tabs { + let tabs = if let Some(tabs) = window.tabbed_windows() { + tabs + } else { + vec![SystemWindowTab::new( + SharedString::from(window.window_title()), + window.window_handle(), + )] + }; + + SystemWindowTabController::add_tab(cx, handle.window_id(), tabs); + } + }); + }); + }) + .detach(); + cx.observe_new(|workspace: &mut Workspace, _, _| { workspace.register_action_renderer(|div, _, window, cx| { let window_id = window.window_handle().window_id(); @@ -336,6 +376,7 @@ impl SystemWindowTabs { impl Render for SystemWindowTabs { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let use_system_window_tabs = WorkspaceSettings::get_global(cx).use_system_window_tabs; let active_background_color = cx.theme().colors().title_bar_background; let inactive_background_color = cx.theme().colors().tab_bar_background; let entity = cx.entity(); @@ -368,7 +409,9 @@ impl Render for SystemWindowTabs { .collect::>(); let number_of_tabs = tab_items.len().max(1); - if !window.tab_bar_visible() && !visible { + if (!window.tab_bar_visible() && !visible) + || (!use_system_window_tabs && number_of_tabs == 1) + { return h_flex().into_any_element(); } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 79cf2bfa66fb217680dea86720eb46402f116958..d1d221fb37ddf4d76804f326f3d60ae7a09cdcbc 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -955,7 +955,7 @@ async fn installation_id() -> Result { async fn restore_or_create_workspace(app_state: Arc, cx: &mut AsyncApp) -> Result<()> { if let Some(locations) = restorable_workspace_locations(cx, &app_state).await { let use_system_window_tabs = cx - .update(|cx| WorkspaceSettings::get(None, cx).use_system_window_tabs) + .update(|cx| WorkspaceSettings::get_global(cx).use_system_window_tabs) .unwrap_or(false); let mut results: Vec> = Vec::new(); let mut tasks = Vec::new();