From b11e1d5132904ceb79a275eb88a79905891d1808 Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Sat, 21 Jun 2025 19:23:44 +0200 Subject: [PATCH 01/42] Add support for automatic window tabbing --- assets/settings/default.json | 2 + crates/agent_ui/src/ui/agent_notification.rs | 1 + crates/collab_ui/src/collab_ui.rs | 1 + crates/gpui/src/platform.rs | 5 ++ crates/gpui/src/platform/mac/window.rs | 61 +++++++++++++++++++- crates/gpui/src/window.rs | 2 + crates/workspace/src/workspace_settings.rs | 5 ++ crates/zed/src/zed.rs | 3 + docs/src/configuring-zed.md | 10 ++++ 9 files changed, 89 insertions(+), 1 deletion(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index c290baf003..e299c07403 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -357,6 +357,8 @@ // Whether to show code action buttons in the editor toolbar. "code_actions": false }, + // Whether to allow windows to tab together based on the user’s tabbing preference (macOS only). + "use_system_tabs": true, // Titlebar related settings "title_bar": { // Whether to show the branch icon beside branch switcher in the titlebar. diff --git a/crates/agent_ui/src/ui/agent_notification.rs b/crates/agent_ui/src/ui/agent_notification.rs index 68480c047f..05a2835c57 100644 --- a/crates/agent_ui/src/ui/agent_notification.rs +++ b/crates/agent_ui/src/ui/agent_notification.rs @@ -62,6 +62,7 @@ impl AgentNotification { app_id: Some(app_id.to_owned()), window_min_size: None, window_decorations: Some(WindowDecorations::Client), + allows_automatic_window_tabbing: None, } } } diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index f9a2fa4925..a8d7f308a9 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -66,5 +66,6 @@ fn notification_window_options( app_id: Some(app_id.to_owned()), window_min_size: None, window_decorations: Some(WindowDecorations::Client), + allows_automatic_window_tabbing: None, } } diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 3e002309e4..8ea29f9f70 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -1105,6 +1105,9 @@ pub struct WindowOptions { /// Whether to use client or server side decorations. Wayland only /// Note that this may be ignored. pub window_decorations: Option, + + /// Whether to allow automatic window tabbing. macOS only. + pub allows_automatic_window_tabbing: Option, } /// The variables that can be configured when creating a new window @@ -1144,6 +1147,7 @@ pub(crate) struct WindowParams { pub display_id: Option, pub window_min_size: Option>, + pub allows_automatic_window_tabbing: Option, } /// Represents the status of how a window should be opened. @@ -1194,6 +1198,7 @@ impl Default for WindowOptions { app_id: None, window_min_size: None, window_decorations: None, + allows_automatic_window_tabbing: None, } } } diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index b6f684a72c..d395682f87 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -24,6 +24,7 @@ use cocoa::{ NSUserDefaults, }, }; + use core_graphics::display::{CGDirectDisplayID, CGPoint, CGRect}; use ctor::ctor; use futures::channel::oneshot; @@ -83,6 +84,13 @@ const NSDragOperationNone: NSDragOperation = 0; #[allow(non_upper_case_globals)] const NSDragOperationCopy: NSDragOperation = 1; +#[derive(PartialEq)] +pub enum UserTabbingPreference { + Never, + Always, + InFullScreen, +} + #[link(name = "CoreGraphics", kind = "framework")] unsafe extern "C" { // Widely used private APIs; Apple uses them for their Terminal.app. @@ -534,6 +542,7 @@ impl MacWindow { show, display_id, window_min_size, + allows_automatic_window_tabbing, }: WindowParams, executor: ForegroundExecutor, renderer_context: renderer::Context, @@ -541,7 +550,12 @@ impl MacWindow { unsafe { let pool = NSAutoreleasePool::new(nil); - let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: NO]; + let allows_automatic_window_tabbing = allows_automatic_window_tabbing.unwrap_or(false); + if allows_automatic_window_tabbing { + let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: YES]; + } else { + let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: NO]; + } let mut style_mask; if let Some(titlebar) = titlebar.as_ref() { @@ -742,6 +756,24 @@ impl MacWindow { } } + let app = NSApplication::sharedApplication(nil); + let main_window: id = msg_send![app, mainWindow]; + + if !main_window.is_null() && main_window != native_window { + let main_window_is_fullscreen = main_window + .styleMask() + .contains(NSWindowStyleMask::NSFullScreenWindowMask); + let user_tabbing_preference = Self::get_user_tabbing_preference() + .unwrap_or(UserTabbingPreference::InFullScreen); + let should_add_as_tab = user_tabbing_preference == UserTabbingPreference::Always + || user_tabbing_preference == UserTabbingPreference::InFullScreen + && main_window_is_fullscreen; + + if allows_automatic_window_tabbing && should_add_as_tab { + let _: () = msg_send![main_window, addTabbedWindow: native_window ordered: 1]; + } + } + if focus && show { native_window.makeKeyAndOrderFront_(nil); } else if show { @@ -796,6 +828,33 @@ impl MacWindow { window_handles } } + + pub fn get_user_tabbing_preference() -> Option { + unsafe { + let defaults: id = NSUserDefaults::standardUserDefaults(); + let domain = NSString::alloc(nil).init_str("NSGlobalDomain"); + let key = NSString::alloc(nil).init_str("AppleWindowTabbingMode"); + + let dict: id = msg_send![defaults, persistentDomainForName: domain]; + let value: id = if !dict.is_null() { + msg_send![dict, objectForKey: key] + } else { + nil + }; + + let value_str = if !value.is_null() { + CStr::from_ptr(NSString::UTF8String(value)).to_string_lossy() + } else { + "".into() + }; + + match value_str.as_ref() { + "never" => Some(UserTabbingPreference::Never), + "always" => Some(UserTabbingPreference::Always), + _ => Some(UserTabbingPreference::InFullScreen), + } + } + } } impl Drop for MacWindow { diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 62aeb0df11..eb2abe3e99 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -944,6 +944,7 @@ impl Window { app_id, window_min_size, window_decorations, + allows_automatic_window_tabbing, } = options; let bounds = window_bounds @@ -960,6 +961,7 @@ impl Window { show, display_id, window_min_size, + allows_automatic_window_tabbing, }, )?; let display_id = platform_window.display().map(|display| display.id()); diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs index 5635347514..848d8537a5 100644 --- a/crates/workspace/src/workspace_settings.rs +++ b/crates/workspace/src/workspace_settings.rs @@ -29,6 +29,7 @@ pub struct WorkspaceSettings { pub on_last_window_closed: OnLastWindowClosed, pub resize_all_panels_in_dock: Vec, pub close_on_file_delete: bool, + pub use_system_tabs: bool, } #[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema)] @@ -202,6 +203,10 @@ pub struct WorkspaceSettingsContent { /// /// Default: false pub close_on_file_delete: Option, + /// Whether to allow windows to tab together based on the user’s tabbing preference (macOS only). + /// + /// Default: false + pub use_system_tabs: Option, } #[derive(Deserialize)] diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 93a62afc6f..cff2ede889 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -282,6 +282,8 @@ pub fn build_window_options(display_uuid: Option, cx: &mut App) -> WindowO _ => gpui::WindowDecorations::Client, }; + let use_system_tabs = WorkspaceSettings::get_global(cx).use_system_tabs; + WindowOptions { titlebar: Some(TitlebarOptions { title: None, @@ -301,6 +303,7 @@ pub fn build_window_options(display_uuid: Option, cx: &mut App) -> WindowO width: px(360.0), height: px(240.0), }), + allows_automatic_window_tabbing: Some(use_system_tabs), } } diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 9d56130256..b1ef016f46 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -1255,6 +1255,16 @@ or Each option controls displaying of a particular toolbar element. If all elements are hidden, the editor toolbar is not displayed. +## Use System Tabs + +- Description: Whether to allow windows to tab together based on the user’s tabbing preference (macOS only). +- Setting: `use_system_tabs` +- Default: `false` + +**Options** + +This setting enables integration with macOS’s native window tabbing feature. When set to `true`, Zed windows can be grouped together as tabs in a single macOS window, following the system-wide tabbing preferences set by the user (such as "Always", "In Full Screen", or "Never"). This setting is only available on macOS. + ## Enable Language Server - Description: Whether or not to use language servers to provide code intelligence. From e16423b18daad102655239942dc4cb5daf63c4e2 Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Sat, 21 Jun 2025 20:22:33 +0200 Subject: [PATCH 02/42] Add toolbar support to window --- crates/agent_ui/src/ui/agent_notification.rs | 1 + crates/collab_ui/src/collab_ui.rs | 1 + crates/gpui/src/platform.rs | 5 +++++ crates/gpui/src/platform/mac/window.rs | 10 ++++++++++ crates/gpui/src/window.rs | 2 ++ crates/rules_library/src/rules_library.rs | 2 +- crates/title_bar/src/platform_title_bar.rs | 6 ++++-- crates/zed/src/zed.rs | 3 ++- 8 files changed, 26 insertions(+), 4 deletions(-) diff --git a/crates/agent_ui/src/ui/agent_notification.rs b/crates/agent_ui/src/ui/agent_notification.rs index 05a2835c57..d1446bf8da 100644 --- a/crates/agent_ui/src/ui/agent_notification.rs +++ b/crates/agent_ui/src/ui/agent_notification.rs @@ -63,6 +63,7 @@ impl AgentNotification { window_min_size: None, window_decorations: Some(WindowDecorations::Client), allows_automatic_window_tabbing: None, + use_toolbar: None, } } } diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index a8d7f308a9..2355922ba1 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -67,5 +67,6 @@ fn notification_window_options( window_min_size: None, window_decorations: Some(WindowDecorations::Client), allows_automatic_window_tabbing: None, + use_toolbar: None, } } diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 8ea29f9f70..cca5b66200 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -1108,6 +1108,9 @@ pub struct WindowOptions { /// Whether to allow automatic window tabbing. macOS only. pub allows_automatic_window_tabbing: Option, + + /// Whether to use a toolbar as titlebar, which increases the height. macOS only. + pub use_toolbar: Option, } /// The variables that can be configured when creating a new window @@ -1148,6 +1151,7 @@ pub(crate) struct WindowParams { pub window_min_size: Option>, pub allows_automatic_window_tabbing: Option, + pub use_toolbar: Option, } /// Represents the status of how a window should be opened. @@ -1199,6 +1203,7 @@ impl Default for WindowOptions { window_min_size: None, window_decorations: None, allows_automatic_window_tabbing: None, + use_toolbar: None, } } } diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index d395682f87..a4ee97f5c2 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -376,6 +376,7 @@ struct MacWindowState { synthetic_drag_counter: usize, traffic_light_position: Option>, transparent_titlebar: bool, + use_toolbar: Option, previous_modifiers_changed_event: Option, keystroke_for_do_command: Option, do_command_handled: Option, @@ -543,6 +544,7 @@ impl MacWindow { display_id, window_min_size, allows_automatic_window_tabbing, + use_toolbar, }: WindowParams, executor: ForegroundExecutor, renderer_context: renderer::Context, @@ -674,6 +676,7 @@ impl MacWindow { external_files_dragged: false, first_mouse: false, fullscreen_restore_bounds: Bounds::default(), + use_toolbar, }))); (*native_window).set_ivar( @@ -756,6 +759,13 @@ impl MacWindow { } } + let use_toolbar = use_toolbar.unwrap_or(false); + if use_toolbar { + let identifier = NSString::alloc(nil).init_str("Toolbar"); + let toolbar = NSToolbar::alloc(nil).initWithIdentifier_(identifier); + native_window.setToolbar_(toolbar); + } + let app = NSApplication::sharedApplication(nil); let main_window: id = msg_send![app, mainWindow]; diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index eb2abe3e99..0b289a16f9 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -945,6 +945,7 @@ impl Window { window_min_size, window_decorations, allows_automatic_window_tabbing, + use_toolbar, } = options; let bounds = window_bounds @@ -962,6 +963,7 @@ impl Window { display_id, window_min_size, allows_automatic_window_tabbing, + use_toolbar, }, )?; let display_id = platform_window.display().map(|display| display.id()); diff --git a/crates/rules_library/src/rules_library.rs b/crates/rules_library/src/rules_library.rs index ec83993e5f..8332181e6a 100644 --- a/crates/rules_library/src/rules_library.rs +++ b/crates/rules_library/src/rules_library.rs @@ -130,7 +130,7 @@ pub fn open_rules_library( titlebar: Some(TitlebarOptions { title: Some("Rules Library".into()), appears_transparent: true, - traffic_light_position: Some(point(px(9.0), px(9.0))), + traffic_light_position: Default::default(), }), app_id: Some(app_id.to_owned()), window_bounds: Some(WindowBounds::Windowed(bounds)), diff --git a/crates/title_bar/src/platform_title_bar.rs b/crates/title_bar/src/platform_title_bar.rs index ef6ef93eed..5cd5a053ae 100644 --- a/crates/title_bar/src/platform_title_bar.rs +++ b/crates/title_bar/src/platform_title_bar.rs @@ -28,7 +28,7 @@ impl PlatformTitleBar { #[cfg(not(target_os = "windows"))] pub fn height(window: &mut Window) -> Pixels { - (1.75 * window.rem_size()).max(px(34.)) + (1.75 * window.rem_size()).max(px(38.)) } #[cfg(target_os = "windows")] @@ -62,6 +62,7 @@ impl Render for PlatformTitleBar { let supported_controls = window.window_controls(); let decorations = window.window_decorations(); let height = Self::height(window); + let tab_height = px(28.); let titlebar_color = self.title_bar_color(window, cx); let close_action = Box::new(workspace::CloseWindow); let children = mem::take(&mut self.children); @@ -69,7 +70,8 @@ impl Render for PlatformTitleBar { h_flex() .window_control_area(WindowControlArea::Drag) .w_full() - .h(height) + .h(height + tab_height) + .pb(tab_height) .map(|this| { if window.is_fullscreen() { this.pl_2() diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index cff2ede889..165e66f2a3 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -288,7 +288,7 @@ pub fn build_window_options(display_uuid: Option, cx: &mut App) -> WindowO titlebar: Some(TitlebarOptions { title: None, appears_transparent: true, - traffic_light_position: Some(point(px(9.0), px(9.0))), + traffic_light_position: Default::default(), }), window_bounds: None, focus: false, @@ -304,6 +304,7 @@ pub fn build_window_options(display_uuid: Option, cx: &mut App) -> WindowO height: px(240.0), }), allows_automatic_window_tabbing: Some(use_system_tabs), + use_toolbar: Some(true), } } From 6cc58b97d205fb19fe09c4c21ffcb195bd457278 Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Sat, 21 Jun 2025 20:38:24 +0200 Subject: [PATCH 03/42] Add support for new window tab button --- crates/gpui/src/app.rs | 18 ++++++++++++++++++ crates/gpui/src/platform.rs | 3 +++ crates/gpui/src/platform/mac/platform.rs | 23 +++++++++++++++++++++++ crates/zed/src/main.rs | 16 ++++++++++++++++ 4 files changed, 60 insertions(+) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 2be1a34e49..5355a4f19a 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -226,6 +226,24 @@ impl Application { pub fn path_for_auxiliary_executable(&self, name: &str) -> Result { self.0.borrow().path_for_auxiliary_executable(name) } + + /// Creates a new window to show as a tab in a tabbed window. + /// On macOS, the system automatically calls this method to create a window for a new tab when the user clicks the plus button in a tabbed window. + pub fn new_window_for_tab(&self, mut callback: F) -> &Self + where + F: 'static + FnMut(&mut App), + { + let this = Rc::downgrade(&self.0); + self.0 + .borrow_mut() + .platform + .new_window_for_tab(Box::new(move || { + if let Some(app) = this.upgrade() { + callback(&mut app.borrow_mut()); + } + })); + self + } } type Handler = Box bool + 'static>; diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index cca5b66200..cefba5b2c5 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -272,6 +272,9 @@ pub(crate) trait Platform: 'static { fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task>; fn read_credentials(&self, url: &str) -> Task)>>>; fn delete_credentials(&self, url: &str) -> Task>; + + #[cfg(any(target_os = "macos"))] + fn new_window_for_tab(&self, callback: Box); } /// A handle to a platform's display, e.g. a monitor or laptop screen. diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 57dfa9c603..42d27666b2 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -144,6 +144,11 @@ unsafe fn build_classes() { on_keyboard_layout_change as extern "C" fn(&mut Object, Sel, id), ); + decl.add_method( + sel!(newWindowForTab:), + new_window_for_tab as extern "C" fn(&mut Object, Sel, id), + ); + decl.register() } } @@ -170,6 +175,7 @@ pub(crate) struct MacPlatformState { open_urls: Option)>>, finish_launching: Option>, dock_menu: Option, + new_window_for_tab: Option>, menus: Option>, } @@ -208,6 +214,7 @@ impl MacPlatform { finish_launching: None, dock_menu: None, on_keyboard_layout_change: None, + new_window_for_tab: None, menus: None, })) } @@ -1219,6 +1226,10 @@ impl Platform for MacPlatform { Ok(()) }) } + + fn new_window_for_tab(&self, callback: Box) { + self.0.lock().new_window_for_tab = Some(callback); + } } impl MacPlatform { @@ -1492,6 +1503,18 @@ extern "C" fn handle_dock_menu(this: &mut Object, _: Sel, _: id) -> id { } } +extern "C" fn new_window_for_tab(this: &mut Object, _: Sel, _: id) { + unsafe { + let platform = get_mac_platform(this); + let mut lock = platform.0.lock(); + if let Some(mut callback) = lock.new_window_for_tab.take() { + drop(lock); + callback(); + platform.0.lock().new_window_for_tab.get_or_insert(callback); + } + } +} + unsafe fn ns_string(string: &str) -> id { unsafe { NSString::alloc(nil).init_str(string).autorelease() } } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index df30d4dd7b..8983730022 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -380,6 +380,22 @@ pub fn main() { .detach(); } }); + app.new_window_for_tab(move |cx| { + for workspace in workspace::local_workspace_windows(cx) { + workspace + .update(cx, |_view, window, cx| { + if window.is_window_active() { + window.dispatch_action( + Box::new(zed_actions::OpenRecent { + create_new_window: true, + }), + cx, + ); + } + }) + .log_err(); + } + }); app.run(move |cx| { menu::init(); From cbeead81aedc95cb59a5b8a9a127e46adbf54fff Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Sun, 22 Jun 2025 01:10:35 +0200 Subject: [PATCH 04/42] Track system tabs state and update titlebar accordingly --- crates/gpui/src/platform.rs | 1 + crates/gpui/src/platform/mac/window.rs | 71 +++++++++++++++++++++- crates/gpui/src/window.rs | 5 ++ crates/title_bar/src/platform_title_bar.rs | 11 +++- 4 files changed, 83 insertions(+), 5 deletions(-) diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index cefba5b2c5..3ac9fb1627 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -498,6 +498,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle { fn on_hit_test_window_control(&self, callback: Box Option>); fn on_close(&self, callback: Box); fn on_appearance_changed(&self, callback: Box); + fn has_system_tabs(&self) -> bool; fn draw(&self, scene: &Scene); fn completed_frame(&self) {} fn sprite_atlas(&self) -> Arc; diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index a4ee97f5c2..a00a928ca8 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -351,6 +351,16 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C conclude_drag_operation as extern "C" fn(&Object, Sel, id), ); + decl.add_method( + sel!(addTitlebarAccessoryViewController:), + add_titlebar_accessory_view_controller as extern "C" fn(&Object, Sel, id), + ); + + decl.add_method( + sel!(removeTitlebarAccessoryViewController:), + remove_titlebar_accessory_view_controller as extern "C" fn(&Object, Sel, id), + ); + decl.register() } } @@ -376,7 +386,6 @@ struct MacWindowState { synthetic_drag_counter: usize, traffic_light_position: Option>, transparent_titlebar: bool, - use_toolbar: Option, previous_modifiers_changed_event: Option, keystroke_for_do_command: Option, do_command_handled: Option, @@ -384,6 +393,7 @@ struct MacWindowState { // Whether the next left-mouse click is also the focusing click. first_mouse: bool, fullscreen_restore_bounds: Bounds, + has_system_tabs: bool, } impl MacWindowState { @@ -676,7 +686,7 @@ impl MacWindow { external_files_dragged: false, first_mouse: false, fullscreen_restore_bounds: Bounds::default(), - use_toolbar, + has_system_tabs: false, }))); (*native_window).set_ivar( @@ -731,6 +741,12 @@ impl MacWindow { WindowKind::Normal => { native_window.setLevel_(NSNormalWindowLevel); native_window.setAcceptsMouseMovedEvents_(YES); + + // Set tabbing identifier so "Merge All Windows" menu works + if allows_automatic_window_tabbing { + let tabbing_id = NSString::alloc(nil).init_str("zed-window"); + let _: () = msg_send![native_window, setTabbingIdentifier: tabbing_id]; + } } WindowKind::PopUp => { // Use a tracking area to allow receiving MouseMoved events even when @@ -1281,6 +1297,10 @@ impl PlatformWindow for MacWindow { self.0.lock().appearance_changed_callback = Some(callback); } + fn has_system_tabs(&self) -> bool { + self.0.lock().has_system_tabs + } + fn draw(&self, scene: &crate::Scene) { let mut this = self.0.lock(); this.renderer.draw(scene); @@ -1783,7 +1803,7 @@ extern "C" fn window_did_change_screen(this: &Object, _: Sel, _: id) { extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) { let window_state = unsafe { get_window_state(this) }; - let lock = window_state.lock(); + let mut lock = window_state.lock(); let is_active = unsafe { lock.native_window.isKeyWindow() == YES }; // When opening a pop-up while the application isn't active, Cocoa sends a spurious @@ -1807,6 +1827,13 @@ extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) executor .spawn(async move { let mut lock = window_state.as_ref().lock(); + + // This is required because the removeTitlebarAccessoryViewController hook does not catch all events. + // We execute this async, because otherwise the window might still report the wrong state. + unsafe { + update_tab_bar_state(&mut lock); + } + if let Some(mut callback) = lock.activate_callback.take() { drop(lock); callback(is_active); @@ -2207,6 +2234,30 @@ extern "C" fn conclude_drag_operation(this: &Object, _: Sel, _: id) { ); } +extern "C" fn add_titlebar_accessory_view_controller(this: &Object, _: Sel, view_controller: id) { + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.as_ref().lock(); + lock.has_system_tabs = true; + + unsafe { + let _: () = msg_send![super(this, class!(NSWindow)), addTitlebarAccessoryViewController: view_controller]; + } +} + +extern "C" fn remove_titlebar_accessory_view_controller( + this: &Object, + _: Sel, + view_controller: id, +) { + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.as_ref().lock(); + lock.has_system_tabs = false; + + unsafe { + let _: () = msg_send![super(this, class!(NSWindow)), removeTitlebarAccessoryViewController: view_controller]; + } +} + async fn synthetic_drag( window_state: Weak>, drag_id: usize, @@ -2342,3 +2393,17 @@ unsafe fn remove_layer_background(layer: id) { } } } + +unsafe fn update_tab_bar_state(lock: &mut MacWindowState) { + let tabbed_windows: id = msg_send![lock.native_window, tabbedWindows]; + let tabbed_windows_count: NSUInteger = if !tabbed_windows.is_null() { + msg_send![tabbed_windows, count] + } else { + 0 + }; + + let should_have_tabs = tabbed_windows_count >= 2; + if lock.has_system_tabs != should_have_tabs { + lock.has_system_tabs = should_have_tabs; + } +} diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 0b289a16f9..54a4de1526 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -4284,6 +4284,11 @@ impl Window { self.platform_window.titlebar_double_click(); } + /// Returns the number of tabbed windows in this window. + pub fn has_system_tabs(&self) -> bool { + self.platform_window.has_system_tabs() + } + /// 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/platform_title_bar.rs b/crates/title_bar/src/platform_title_bar.rs index 5cd5a053ae..93df73e291 100644 --- a/crates/title_bar/src/platform_title_bar.rs +++ b/crates/title_bar/src/platform_title_bar.rs @@ -70,8 +70,15 @@ impl Render for PlatformTitleBar { h_flex() .window_control_area(WindowControlArea::Drag) .w_full() - .h(height + tab_height) - .pb(tab_height) + .map(|this| { + if window.has_system_tabs() && window.is_fullscreen() { + this.h(height + tab_height).pt(tab_height) + } else if window.has_system_tabs() { + this.h(height + tab_height).pb(tab_height) + } else { + this.h(height) + } + }) .map(|this| { if window.is_fullscreen() { this.pl_2() From a448768a90a9765635f6dfa0c1dee81df13e5d52 Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Sun, 22 Jun 2025 01:41:03 +0200 Subject: [PATCH 05/42] Fix fullscreen toolbar presentation --- crates/gpui/src/platform/mac/window.rs | 16 ++++++++++++++++ crates/title_bar/src/platform_title_bar.rs | 4 +--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index a00a928ca8..f5f5b8976d 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -361,6 +361,12 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C remove_titlebar_accessory_view_controller as extern "C" fn(&Object, Sel, id), ); + decl.add_method( + sel!(window:willUseFullScreenPresentationOptions:), + window_will_use_fullscreen_presentation_options + as extern "C" fn(&Object, Sel, id, NSUInteger) -> NSUInteger, + ); + decl.register() } } @@ -1781,6 +1787,16 @@ extern "C" fn window_will_exit_fullscreen(this: &Object, _: Sel, _: id) { } } +extern "C" fn window_will_use_fullscreen_presentation_options( + _this: &Object, + _sel: Sel, + _window: id, + _proposed_options: NSUInteger, +) -> NSUInteger { + // NSApplicationPresentationAutoHideToolbar | NSApplicationPresentationAutoHideMenuBar | NSApplicationPresentationFullScreen + (1 << 11) | (1 << 2) | (1 << 10) +} + pub(crate) fn is_macos_version_at_least(version: NSOperatingSystemVersion) -> bool { unsafe { NSProcessInfo::processInfo(nil).isOperatingSystemAtLeastVersion(version) } } diff --git a/crates/title_bar/src/platform_title_bar.rs b/crates/title_bar/src/platform_title_bar.rs index 93df73e291..2db8048e30 100644 --- a/crates/title_bar/src/platform_title_bar.rs +++ b/crates/title_bar/src/platform_title_bar.rs @@ -71,9 +71,7 @@ impl Render for PlatformTitleBar { .window_control_area(WindowControlArea::Drag) .w_full() .map(|this| { - if window.has_system_tabs() && window.is_fullscreen() { - this.h(height + tab_height).pt(tab_height) - } else if window.has_system_tabs() { + if window.has_system_tabs() && !window.is_fullscreen() { this.h(height + tab_height).pb(tab_height) } else { this.h(height) From 0573cdab7a5f90bc5b1744a5791ecd396e461510 Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Sun, 22 Jun 2025 16:34:45 +0200 Subject: [PATCH 06/42] Refactor titlebar transparency handling for improved tab appearance --- crates/gpui/src/platform.rs | 12 +- crates/gpui/src/platform/mac/window.rs | 135 +++++++++++++++--- .../src/platform/mac/window_appearance.rs | 11 ++ crates/gpui/src/window.rs | 21 ++- crates/zed/src/main.rs | 20 ++- 5 files changed, 170 insertions(+), 29 deletions(-) diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 3ac9fb1627..12db394146 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -39,9 +39,9 @@ use crate::{ Action, AnyWindowHandle, App, AsyncWindowContext, BackgroundExecutor, Bounds, DEFAULT_WINDOW_SIZE, DevicePixels, DispatchEventResult, Font, FontId, FontMetrics, FontRun, ForegroundExecutor, GlyphId, GpuSpecs, ImageSource, Keymap, LineLayout, Pixels, PlatformInput, - Point, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, ScaledPixels, Scene, - ShapedGlyph, ShapedRun, SharedString, Size, SvgRenderer, SvgSize, Task, TaskLabel, Window, - WindowControlArea, hash, point, px, size, + Point, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Rgba, ScaledPixels, + Scene, ShapedGlyph, ShapedRun, SharedString, Size, SvgRenderer, SvgSize, Task, TaskLabel, + Window, WindowControlArea, hash, point, px, size, }; use anyhow::Result; use async_task::Runnable; @@ -498,7 +498,6 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle { fn on_hit_test_window_control(&self, callback: Box Option>); fn on_close(&self, callback: Box); fn on_appearance_changed(&self, callback: Box); - fn has_system_tabs(&self) -> bool; fn draw(&self, scene: &Scene); fn completed_frame(&self) {} fn sprite_atlas(&self) -> Arc; @@ -507,6 +506,11 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle { fn set_edited(&mut self, _edited: bool) {} fn show_character_palette(&self) {} fn titlebar_double_click(&self) {} + fn set_background_color(&self, _color: Rgba) {} + fn set_appearance(&self, _appearance: WindowAppearance) {} + fn has_system_tabs(&self) -> bool { + false + } #[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 f5f5b8976d..32ecefec58 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -3,7 +3,7 @@ use crate::{ AnyWindowHandle, Bounds, Capslock, DisplayLink, ExternalPaths, FileDropEvent, ForegroundExecutor, KeyDownEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay, - PlatformInput, PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions, + PlatformInput, PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions, Rgba, ScaledPixels, Size, Timer, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowKind, WindowParams, platform::PlatformInputHandler, point, px, size, }; @@ -32,7 +32,7 @@ use objc::{ class, declare::ClassDecl, msg_send, - runtime::{BOOL, Class, NO, Object, Protocol, Sel, YES}, + runtime::{BOOL, Class, NO, Object, Protocol, Sel, YES, class_getName}, sel, sel_impl, }; use parking_lot::Mutex; @@ -40,7 +40,7 @@ use raw_window_handle as rwh; use smallvec::SmallVec; use std::{ cell::Cell, - ffi::{CStr, c_void}, + ffi::{CStr, c_char, c_void}, mem, ops::Range, path::PathBuf, @@ -391,7 +391,6 @@ struct MacWindowState { last_key_equivalent: Option, synthetic_drag_counter: usize, traffic_light_position: Option>, - transparent_titlebar: bool, previous_modifiers_changed_event: Option, keystroke_for_do_command: Option, do_command_handled: Option, @@ -683,9 +682,7 @@ impl MacWindow { traffic_light_position: titlebar .as_ref() .and_then(|titlebar| titlebar.traffic_light_position), - transparent_titlebar: titlebar - .as_ref() - .map_or(true, |titlebar| titlebar.appears_transparent), + previous_modifiers_changed_event: None, keystroke_for_do_command: None, do_command_handled: None, @@ -722,7 +719,7 @@ impl MacWindow { } if titlebar.map_or(true, |titlebar| titlebar.appears_transparent) { - native_window.setTitlebarAppearsTransparent_(YES); + hide_titlebar_effect_view(native_window); native_window.setTitleVisibility_(NSWindowTitleVisibility::NSWindowTitleHidden); } @@ -942,6 +939,29 @@ impl PlatformWindow for MacWindow { .detach(); } + fn set_background_color(&self, color: Rgba) { + unsafe { + let native_window = self.0.lock().native_window; + let color = NSColor::colorWithSRGBRed_green_blue_alpha_( + nil, + color.r as f64, + color.g as f64, + color.b as f64, + color.a as f64, + ); + native_window.setBackgroundColor_(color); + } + } + + fn set_appearance(&self, appearance: WindowAppearance) { + unsafe { + let native_window = self.0.lock().native_window; + let appearance: id = + msg_send![class!(NSAppearance), appearanceNamed: appearance.into_native()]; + NSWindow::setAppearance(native_window, appearance); + } + } + fn scale_factor(&self) -> f32 { self.0.as_ref().lock().scale_factor() } @@ -1768,21 +1788,28 @@ extern "C" fn window_will_enter_fullscreen(this: &Object, _: Sel, _: id) { let min_version = NSOperatingSystemVersion::new(15, 3, 0); if is_macos_version_at_least(min_version) { - unsafe { - lock.native_window.setTitlebarAppearsTransparent_(NO); - } + let executor = lock.executor.clone(); + drop(lock); + executor + .spawn(async move { + let mut lock = window_state.as_ref().lock(); + + unsafe { + let color = lock.native_window.backgroundColor(); + set_fullscreen_titlebar_color(color); + } + }) + .detach(); } } -extern "C" fn window_will_exit_fullscreen(this: &Object, _: Sel, _: id) { - let window_state = unsafe { get_window_state(this) }; - let mut lock = window_state.as_ref().lock(); - +extern "C" fn window_will_exit_fullscreen(_: &Object, _: Sel, _: id) { let min_version = NSOperatingSystemVersion::new(15, 3, 0); - if is_macos_version_at_least(min_version) && lock.transparent_titlebar { + if is_macos_version_at_least(min_version) { unsafe { - lock.native_window.setTitlebarAppearsTransparent_(YES); + let color = NSColor::clearColor(nil); + set_fullscreen_titlebar_color(color); } } } @@ -2423,3 +2450,77 @@ unsafe fn update_tab_bar_state(lock: &mut MacWindowState) { lock.has_system_tabs = should_have_tabs; } } + +// By hiding the visual effect view, we allow the window's (or titlebar's in this case) +// background color to show through. If we were to set `titlebarAppearsTransparent` to true +// the selected tab would look fine, but the unselected ones and new tab button backgrounds +// would be an opaque color. When the titlebar isn't transparent, however, the system applies +// a compositing effect to the unselected tab backgrounds, which makes them blend with the +// titlebar's/window's background. +fn hide_titlebar_effect_view(native_window: id) { + unsafe { + let content_view: id = msg_send![native_window, contentView]; + let frame_view: id = msg_send![content_view, superview]; + + if let Some(titlebar_view) = find_view_by_class_name(frame_view, "NSTitlebarView") { + if let Some(effect_view) = find_view_by_class_name(titlebar_view, "NSVisualEffectView") + { + let _: () = msg_send![effect_view, setHidden: YES]; + } + } + } +} + +// When the window is fullscreen, the titlebar is only shown when moving the mouse to the top of the screen. +// The titlebar will then show as overlay on top of the window's content, so we need to make sure it's not transparent. +fn set_fullscreen_titlebar_color(color: *mut Object) { + unsafe { + let nsapp: id = msg_send![class!(NSApplication), sharedApplication]; + let windows: id = msg_send![nsapp, windows]; + let count: NSUInteger = msg_send![windows, count]; + + for i in 0..count { + let window: id = msg_send![windows, objectAtIndex: i]; + let class_name: id = msg_send![window, className]; + let class_name_str = class_name.to_str(); + + if class_name_str != "NSToolbarFullScreenWindow" { + continue; + } + + let content_view: id = msg_send![window, contentView]; + if let Some(titlebar_container) = + find_view_by_class_name(content_view, "NSTitlebarContainerView") + { + let _: () = msg_send![titlebar_container, setWantsLayer: YES]; + let layer: id = msg_send![titlebar_container, layer]; + let cg_color: *mut std::ffi::c_void = msg_send![color, CGColor]; + let _: () = msg_send![layer, setBackgroundColor: cg_color]; + } + } + } +} + +fn find_view_by_class_name(view: id, target_class: &str) -> Option { + unsafe { + let view_class: *const Class = msg_send![view, class]; + let class_name_ptr: *const c_char = class_getName(view_class); + let class_name = CStr::from_ptr(class_name_ptr).to_str().unwrap_or(""); + println!("Class name: {}", class_name); + if class_name == target_class { + return Some(view); + } + + let subviews: id = msg_send![view, subviews]; + let count: usize = msg_send![subviews, count]; + + for i in 0..count { + let subview: id = msg_send![subviews, objectAtIndex: i]; + if let Some(found) = find_view_by_class_name(subview, target_class) { + return Some(found); + } + } + } + + None +} diff --git a/crates/gpui/src/platform/mac/window_appearance.rs b/crates/gpui/src/platform/mac/window_appearance.rs index 65c409d30c..81372119fc 100644 --- a/crates/gpui/src/platform/mac/window_appearance.rs +++ b/crates/gpui/src/platform/mac/window_appearance.rs @@ -28,6 +28,17 @@ impl WindowAppearance { } } } + + pub(crate) unsafe fn into_native(self) -> id { + unsafe { + match self { + Self::VibrantLight => NSAppearanceNameVibrantLight, + Self::VibrantDark => NSAppearanceNameVibrantDark, + Self::Light => NSAppearanceNameAqua, + Self::Dark => NSAppearanceNameDarkAqua, + } + } + } } #[link(name = "AppKit", kind = "framework")] diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 54a4de1526..6709e1cd34 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -11,9 +11,9 @@ use crate::{ MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, PromptButton, PromptLevel, Quad, Render, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Replay, ResizeEdge, - SMOOTH_SVG_SCALE_FACTOR, SUBPIXEL_VARIANTS, ScaledPixels, Scene, Shadow, SharedString, Size, - StrikethroughStyle, Style, SubscriberSet, Subscription, TabHandles, TaffyLayoutEngine, Task, - TextStyle, TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle, + Rgba, SMOOTH_SVG_SCALE_FACTOR, SUBPIXEL_VARIANTS, ScaledPixels, Scene, Shadow, SharedString, + Size, StrikethroughStyle, Style, SubscriberSet, Subscription, TabHandles, TaffyLayoutEngine, + Task, TextStyle, TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem, point, prelude::*, px, rems, size, transparent_black, @@ -4279,12 +4279,25 @@ impl Window { } /// Perform titlebar double-click action. - /// This is MacOS specific. + /// This is macOS specific. pub fn titlebar_double_click(&self) { self.platform_window.titlebar_double_click(); } + /// Sets the window appearance. + /// This is macOS specific. + pub fn set_appearance(&self, appearance: WindowAppearance) { + self.platform_window.set_appearance(appearance); + } + + /// Set the background color of the window. + /// This is macOS specific. + pub fn set_background_color(&self, color: Rgba) { + self.platform_window.set_background_color(color); + } + /// Returns the number of tabbed windows in this window. + /// This is macOS specific. pub fn has_system_tabs(&self) -> bool { self.platform_window.has_system_tabs() } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 8983730022..f7c2a3e760 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -16,7 +16,10 @@ use extension_host::ExtensionStore; use fs::{Fs, RealFs}; use futures::{StreamExt, channel::oneshot, future}; use git::GitHostingProviderRegistry; -use gpui::{App, AppContext as _, Application, AsyncApp, Focusable as _, UpdateGlobal as _}; +use gpui::{ + App, AppContext as _, Application, AsyncApp, Focusable as _, UpdateGlobal as _, + WindowAppearance, +}; use gpui_tokio::Tokio; use http_client::{Url, read_proxy_from_env}; @@ -41,8 +44,8 @@ use std::{ sync::Arc, }; use theme::{ - ActiveTheme, IconThemeNotFoundError, SystemAppearance, ThemeNotFoundError, ThemeRegistry, - ThemeSettings, + ActiveTheme, Appearance, IconThemeNotFoundError, SystemAppearance, ThemeNotFoundError, + ThemeRegistry, ThemeSettings, }; use util::{ResultExt, TryFutureExt, maybe}; use uuid::Uuid; @@ -661,10 +664,19 @@ pub fn main() { let client = app_state.client.clone(); move |cx| { for &mut window in cx.windows().iter_mut() { + let title_bar_background = cx.theme().colors().title_bar_background.to_rgb(); let background_appearance = cx.theme().window_background_appearance(); + let appearance = cx.theme().appearance(); + window .update(cx, |_, window, _| { - window.set_background_appearance(background_appearance) + window.set_background_color(title_bar_background); + window.set_background_appearance(background_appearance); + + match appearance { + Appearance::Light => window.set_appearance(WindowAppearance::Light), + Appearance::Dark => window.set_appearance(WindowAppearance::Dark), + } }) .ok(); } From 44689c6017bbd7f51a70ba6578418d25578f0094 Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Sun, 22 Jun 2025 19:25:11 +0200 Subject: [PATCH 07/42] Fix support for transparent/opaque/blurred backgrounds --- crates/gpui/src/platform.rs | 2 +- crates/gpui/src/platform/mac/window.rs | 103 +++++++++---------------- crates/gpui/src/window.rs | 6 +- crates/zed/src/main.rs | 2 +- 4 files changed, 41 insertions(+), 72 deletions(-) diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 12db394146..528ce85770 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -506,7 +506,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle { fn set_edited(&mut self, _edited: bool) {} fn show_character_palette(&self) {} fn titlebar_double_click(&self) {} - fn set_background_color(&self, _color: Rgba) {} + fn set_titlebar_background_color(&self, _color: Rgba) {} fn set_appearance(&self, _appearance: WindowAppearance) {} fn has_system_tabs(&self) -> bool { false diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 32ecefec58..2ba4e36bb5 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -10,12 +10,10 @@ use crate::{ use block::ConcreteBlock; use cocoa::{ appkit::{ - NSAppKitVersionNumber, NSAppKitVersionNumber12_0, NSApplication, NSBackingStoreBuffered, - NSColor, NSEvent, NSEventModifierFlags, NSFilenamesPboardType, NSPasteboard, NSScreen, - NSView, NSViewHeightSizable, NSViewWidthSizable, NSVisualEffectMaterial, - NSVisualEffectState, NSVisualEffectView, NSWindow, NSWindowButton, - NSWindowCollectionBehavior, NSWindowOcclusionState, NSWindowOrderingMode, - NSWindowStyleMask, NSWindowTitleVisibility, + NSApplication, NSBackingStoreBuffered, NSColor, NSEvent, NSEventModifierFlags, + NSFilenamesPboardType, NSPasteboard, NSScreen, NSToolbar, NSView, NSViewHeightSizable, + NSViewWidthSizable, NSWindow, NSWindowButton, NSWindowCollectionBehavior, + NSWindowOcclusionState, NSWindowStyleMask, NSWindowTitleVisibility, }, base::{id, nil}, foundation::{ @@ -399,6 +397,7 @@ struct MacWindowState { first_mouse: bool, fullscreen_restore_bounds: Bounds, has_system_tabs: bool, + titlebar_color: Option, } impl MacWindowState { @@ -690,6 +689,7 @@ impl MacWindow { first_mouse: false, fullscreen_restore_bounds: Bounds::default(), has_system_tabs: false, + titlebar_color: None, }))); (*native_window).set_ivar( @@ -721,6 +721,7 @@ impl MacWindow { if titlebar.map_or(true, |titlebar| titlebar.appears_transparent) { hide_titlebar_effect_view(native_window); native_window.setTitleVisibility_(NSWindowTitleVisibility::NSWindowTitleHidden); + let _: () = msg_send![native_window, setTitlebarSeparatorStyle: 1]; // NSTitlebarSeparatorStyleLine } native_view.setAutoresizingMask_(NSViewWidthSizable | NSViewHeightSizable); @@ -939,18 +940,8 @@ impl PlatformWindow for MacWindow { .detach(); } - fn set_background_color(&self, color: Rgba) { - unsafe { - let native_window = self.0.lock().native_window; - let color = NSColor::colorWithSRGBRed_green_blue_alpha_( - nil, - color.r as f64, - color.g as f64, - color.b as f64, - color.a as f64, - ); - native_window.setBackgroundColor_(color); - } + fn set_titlebar_background_color(&self, color: Rgba) { + self.0.lock().titlebar_color = Some(color); } fn set_appearance(&self, appearance: WindowAppearance) { @@ -1166,57 +1157,28 @@ impl PlatformWindow for MacWindow { fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) { let mut this = self.0.as_ref().lock(); + this.renderer + .update_transparency(background_appearance != WindowBackgroundAppearance::Opaque); - let opaque = background_appearance == WindowBackgroundAppearance::Opaque; - this.renderer.update_transparency(!opaque); + let blur_radius = if background_appearance == WindowBackgroundAppearance::Blurred { + 80 + } else { + 0 + }; + let opaque = (background_appearance == WindowBackgroundAppearance::Opaque).to_objc(); unsafe { - this.native_window.setOpaque_(opaque as BOOL); - let background_color = if opaque { + this.native_window.setOpaque_(opaque); + // Shadows for transparent windows cause artifacts and performance issues + this.native_window.setHasShadow_(opaque); + let clear_color = if opaque == YES { NSColor::colorWithSRGBRed_green_blue_alpha_(nil, 0f64, 0f64, 0f64, 1f64) } else { - // Not using `+[NSColor clearColor]` to avoid broken shadow. - NSColor::colorWithSRGBRed_green_blue_alpha_(nil, 0f64, 0f64, 0f64, 0.0001) + NSColor::clearColor(nil) }; - this.native_window.setBackgroundColor_(background_color); - - if NSAppKitVersionNumber < NSAppKitVersionNumber12_0 { - // Whether `-[NSVisualEffectView respondsToSelector:@selector(_updateProxyLayer)]`. - // On macOS Catalina/Big Sur `NSVisualEffectView` doesn’t own concrete sublayers - // but uses a `CAProxyLayer`. Use the legacy WindowServer API. - let blur_radius = if background_appearance == WindowBackgroundAppearance::Blurred { - 80 - } else { - 0 - }; - - let window_number = this.native_window.windowNumber(); - CGSSetWindowBackgroundBlurRadius(CGSMainConnectionID(), window_number, blur_radius); - } else { - // On newer macOS `NSVisualEffectView` manages the effect layer directly. Using it - // could have a better performance (it downsamples the backdrop) and more control - // over the effect layer. - if background_appearance != WindowBackgroundAppearance::Blurred { - if let Some(blur_view) = this.blurred_view { - NSView::removeFromSuperview(blur_view); - this.blurred_view = None; - } - } else if this.blurred_view == None { - let content_view = this.native_window.contentView(); - let frame = NSView::bounds(content_view); - let mut blur_view: id = msg_send![BLURRED_VIEW_CLASS, alloc]; - blur_view = NSView::initWithFrame_(blur_view, frame); - blur_view.setAutoresizingMask_(NSViewWidthSizable | NSViewHeightSizable); - - let _: () = msg_send![ - content_view, - addSubview: blur_view - positioned: NSWindowOrderingMode::NSWindowBelow - relativeTo: nil - ]; - this.blurred_view = Some(blur_view.autorelease()); - } - } + this.native_window.setBackgroundColor_(clear_color); + let window_number = this.native_window.windowNumber(); + CGSSetWindowBackgroundBlurRadius(CGSMainConnectionID(), window_number, blur_radius); } } @@ -1793,10 +1755,17 @@ extern "C" fn window_will_enter_fullscreen(this: &Object, _: Sel, _: id) { executor .spawn(async move { let mut lock = window_state.as_ref().lock(); - - unsafe { - let color = lock.native_window.backgroundColor(); - set_fullscreen_titlebar_color(color); + if let Some(titlebar_color) = lock.titlebar_color { + unsafe { + let mut color = NSColor::colorWithSRGBRed_green_blue_alpha_( + nil, + titlebar_color.r as f64, + titlebar_color.g as f64, + titlebar_color.b as f64, + titlebar_color.a as f64, + ); + set_fullscreen_titlebar_color(color); + } } }) .detach(); diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 6709e1cd34..0f0ad1f120 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -4290,10 +4290,10 @@ impl Window { self.platform_window.set_appearance(appearance); } - /// Set the background color of the window. + /// Set the background color of the titlebar. /// This is macOS specific. - pub fn set_background_color(&self, color: Rgba) { - self.platform_window.set_background_color(color); + pub fn set_titlebar_background_color(&self, color: Rgba) { + self.platform_window.set_titlebar_background_color(color); } /// Returns the number of tabbed windows in this window. diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index f7c2a3e760..d3aeb20c13 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -670,7 +670,7 @@ pub fn main() { window .update(cx, |_, window, _| { - window.set_background_color(title_bar_background); + window.set_titlebar_background_color(title_bar_background); window.set_background_appearance(background_appearance); match appearance { From 330ebc3b87990e51660218285b6934a2c79e7241 Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Sun, 22 Jun 2025 22:15:12 +0200 Subject: [PATCH 08/42] Add commands for window tab management --- Cargo.lock | 1 + crates/gpui/src/platform.rs | 4 ++ crates/gpui/src/platform/mac/window.rs | 30 ++++++++++++- crates/gpui/src/window.rs | 24 +++++++++++ crates/rules_library/src/rules_library.rs | 2 +- crates/workspace/src/workspace.rs | 52 +++++++++++++++++++++++ crates/zed/Cargo.toml | 1 + crates/zed/src/main.rs | 18 +++++++- crates/zed/src/zed.rs | 4 +- 9 files changed, 131 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dc9d074f01..7ea77930de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20398,6 +20398,7 @@ dependencies = [ "collab_ui", "collections", "command_palette", + "command_palette_hooks", "component", "copilot", "crashes", diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 528ce85770..7b9621ada1 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -511,6 +511,10 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle { fn has_system_tabs(&self) -> bool { false } + fn show_next_window_tab(&self) {} + fn show_previous_window_tab(&self) {} + fn merge_all_windows(&self) {} + fn move_window_tab_to_new_window(&self) {} #[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 2ba4e36bb5..e8e95203f2 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -945,14 +945,42 @@ impl PlatformWindow for MacWindow { } fn set_appearance(&self, appearance: WindowAppearance) { + let native_window = self.0.lock().native_window; unsafe { - let native_window = self.0.lock().native_window; let appearance: id = msg_send![class!(NSAppearance), appearanceNamed: appearance.into_native()]; NSWindow::setAppearance(native_window, appearance); } } + fn show_next_window_tab(&self) { + let native_window = self.0.lock().native_window; + unsafe { + let _: () = msg_send![native_window, selectNextTab:nil]; + } + } + + fn show_previous_window_tab(&self) { + let native_window = self.0.lock().native_window; + unsafe { + let _: () = msg_send![native_window, selectPreviousTab:nil]; + } + } + + fn merge_all_windows(&self) { + let native_window = self.0.lock().native_window; + unsafe { + let _: () = msg_send![native_window, mergeAllWindows:nil]; + } + } + + fn move_window_tab_to_new_window(&self) { + let native_window = self.0.lock().native_window; + unsafe { + let _: () = msg_send![native_window, moveTabToNewWindow: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 0f0ad1f120..ff72d438b6 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -4302,6 +4302,30 @@ impl Window { self.platform_window.has_system_tabs() } + /// Selects the next tab in the tab group in the trailing direction. + /// This is macOS specific. + pub fn show_next_window_tab(&self) { + self.platform_window.show_next_window_tab() + } + + /// Selects the previous tab in the tab group in the leading direction. + /// This is macOS specific. + pub fn show_previous_window_tab(&self) { + self.platform_window.show_previous_window_tab() + } + + /// Merges all open windows into a single tabbed window. + /// This is macOS specific. + pub fn merge_all_windows(&self) { + self.platform_window.merge_all_windows() + } + + /// Moves the tab to a new containing window. + /// This is macOS specific. + pub fn move_window_tab_to_new_window(&self) { + self.platform_window.move_window_tab_to_new_window() + } + /// 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/rules_library/src/rules_library.rs b/crates/rules_library/src/rules_library.rs index 8332181e6a..5402f2a3f9 100644 --- a/crates/rules_library/src/rules_library.rs +++ b/crates/rules_library/src/rules_library.rs @@ -4,7 +4,7 @@ use editor::{CompletionProvider, SelectionEffects}; use editor::{CurrentLineHighlight, Editor, EditorElement, EditorEvent, EditorStyle, actions::Tab}; use gpui::{ Action, App, Bounds, Entity, EventEmitter, Focusable, PromptLevel, Subscription, Task, - TextStyle, TitlebarOptions, WindowBounds, WindowHandle, WindowOptions, actions, point, size, + TextStyle, TitlebarOptions, WindowBounds, WindowHandle, WindowOptions, actions, size, transparent_black, }; use language::{Buffer, LanguageRegistry, language_settings::SoftWrap}; diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 4a22107c42..4b8d7434e9 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -253,6 +253,10 @@ actions!( RestoreBanner, /// Toggles expansion of the selected item. ToggleExpandItem, + ShowNextWindowTab, + ShowPreviousWindowTab, + MergeAllWindows, + MoveWindowTabToNewWindow ] ); @@ -5572,6 +5576,22 @@ impl Workspace { workspace.activate_previous_window(cx) }), ) + .on_action(cx.listener(|workspace, _: &ShowNextWindowTab, window, cx| { + workspace.show_next_window_tab(cx, window) + })) + .on_action( + cx.listener(|workspace, _: &ShowPreviousWindowTab, window, cx| { + workspace.show_previous_window_tab(cx, window) + }), + ) + .on_action(cx.listener(|workspace, _: &MergeAllWindows, window, cx| { + workspace.merge_all_windows(cx, window) + })) + .on_action( + cx.listener(|workspace, _: &MoveWindowTabToNewWindow, window, cx| { + workspace.move_window_tab_to_new_window(cx, window) + }), + ) .on_action(cx.listener(|workspace, _: &ActivatePaneLeft, window, cx| { workspace.activate_pane_in_direction(SplitDirection::Left, window, cx) })) @@ -5891,6 +5911,38 @@ impl Workspace { .ok(); } + pub fn show_next_window_tab(&mut self, cx: &mut Context, window: &mut Window) { + cx.spawn_in(window, async move |_, cx| { + cx.update(|window, _cx| window.show_next_window_tab())?; + anyhow::Ok(()) + }) + .detach_and_log_err(cx) + } + + pub fn show_previous_window_tab(&mut self, cx: &mut Context, window: &mut Window) { + cx.spawn_in(window, async move |_, cx| { + cx.update(|window, _cx| window.show_previous_window_tab())?; + anyhow::Ok(()) + }) + .detach_and_log_err(cx) + } + + pub fn merge_all_windows(&mut self, cx: &mut Context, window: &mut Window) { + cx.spawn_in(window, async move |_, cx| { + cx.update(|window, _cx| window.merge_all_windows())?; + anyhow::Ok(()) + }) + .detach_and_log_err(cx) + } + + pub fn move_window_tab_to_new_window(&mut self, cx: &mut Context, window: &mut Window) { + cx.spawn_in(window, async move |_, cx| { + cx.update(|window, _cx| window.move_window_tab_to_new_window())?; + anyhow::Ok(()) + }) + .detach_and_log_err(cx) + } + pub fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context) { if cx.stop_active_drag(window) { return; diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index d69efaf6c0..bec68bae99 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -43,6 +43,7 @@ client.workspace = true collab_ui.workspace = true collections.workspace = true command_palette.workspace = true +command_palette_hooks.workspace = true component.workspace = true copilot.workspace = true crashes.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index d3aeb20c13..19e802fb06 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -8,6 +8,7 @@ use cli::FORCE_CLI_MODE_ENV_VAR_NAME; use client::{Client, ProxySettings, UserStore, parse_zed_link}; use collab_ui::channel_view::ChannelView; use collections::HashMap; +use command_palette_hooks::CommandPaletteFilter; use crashes::InitCrashHandler; use db::kvp::{GLOBAL_KEY_VALUE_STORE, KEY_VALUE_STORE}; use editor::Editor; @@ -37,6 +38,7 @@ use release_channel::{AppCommitSha, AppVersion, ReleaseChannel}; use session::{AppSession, Session}; use settings::{BaseKeymap, Settings, SettingsStore, watch_config_file}; use std::{ + any::TypeId, env, io::{self, IsTerminal}, path::{Path, PathBuf}, @@ -50,7 +52,8 @@ use theme::{ use util::{ResultExt, TryFutureExt, maybe}; use uuid::Uuid; use workspace::{ - AppState, SerializedWorkspaceLocation, Toast, Workspace, WorkspaceSettings, WorkspaceStore, + AppState, MergeAllWindows, MoveWindowTabToNewWindow, SerializedWorkspaceLocation, + ShowNextWindowTab, ShowPreviousWindowTab, Toast, Workspace, WorkspaceSettings, WorkspaceStore, notifications::NotificationId, }; use zed::{ @@ -691,6 +694,19 @@ pub fn main() { client.reconnect(&cx.to_async()); } } + + let use_system_tabs = WorkspaceSettings::get_global(cx).use_system_tabs; + if !use_system_tabs { + let system_window_tab_actions = [ + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + ]; + + let filter = CommandPaletteFilter::global_mut(cx); + filter.hide_action_types(&system_window_tab_actions); + } } }) .detach(); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 165e66f2a3..65cce3bb5a 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -27,8 +27,8 @@ use git_ui::project_diff::ProjectDiffToolbar; use gpui::{ Action, App, AppContext as _, Context, DismissEvent, Element, Entity, Focusable, KeyBinding, ParentElement, PathPromptOptions, PromptLevel, ReadGlobal, SharedString, Styled, Task, - TitlebarOptions, UpdateGlobal, Window, WindowKind, WindowOptions, actions, image_cache, point, - px, retain_all, + TitlebarOptions, UpdateGlobal, Window, WindowKind, WindowOptions, actions, image_cache, px, + retain_all, }; use image_viewer::ImageInfo; use language::Capability; From c2fecdc6d543b556155d2bd790de9ed28a4b8c27 Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Sun, 22 Jun 2025 23:04:06 +0200 Subject: [PATCH 09/42] Rename use_system_tabs to use_system_window_tabs --- assets/settings/default.json | 2 +- crates/workspace/src/workspace_settings.rs | 4 ++-- crates/zed/src/main.rs | 5 +++-- crates/zed/src/zed.rs | 4 ++-- docs/src/configuring-zed.md | 2 +- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index e299c07403..98a318b0e8 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -358,7 +358,7 @@ "code_actions": false }, // Whether to allow windows to tab together based on the user’s tabbing preference (macOS only). - "use_system_tabs": true, + "use_system_window_tabs": true, // Titlebar related settings "title_bar": { // Whether to show the branch icon beside branch switcher in the titlebar. diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs index 848d8537a5..48161fb421 100644 --- a/crates/workspace/src/workspace_settings.rs +++ b/crates/workspace/src/workspace_settings.rs @@ -29,7 +29,7 @@ pub struct WorkspaceSettings { pub on_last_window_closed: OnLastWindowClosed, pub resize_all_panels_in_dock: Vec, pub close_on_file_delete: bool, - pub use_system_tabs: bool, + pub use_system_window_tabs: bool, } #[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema)] @@ -206,7 +206,7 @@ pub struct WorkspaceSettingsContent { /// Whether to allow windows to tab together based on the user’s tabbing preference (macOS only). /// /// Default: false - pub use_system_tabs: Option, + pub use_system_window_tabs: Option, } #[derive(Deserialize)] diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 19e802fb06..d31eede298 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -695,8 +695,9 @@ pub fn main() { } } - let use_system_tabs = WorkspaceSettings::get_global(cx).use_system_tabs; - if !use_system_tabs { + let use_system_window_tabs = + WorkspaceSettings::get_global(cx).use_system_window_tabs; + if !use_system_window_tabs { let system_window_tab_actions = [ TypeId::of::(), TypeId::of::(), diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 65cce3bb5a..e52352b0d0 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -282,7 +282,7 @@ pub fn build_window_options(display_uuid: Option, cx: &mut App) -> WindowO _ => gpui::WindowDecorations::Client, }; - let use_system_tabs = WorkspaceSettings::get_global(cx).use_system_tabs; + let use_system_window_tabs = WorkspaceSettings::get_global(cx).use_system_window_tabs; WindowOptions { titlebar: Some(TitlebarOptions { @@ -303,7 +303,7 @@ pub fn build_window_options(display_uuid: Option, cx: &mut App) -> WindowO width: px(360.0), height: px(240.0), }), - allows_automatic_window_tabbing: Some(use_system_tabs), + allows_automatic_window_tabbing: Some(use_system_window_tabs), use_toolbar: Some(true), } } diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index b1ef016f46..2adf7d75de 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -1258,7 +1258,7 @@ Each option controls displaying of a particular toolbar element. If all elements ## Use System Tabs - Description: Whether to allow windows to tab together based on the user’s tabbing preference (macOS only). -- Setting: `use_system_tabs` +- Setting: `use_system_window_tabs` - Default: `false` **Options** From 8950272bcb59d8634ae06d2123bee19872966ea2 Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Sun, 22 Jun 2025 23:37:38 +0200 Subject: [PATCH 10/42] Rename variables and prevent lock from hanging main thread --- crates/gpui/src/platform.rs | 4 +- crates/gpui/src/platform/mac/window.rs | 68 +++++++++++++--------- crates/gpui/src/window.rs | 9 +-- crates/title_bar/src/platform_title_bar.rs | 2 +- crates/zed/src/main.rs | 2 +- 5 files changed, 48 insertions(+), 37 deletions(-) diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 7b9621ada1..05679e5b8d 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -506,9 +506,9 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle { fn set_edited(&mut self, _edited: bool) {} fn show_character_palette(&self) {} fn titlebar_double_click(&self) {} - fn set_titlebar_background_color(&self, _color: Rgba) {} + fn set_fullscreen_titlebar_background_color(&self, _color: Rgba) {} fn set_appearance(&self, _appearance: WindowAppearance) {} - fn has_system_tabs(&self) -> bool { + fn has_system_window_tabs(&self) -> bool { false } fn show_next_window_tab(&self) {} diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index e8e95203f2..33241deb66 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -13,7 +13,7 @@ use cocoa::{ NSApplication, NSBackingStoreBuffered, NSColor, NSEvent, NSEventModifierFlags, NSFilenamesPboardType, NSPasteboard, NSScreen, NSToolbar, NSView, NSViewHeightSizable, NSViewWidthSizable, NSWindow, NSWindowButton, NSWindowCollectionBehavior, - NSWindowOcclusionState, NSWindowStyleMask, NSWindowTitleVisibility, + NSWindowOcclusionState, NSWindowStyleMask, NSWindowTitleVisibility, NSWindowToolbarStyle, }, base::{id, nil}, foundation::{ @@ -389,6 +389,7 @@ struct MacWindowState { last_key_equivalent: Option, synthetic_drag_counter: usize, traffic_light_position: Option>, + transparent_titlebar: bool, previous_modifiers_changed_event: Option, keystroke_for_do_command: Option, do_command_handled: Option, @@ -396,8 +397,8 @@ struct MacWindowState { // Whether the next left-mouse click is also the focusing click. first_mouse: bool, fullscreen_restore_bounds: Bounds, - has_system_tabs: bool, - titlebar_color: Option, + has_system_window_tabs: bool, + fullscreen_titlebar_color: Option, } impl MacWindowState { @@ -681,15 +682,17 @@ impl MacWindow { traffic_light_position: titlebar .as_ref() .and_then(|titlebar| titlebar.traffic_light_position), - + transparent_titlebar: titlebar + .as_ref() + .map_or(true, |titlebar| titlebar.appears_transparent), previous_modifiers_changed_event: None, keystroke_for_do_command: None, do_command_handled: None, external_files_dragged: false, first_mouse: false, fullscreen_restore_bounds: Bounds::default(), - has_system_tabs: false, - titlebar_color: None, + has_system_window_tabs: false, + fullscreen_titlebar_color: None, }))); (*native_window).set_ivar( @@ -783,7 +786,10 @@ impl MacWindow { if use_toolbar { let identifier = NSString::alloc(nil).init_str("Toolbar"); let toolbar = NSToolbar::alloc(nil).initWithIdentifier_(identifier); + native_window.setToolbar_(toolbar); + native_window + .setToolbarStyle_(NSWindowToolbarStyle::NSWindowToolbarStyleUnifiedCompact); } let app = NSApplication::sharedApplication(nil); @@ -940,8 +946,8 @@ impl PlatformWindow for MacWindow { .detach(); } - fn set_titlebar_background_color(&self, color: Rgba) { - self.0.lock().titlebar_color = Some(color); + fn set_fullscreen_titlebar_background_color(&self, color: Rgba) { + self.0.lock().fullscreen_titlebar_color = Some(color); } fn set_appearance(&self, appearance: WindowAppearance) { @@ -1313,8 +1319,8 @@ impl PlatformWindow for MacWindow { self.0.lock().appearance_changed_callback = Some(callback); } - fn has_system_tabs(&self) -> bool { - self.0.lock().has_system_tabs + fn has_system_window_tabs(&self) -> bool { + self.0.lock().has_system_window_tabs } fn draw(&self, scene: &crate::Scene) { @@ -1777,20 +1783,22 @@ extern "C" fn window_will_enter_fullscreen(this: &Object, _: Sel, _: id) { let min_version = NSOperatingSystemVersion::new(15, 3, 0); - if is_macos_version_at_least(min_version) { + if is_macos_version_at_least(min_version) && lock.transparent_titlebar { + // Execute the fullscreen titlebar color change asynchronously, + // to ensure the new titlebar is active. let executor = lock.executor.clone(); drop(lock); executor .spawn(async move { let mut lock = window_state.as_ref().lock(); - if let Some(titlebar_color) = lock.titlebar_color { + if let Some(fullscreen_titlebar_color) = lock.fullscreen_titlebar_color { unsafe { let mut color = NSColor::colorWithSRGBRed_green_blue_alpha_( nil, - titlebar_color.r as f64, - titlebar_color.g as f64, - titlebar_color.b as f64, - titlebar_color.a as f64, + fullscreen_titlebar_color.r as f64, + fullscreen_titlebar_color.g as f64, + fullscreen_titlebar_color.b as f64, + fullscreen_titlebar_color.a as f64, ); set_fullscreen_titlebar_color(color); } @@ -1800,10 +1808,15 @@ extern "C" fn window_will_enter_fullscreen(this: &Object, _: Sel, _: id) { } } -extern "C" fn window_will_exit_fullscreen(_: &Object, _: Sel, _: id) { +extern "C" fn window_will_exit_fullscreen(this: &Object, _: Sel, _: id) { + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.as_ref().lock(); + let min_version = NSOperatingSystemVersion::new(15, 3, 0); - if is_macos_version_at_least(min_version) { + if is_macos_version_at_least(min_version) && lock.transparent_titlebar { + // Execute the fullscreen titlebar color change immediately, + // before the window exits fullscreen (and the titlebar is hidden). unsafe { let color = NSColor::clearColor(nil); set_fullscreen_titlebar_color(color); @@ -2275,13 +2288,12 @@ extern "C" fn conclude_drag_operation(this: &Object, _: Sel, _: id) { } extern "C" fn add_titlebar_accessory_view_controller(this: &Object, _: Sel, view_controller: id) { - let window_state = unsafe { get_window_state(this) }; - let mut lock = window_state.as_ref().lock(); - lock.has_system_tabs = true; - unsafe { let _: () = msg_send![super(this, class!(NSWindow)), addTitlebarAccessoryViewController: view_controller]; } + + let window_state = unsafe { get_window_state(this) }; + window_state.lock().has_system_window_tabs = true; } extern "C" fn remove_titlebar_accessory_view_controller( @@ -2289,13 +2301,12 @@ extern "C" fn remove_titlebar_accessory_view_controller( _: Sel, view_controller: id, ) { - let window_state = unsafe { get_window_state(this) }; - let mut lock = window_state.as_ref().lock(); - lock.has_system_tabs = false; - unsafe { let _: () = msg_send![super(this, class!(NSWindow)), removeTitlebarAccessoryViewController: view_controller]; } + + let window_state = unsafe { get_window_state(this) }; + window_state.lock().has_system_window_tabs = false; } async fn synthetic_drag( @@ -2443,8 +2454,8 @@ unsafe fn update_tab_bar_state(lock: &mut MacWindowState) { }; let should_have_tabs = tabbed_windows_count >= 2; - if lock.has_system_tabs != should_have_tabs { - lock.has_system_tabs = should_have_tabs; + if lock.has_system_window_tabs != should_have_tabs { + lock.has_system_window_tabs = should_have_tabs; } } @@ -2503,7 +2514,6 @@ fn find_view_by_class_name(view: id, target_class: &str) -> Option { let view_class: *const Class = msg_send![view, class]; let class_name_ptr: *const c_char = class_getName(view_class); let class_name = CStr::from_ptr(class_name_ptr).to_str().unwrap_or(""); - println!("Class name: {}", class_name); if class_name == target_class { return Some(view); } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index ff72d438b6..f01fb55b10 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -4292,14 +4292,15 @@ impl Window { /// Set the background color of the titlebar. /// This is macOS specific. - pub fn set_titlebar_background_color(&self, color: Rgba) { - self.platform_window.set_titlebar_background_color(color); + pub fn set_fullscreen_titlebar_background_color(&self, color: Rgba) { + self.platform_window + .set_fullscreen_titlebar_background_color(color); } /// Returns the number of tabbed windows in this window. /// This is macOS specific. - pub fn has_system_tabs(&self) -> bool { - self.platform_window.has_system_tabs() + pub fn has_system_window_tabs(&self) -> bool { + self.platform_window.has_system_window_tabs() } /// Selects the next tab in the tab group in the trailing direction. diff --git a/crates/title_bar/src/platform_title_bar.rs b/crates/title_bar/src/platform_title_bar.rs index 2db8048e30..e66b2852b9 100644 --- a/crates/title_bar/src/platform_title_bar.rs +++ b/crates/title_bar/src/platform_title_bar.rs @@ -71,7 +71,7 @@ impl Render for PlatformTitleBar { .window_control_area(WindowControlArea::Drag) .w_full() .map(|this| { - if window.has_system_tabs() && !window.is_fullscreen() { + if window.has_system_window_tabs() && !window.is_fullscreen() { this.h(height + tab_height).pb(tab_height) } else { this.h(height) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index d31eede298..b828f13a46 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -673,7 +673,7 @@ pub fn main() { window .update(cx, |_, window, _| { - window.set_titlebar_background_color(title_bar_background); + window.set_fullscreen_titlebar_background_color(title_bar_background); window.set_background_appearance(background_appearance); match appearance { From 33bbd544500e7bc7bfa3af6d60c517acb82bdb07 Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Mon, 23 Jun 2025 00:09:31 +0200 Subject: [PATCH 11/42] Add VS Code setting importer for use_system_window_tabs --- assets/settings/default.json | 2 +- crates/workspace/src/workspace_settings.rs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 98a318b0e8..6b1f8df59f 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -358,7 +358,7 @@ "code_actions": false }, // Whether to allow windows to tab together based on the user’s tabbing preference (macOS only). - "use_system_window_tabs": true, + "use_system_window_tabs": false, // Titlebar related settings "title_bar": { // Whether to show the branch icon beside branch switcher in the titlebar. diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs index 48161fb421..bc7f1d869c 100644 --- a/crates/workspace/src/workspace_settings.rs +++ b/crates/workspace/src/workspace_settings.rs @@ -6,7 +6,7 @@ use collections::HashMap; use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, VsCodeSettings}; #[derive(Deserialize)] pub struct WorkspaceSettings { @@ -355,6 +355,8 @@ impl Settings for WorkspaceSettings { current.max_tabs = Some(n) } + vscode.bool_setting("window.nativeTabs", &mut current.use_system_window_tabs); + // some combination of "window.restoreWindows" and "workbench.startupEditor" might // map to our "restore_on_startup" From 2c0e6466198c4ca8dfe9b7759a90815dd09cd15f Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Mon, 23 Jun 2025 19:08:20 +0200 Subject: [PATCH 12/42] Improve tabbar state syncing --- crates/gpui/src/platform.rs | 1 + crates/gpui/src/platform/mac/window.rs | 30 +++++++++++++++++----- crates/gpui/src/window.rs | 18 ++++++++++++- crates/workspace/src/workspace_settings.rs | 2 +- 4 files changed, 43 insertions(+), 8 deletions(-) diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 05679e5b8d..3374471425 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -511,6 +511,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle { fn has_system_window_tabs(&self) -> bool { false } + fn refresh_has_system_window_tabs(&self) {} fn show_next_window_tab(&self) {} fn show_previous_window_tab(&self) {} fn merge_all_windows(&self) {} diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 33241deb66..7921f7f723 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -1323,6 +1323,30 @@ impl PlatformWindow for MacWindow { self.0.lock().has_system_window_tabs } + fn refresh_has_system_window_tabs(&self) { + let mut this = self.0.lock(); + let executor = this.executor.clone(); + let native_window = this.native_window; + let has_system_window_tabs = &mut this.has_system_window_tabs as *mut bool; + executor + .spawn(async move { + unsafe { + let tabbed_windows: id = msg_send![native_window, tabbedWindows]; + let tabbed_windows_count: NSUInteger = if !tabbed_windows.is_null() { + msg_send![tabbed_windows, count] + } else { + 0 + }; + + let should_have_tabs = tabbed_windows_count >= 2; + if *has_system_window_tabs != should_have_tabs { + *has_system_window_tabs = should_have_tabs; + } + } + }) + .detach(); + } + fn draw(&self, scene: &crate::Scene) { let mut this = self.0.lock(); this.renderer.draw(scene); @@ -1881,12 +1905,6 @@ extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) .spawn(async move { let mut lock = window_state.as_ref().lock(); - // This is required because the removeTitlebarAccessoryViewController hook does not catch all events. - // We execute this async, because otherwise the window might still report the wrong state. - unsafe { - update_tab_bar_state(&mut lock); - } - if let Some(mut callback) = lock.activate_callback.take() { drop(lock); callback(is_active); diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index f01fb55b10..00002fbae9 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -995,9 +995,19 @@ impl Window { } platform_window.on_close(Box::new({ + let windows = cx.windows(); let mut cx = cx.to_async(); move || { let _ = handle.update(&mut cx, |_, window, _| window.remove_window()); + + windows + .into_iter() + .filter(|w| w.window_id() != handle.window_id()) + .for_each(|window| { + let _ = window.update(&mut cx, |_, window, _| { + window.refresh_has_system_window_tabs(); + }); + }); } })); platform_window.on_request_frame(Box::new({ @@ -4297,12 +4307,18 @@ impl Window { .set_fullscreen_titlebar_background_color(color); } - /// Returns the number of tabbed windows in this window. + /// Returns whether the window has more then 1 tab (therefore showing the tab bar). /// This is macOS specific. pub fn has_system_window_tabs(&self) -> bool { self.platform_window.has_system_window_tabs() } + /// Syncs the window's tab state. + /// This is macOS specific. + pub fn refresh_has_system_window_tabs(&self) { + self.platform_window.refresh_has_system_window_tabs(); + } + /// Selects the next tab in the tab group in the trailing direction. /// This is macOS specific. pub fn show_next_window_tab(&self) { diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs index bc7f1d869c..e25ebaa4ab 100644 --- a/crates/workspace/src/workspace_settings.rs +++ b/crates/workspace/src/workspace_settings.rs @@ -6,7 +6,7 @@ use collections::HashMap; use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources, VsCodeSettings}; +use settings::{Settings, SettingsSources}; #[derive(Deserialize)] pub struct WorkspaceSettings { From ea00bae73ab211d44c8e32ba41d0b2027139b43a Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Mon, 23 Jun 2025 19:24:53 +0200 Subject: [PATCH 13/42] Fix system window tab commands not showing after setting change --- crates/zed/src/main.rs | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index b828f13a46..9c6262c0d5 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -697,16 +697,21 @@ pub fn main() { let use_system_window_tabs = WorkspaceSettings::get_global(cx).use_system_window_tabs; - if !use_system_window_tabs { - let system_window_tab_actions = [ - TypeId::of::(), - TypeId::of::(), - TypeId::of::(), - TypeId::of::(), - ]; + let system_window_tab_actions = [ + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + ]; - let filter = CommandPaletteFilter::global_mut(cx); - filter.hide_action_types(&system_window_tab_actions); + if use_system_window_tabs { + CommandPaletteFilter::update_global(cx, |filter, _cx| { + filter.show_action_types(system_window_tab_actions.iter()); + }); + } else { + CommandPaletteFilter::update_global(cx, |filter, _cx| { + filter.hide_action_types(&system_window_tab_actions); + }); } } }) From 0fac0d7d931328968f001aa51496fed1eb51bda9 Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Mon, 23 Jun 2025 19:26:09 +0200 Subject: [PATCH 14/42] Apply fullscreen titlebar background color immediately when necessary --- crates/gpui/src/platform/mac/window.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 7921f7f723..5b074b1518 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -948,6 +948,19 @@ impl PlatformWindow for MacWindow { fn set_fullscreen_titlebar_background_color(&self, color: Rgba) { self.0.lock().fullscreen_titlebar_color = Some(color); + + if self.is_fullscreen() { + unsafe { + let mut color = NSColor::colorWithSRGBRed_green_blue_alpha_( + nil, + color.r as f64, + color.g as f64, + color.b as f64, + color.a as f64, + ); + set_fullscreen_titlebar_color(color); + } + } } fn set_appearance(&self, appearance: WindowAppearance) { From 6f553bdd1555ae47da1adb7da6ed4f0784c042c9 Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Tue, 24 Jun 2025 18:48:14 +0200 Subject: [PATCH 15/42] Fix incorrect enum mapping --- crates/gpui/src/platform/mac/window.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 5b074b1518..87f2fb45c1 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -885,7 +885,7 @@ impl MacWindow { }; match value_str.as_ref() { - "never" => Some(UserTabbingPreference::Never), + "manual" => Some(UserTabbingPreference::Never), "always" => Some(UserTabbingPreference::Always), _ => Some(UserTabbingPreference::InFullScreen), } From f589cc457073e277bcdc46b50bbef5a44b382b22 Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Tue, 24 Jun 2025 18:48:50 +0200 Subject: [PATCH 16/42] Better detect tabbar changes --- crates/gpui/src/platform/mac/window.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 87f2fb45c1..36630b34d2 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -1918,6 +1918,20 @@ extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) .spawn(async move { let mut lock = window_state.as_ref().lock(); + unsafe { + let tabbed_windows: id = msg_send![lock.native_window, tabbedWindows]; + let tabbed_windows_count: NSUInteger = if !tabbed_windows.is_null() { + msg_send![tabbed_windows, count] + } else { + 0 + }; + + let should_have_tabs = tabbed_windows_count >= 2; + if lock.has_system_window_tabs != should_have_tabs { + lock.has_system_window_tabs = should_have_tabs; + } + } + if let Some(mut callback) = lock.activate_callback.take() { drop(lock); callback(is_active); From 1ac717451818019f83cd637bdc7f3c8db2668c1f Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Wed, 25 Jun 2025 23:37:06 +0200 Subject: [PATCH 17/42] Refactor to GPUI implementation --- Cargo.lock | 2 +- crates/agent_ui/src/ui/agent_notification.rs | 1 - crates/collab_ui/src/collab_ui.rs | 1 - crates/gpui/src/app.rs | 182 +++++++- crates/gpui/src/platform.rs | 34 +- crates/gpui/src/platform/mac/platform.rs | 23 - crates/gpui/src/platform/mac/window.rs | 412 +++++++----------- .../src/platform/mac/window_appearance.rs | 11 - crates/gpui/src/window.rs | 136 +++--- crates/rules_library/src/rules_library.rs | 6 +- crates/title_bar/Cargo.toml | 1 + crates/title_bar/src/platform_title_bar.rs | 33 +- crates/title_bar/src/system_window_tabs.rs | 353 +++++++++++++++ crates/title_bar/src/title_bar.rs | 3 +- crates/workspace/src/workspace.rs | 63 +-- crates/zed/Cargo.toml | 1 - crates/zed/src/main.rs | 60 +-- crates/zed/src/zed.rs | 7 +- 18 files changed, 813 insertions(+), 516 deletions(-) create mode 100644 crates/title_bar/src/system_window_tabs.rs diff --git a/Cargo.lock b/Cargo.lock index 7ea77930de..f4941a1abb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16711,6 +16711,7 @@ dependencies = [ "client", "cloud_llm_client", "collections", + "command_palette_hooks", "db", "gpui", "http_client", @@ -20398,7 +20399,6 @@ dependencies = [ "collab_ui", "collections", "command_palette", - "command_palette_hooks", "component", "copilot", "crashes", diff --git a/crates/agent_ui/src/ui/agent_notification.rs b/crates/agent_ui/src/ui/agent_notification.rs index d1446bf8da..05a2835c57 100644 --- a/crates/agent_ui/src/ui/agent_notification.rs +++ b/crates/agent_ui/src/ui/agent_notification.rs @@ -63,7 +63,6 @@ impl AgentNotification { window_min_size: None, window_decorations: Some(WindowDecorations::Client), allows_automatic_window_tabbing: None, - use_toolbar: None, } } } diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 2355922ba1..a8d7f308a9 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -67,6 +67,5 @@ fn notification_window_options( window_min_size: None, window_decorations: Some(WindowDecorations::Client), allows_automatic_window_tabbing: None, - use_toolbar: None, } } diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 5355a4f19a..a086c571f9 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -39,8 +39,8 @@ use crate::{ Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, Point, PromptBuilder, PromptButton, PromptHandle, PromptLevel, Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource, - SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, Window, WindowAppearance, - WindowHandle, WindowId, WindowInvalidator, + SharedString, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, Window, + WindowAppearance, WindowHandle, WindowId, WindowInvalidator, colors::{Colors, GlobalColors}, current_platform, hash, init_app_menus, }; @@ -226,24 +226,6 @@ impl Application { pub fn path_for_auxiliary_executable(&self, name: &str) -> Result { self.0.borrow().path_for_auxiliary_executable(name) } - - /// Creates a new window to show as a tab in a tabbed window. - /// On macOS, the system automatically calls this method to create a window for a new tab when the user clicks the plus button in a tabbed window. - pub fn new_window_for_tab(&self, mut callback: F) -> &Self - where - F: 'static + FnMut(&mut App), - { - let this = Rc::downgrade(&self.0); - self.0 - .borrow_mut() - .platform - .new_window_for_tab(Box::new(move || { - if let Some(app) = this.upgrade() { - callback(&mut app.borrow_mut()); - } - })); - self - } } type Handler = Box bool + 'static>; @@ -255,6 +237,165 @@ type WindowClosedHandler = Box; type ReleaseListener = Box; type NewEntityListener = Box, &mut App) + 'static>; +#[doc(hidden)] +#[derive(Clone)] +pub struct SystemWindowTab { + pub id: WindowId, + pub title: SharedString, + pub handle: AnyWindowHandle, +} + +impl SystemWindowTab { + /// Create a new instance of the window tab. + pub fn new(id: WindowId, title: SharedString, handle: AnyWindowHandle) -> Self { + Self { id, title, handle } + } +} + +/// A controller for managing window tabs. +#[derive(Default)] +pub struct SystemWindowTabController { + tabs: FxHashMap>, +} + +impl Global for SystemWindowTabController {} + +impl SystemWindowTabController { + /// Create a new instance of the window tab controller. + pub fn new() -> Self { + Self { + tabs: FxHashMap::default(), + } + } + + /// Initialize the global window tab controller. + pub fn init(cx: &mut App) { + cx.set_global(SystemWindowTabController::new()); + } + + /// Get all tabs. + pub fn tabs(&self) -> &FxHashMap> { + &self.tabs + } + + /// Get all windows in a tab. + pub fn windows(&self, tab_group: usize) -> Option<&Vec> { + self.tabs.get(&tab_group) + } + + /// Add a window to a tab group. + pub fn add_window( + cx: &mut App, + tab_group: usize, + handle: AnyWindowHandle, + title: SharedString, + ) { + let mut controller = cx.global_mut::(); + let id = handle.window_id(); + + for (existing_group, windows) in controller.tabs.iter_mut() { + if *existing_group != tab_group { + if let Some(pos) = windows.iter().position(|tab| tab.id == id) { + windows.remove(pos); + } + } + } + + controller.tabs.retain(|_, windows| !windows.is_empty()); + let windows = controller.tabs.entry(tab_group).or_insert_with(Vec::new); + if !windows.iter().any(|tab| tab.id == id) { + windows.push(SystemWindowTab::new(id, title, handle)); + } + } + + /// Remove a window from a tab group. + pub fn remove_window(cx: &mut App, id: WindowId) { + let mut controller = cx.global_mut::(); + controller.tabs.retain(|_, windows| { + if let Some(pos) = windows.iter().position(|tab| tab.id == id) { + windows.remove(pos); + } + !windows.is_empty() + }); + } + + /// Move window to a new position within the same tab group. + pub fn update_window_position(cx: &mut App, id: WindowId, ix: usize) { + let mut controller = cx.global_mut::(); + for (_, windows) in controller.tabs.iter_mut() { + if let Some(current_pos) = windows.iter().position(|tab| tab.id == id) { + if ix < windows.len() && current_pos != ix { + let window_tab = windows.remove(current_pos); + windows.insert(ix, window_tab); + } + break; + } + } + } + + /// Update the title of a window. + pub fn update_window_title(cx: &mut App, id: WindowId, title: SharedString) { + let mut controller = cx.global_mut::(); + for windows in controller.tabs.values_mut() { + for tab in windows.iter_mut() { + if tab.id == id { + tab.title = title.clone(); + } + } + } + } + + /// Merge all windows to a single tab group. + pub fn merge_all_windows(cx: &mut App, tab_group: usize) { + let mut controller = cx.global_mut::(); + let mut all_windows = Vec::new(); + for windows in controller.tabs.values() { + all_windows.extend(windows.iter().cloned()); + } + + controller.tabs.clear(); + if !all_windows.is_empty() { + controller.tabs.insert(tab_group, all_windows); + } + } + + /// Selects the next tab in the tab group in the trailing direction. + pub fn select_next_tab(cx: &mut App, tab_group: usize, id: WindowId) { + let mut controller = cx.global_mut::(); + let windows = controller.tabs.get_mut(&tab_group).unwrap(); + let current_index = windows.iter().position(|tab| tab.id == id).unwrap(); + let next_index = (current_index + 1) % windows.len(); + + let _ = &windows[next_index].handle.update(cx, |_, window, _| { + window.activate_window(); + }); + } + + /// Selects the previous tab in the tab group in the leading direction. + pub fn select_previous_tab(cx: &mut App, tab_group: usize, id: WindowId) { + log::info!("select_previous_tab"); + let mut controller = cx.global_mut::(); + let windows = controller.tabs.get_mut(&tab_group).unwrap(); + let current_index = windows.iter().position(|tab| tab.id == id).unwrap(); + log::info!("current_index: {}", current_index); + let previous_index = if current_index == 0 { + windows.len() - 1 + } else { + current_index - 1 + }; + log::info!("previous_index: {}", previous_index); + + let result = &windows[previous_index].handle.update(cx, |_, window, _| { + log::info!("activate_window"); + window.activate_window(); + }); + + if let Err(err) = result { + log::info!("Error activating window: {}", err); + } + } +} + /// Contains the state of the full application, and passed as a reference to a variety of callbacks. /// Other [Context] derefs to this type. /// You need a reference to an `App` to access the state of a [Entity]. @@ -387,6 +528,7 @@ impl App { }); init_app_menus(platform.as_ref(), &app.borrow()); + SystemWindowTabController::init(&mut app.borrow_mut()); platform.on_keyboard_layout_change(Box::new({ let app = Rc::downgrade(&app); diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 3374471425..b6e8068262 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -39,9 +39,9 @@ use crate::{ Action, AnyWindowHandle, App, AsyncWindowContext, BackgroundExecutor, Bounds, DEFAULT_WINDOW_SIZE, DevicePixels, DispatchEventResult, Font, FontId, FontMetrics, FontRun, ForegroundExecutor, GlyphId, GpuSpecs, ImageSource, Keymap, LineLayout, Pixels, PlatformInput, - Point, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Rgba, ScaledPixels, - Scene, ShapedGlyph, ShapedRun, SharedString, Size, SvgRenderer, SvgSize, Task, TaskLabel, - Window, WindowControlArea, hash, point, px, size, + Point, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, ScaledPixels, Scene, + ShapedGlyph, ShapedRun, SharedString, Size, SvgRenderer, SvgSize, Task, TaskLabel, Window, + WindowControlArea, hash, point, px, size, }; use anyhow::Result; use async_task::Runnable; @@ -272,9 +272,6 @@ pub(crate) trait Platform: 'static { fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task>; fn read_credentials(&self, url: &str) -> Task)>>>; fn delete_credentials(&self, url: &str) -> Task>; - - #[cfg(any(target_os = "macos"))] - fn new_window_for_tab(&self, callback: Box); } /// A handle to a platform's display, e.g. a monitor or laptop screen. @@ -503,19 +500,21 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle { fn sprite_atlas(&self) -> Arc; // macOS specific methods + fn get_title(&self) -> String { + String::new() + } fn set_edited(&mut self, _edited: bool) {} fn show_character_palette(&self) {} fn titlebar_double_click(&self) {} - fn set_fullscreen_titlebar_background_color(&self, _color: Rgba) {} - fn set_appearance(&self, _appearance: WindowAppearance) {} - fn has_system_window_tabs(&self) -> bool { - false - } - fn refresh_has_system_window_tabs(&self) {} - fn show_next_window_tab(&self) {} - fn show_previous_window_tab(&self) {} + fn on_select_previous_tab(&self, _callback: Box) {} + fn on_select_next_tab(&self, _callback: Box) {} + fn on_merge_all_windows(&self, _callback: Box) {} fn merge_all_windows(&self) {} - fn move_window_tab_to_new_window(&self) {} + fn move_tab_to_new_window(&self) {} + fn toggle_window_tab_overview(&self) {} + fn tab_group(&self) -> Option { + None + } #[cfg(target_os = "windows")] fn get_raw_handle(&self) -> windows::HWND; @@ -1121,9 +1120,6 @@ pub struct WindowOptions { /// Whether to allow automatic window tabbing. macOS only. pub allows_automatic_window_tabbing: Option, - - /// Whether to use a toolbar as titlebar, which increases the height. macOS only. - pub use_toolbar: Option, } /// The variables that can be configured when creating a new window @@ -1164,7 +1160,6 @@ pub(crate) struct WindowParams { pub window_min_size: Option>, pub allows_automatic_window_tabbing: Option, - pub use_toolbar: Option, } /// Represents the status of how a window should be opened. @@ -1216,7 +1211,6 @@ impl Default for WindowOptions { window_min_size: None, window_decorations: None, allows_automatic_window_tabbing: None, - use_toolbar: None, } } } diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 42d27666b2..57dfa9c603 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -144,11 +144,6 @@ unsafe fn build_classes() { on_keyboard_layout_change as extern "C" fn(&mut Object, Sel, id), ); - decl.add_method( - sel!(newWindowForTab:), - new_window_for_tab as extern "C" fn(&mut Object, Sel, id), - ); - decl.register() } } @@ -175,7 +170,6 @@ pub(crate) struct MacPlatformState { open_urls: Option)>>, finish_launching: Option>, dock_menu: Option, - new_window_for_tab: Option>, menus: Option>, } @@ -214,7 +208,6 @@ impl MacPlatform { finish_launching: None, dock_menu: None, on_keyboard_layout_change: None, - new_window_for_tab: None, menus: None, })) } @@ -1226,10 +1219,6 @@ impl Platform for MacPlatform { Ok(()) }) } - - fn new_window_for_tab(&self, callback: Box) { - self.0.lock().new_window_for_tab = Some(callback); - } } impl MacPlatform { @@ -1503,18 +1492,6 @@ extern "C" fn handle_dock_menu(this: &mut Object, _: Sel, _: id) -> id { } } -extern "C" fn new_window_for_tab(this: &mut Object, _: Sel, _: id) { - unsafe { - let platform = get_mac_platform(this); - let mut lock = platform.0.lock(); - if let Some(mut callback) = lock.new_window_for_tab.take() { - drop(lock); - callback(); - platform.0.lock().new_window_for_tab.get_or_insert(callback); - } - } -} - unsafe fn ns_string(string: &str) -> id { unsafe { NSString::alloc(nil).init_str(string).autorelease() } } diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 36630b34d2..ecc9ef2c3a 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -3,17 +3,19 @@ use crate::{ AnyWindowHandle, Bounds, Capslock, DisplayLink, ExternalPaths, FileDropEvent, ForegroundExecutor, KeyDownEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay, - PlatformInput, PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions, Rgba, + PlatformInput, PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions, ScaledPixels, Size, Timer, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowKind, WindowParams, platform::PlatformInputHandler, point, px, size, }; use block::ConcreteBlock; use cocoa::{ appkit::{ - NSApplication, NSBackingStoreBuffered, NSColor, NSEvent, NSEventModifierFlags, - NSFilenamesPboardType, NSPasteboard, NSScreen, NSToolbar, NSView, NSViewHeightSizable, - NSViewWidthSizable, NSWindow, NSWindowButton, NSWindowCollectionBehavior, - NSWindowOcclusionState, NSWindowStyleMask, NSWindowTitleVisibility, NSWindowToolbarStyle, + NSAppKitVersionNumber, NSAppKitVersionNumber12_0, NSApplication, NSBackingStoreBuffered, + NSColor, NSEvent, NSEventModifierFlags, NSFilenamesPboardType, NSPasteboard, NSScreen, + NSView, NSViewHeightSizable, NSViewWidthSizable, NSVisualEffectMaterial, + NSVisualEffectState, NSVisualEffectView, NSWindow, NSWindowButton, + NSWindowCollectionBehavior, NSWindowOcclusionState, NSWindowOrderingMode, + NSWindowStyleMask, NSWindowTitleVisibility, }, base::{id, nil}, foundation::{ @@ -30,7 +32,7 @@ use objc::{ class, declare::ClassDecl, msg_send, - runtime::{BOOL, Class, NO, Object, Protocol, Sel, YES, class_getName}, + runtime::{BOOL, Class, NO, Object, Protocol, Sel, YES}, sel, sel_impl, }; use parking_lot::Mutex; @@ -38,7 +40,7 @@ use raw_window_handle as rwh; use smallvec::SmallVec; use std::{ cell::Cell, - ffi::{CStr, c_char, c_void}, + ffi::{CStr, c_void}, mem, ops::Range, path::PathBuf, @@ -355,14 +357,18 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C ); decl.add_method( - sel!(removeTitlebarAccessoryViewController:), - remove_titlebar_accessory_view_controller as extern "C" fn(&Object, Sel, id), + sel!(selectNextTab:), + select_next_tab as extern "C" fn(&Object, Sel, id), ); decl.add_method( - sel!(window:willUseFullScreenPresentationOptions:), - window_will_use_fullscreen_presentation_options - as extern "C" fn(&Object, Sel, id, NSUInteger) -> NSUInteger, + sel!(selectPreviousTab:), + select_previous_tab as extern "C" fn(&Object, Sel, id), + ); + + decl.add_method( + sel!(mergeAllWindows:), + merge_all_windows as extern "C" fn(&Object, Sel, id), ); decl.register() @@ -397,8 +403,9 @@ struct MacWindowState { // Whether the next left-mouse click is also the focusing click. first_mouse: bool, fullscreen_restore_bounds: Bounds, - has_system_window_tabs: bool, - fullscreen_titlebar_color: Option, + select_next_tab_callback: Option>, + select_previous_tab_callback: Option>, + merge_all_windows_callback: Option>, } impl MacWindowState { @@ -559,7 +566,6 @@ impl MacWindow { display_id, window_min_size, allows_automatic_window_tabbing, - use_toolbar, }: WindowParams, executor: ForegroundExecutor, renderer_context: renderer::Context, @@ -691,8 +697,9 @@ impl MacWindow { external_files_dragged: false, first_mouse: false, fullscreen_restore_bounds: Bounds::default(), - has_system_window_tabs: false, - fullscreen_titlebar_color: None, + select_next_tab_callback: None, + select_previous_tab_callback: None, + merge_all_windows_callback: None, }))); (*native_window).set_ivar( @@ -722,9 +729,8 @@ impl MacWindow { } if titlebar.map_or(true, |titlebar| titlebar.appears_transparent) { - hide_titlebar_effect_view(native_window); + native_window.setTitlebarAppearsTransparent_(YES); native_window.setTitleVisibility_(NSWindowTitleVisibility::NSWindowTitleHidden); - let _: () = msg_send![native_window, setTitlebarSeparatorStyle: 1]; // NSTitlebarSeparatorStyleLine } native_view.setAutoresizingMask_(NSViewWidthSizable | NSViewHeightSizable); @@ -782,16 +788,6 @@ impl MacWindow { } } - let use_toolbar = use_toolbar.unwrap_or(false); - if use_toolbar { - let identifier = NSString::alloc(nil).init_str("Toolbar"); - let toolbar = NSToolbar::alloc(nil).initWithIdentifier_(identifier); - - native_window.setToolbar_(toolbar); - native_window - .setToolbarStyle_(NSWindowToolbarStyle::NSWindowToolbarStyleUnifiedCompact); - } - let app = NSApplication::sharedApplication(nil); let main_window: id = msg_send![app, mainWindow]; @@ -806,7 +802,13 @@ impl MacWindow { && main_window_is_fullscreen; if allows_automatic_window_tabbing && should_add_as_tab { - let _: () = msg_send![main_window, addTabbedWindow: native_window ordered: 1]; + let main_window_can_tab: BOOL = + msg_send![main_window, respondsToSelector: sel!(addTabbedWindow:ordered:)]; + let main_window_visible: BOOL = msg_send![main_window, isVisible]; + + if main_window_can_tab == YES && main_window_visible == YES { + let _: () = msg_send![main_window, addTabbedWindow: native_window ordered: NSWindowOrderingMode::NSWindowAbove]; + } } } @@ -946,46 +948,6 @@ impl PlatformWindow for MacWindow { .detach(); } - fn set_fullscreen_titlebar_background_color(&self, color: Rgba) { - self.0.lock().fullscreen_titlebar_color = Some(color); - - if self.is_fullscreen() { - unsafe { - let mut color = NSColor::colorWithSRGBRed_green_blue_alpha_( - nil, - color.r as f64, - color.g as f64, - color.b as f64, - color.a as f64, - ); - set_fullscreen_titlebar_color(color); - } - } - } - - fn set_appearance(&self, appearance: WindowAppearance) { - let native_window = self.0.lock().native_window; - unsafe { - let appearance: id = - msg_send![class!(NSAppearance), appearanceNamed: appearance.into_native()]; - NSWindow::setAppearance(native_window, appearance); - } - } - - fn show_next_window_tab(&self) { - let native_window = self.0.lock().native_window; - unsafe { - let _: () = msg_send![native_window, selectNextTab:nil]; - } - } - - fn show_previous_window_tab(&self) { - let native_window = self.0.lock().native_window; - unsafe { - let _: () = msg_send![native_window, selectPreviousTab:nil]; - } - } - fn merge_all_windows(&self) { let native_window = self.0.lock().native_window; unsafe { @@ -993,13 +955,20 @@ impl PlatformWindow for MacWindow { } } - fn move_window_tab_to_new_window(&self) { + fn move_tab_to_new_window(&self) { let native_window = self.0.lock().native_window; unsafe { let _: () = msg_send![native_window, moveTabToNewWindow:nil]; } } + fn toggle_window_tab_overview(&self) { + let native_window = self.0.lock().native_window; + unsafe { + let _: () = msg_send![native_window, toggleTabOverview:nil]; + } + } + fn scale_factor(&self) -> f32 { self.0.as_ref().lock().scale_factor() } @@ -1200,32 +1169,72 @@ impl PlatformWindow for MacWindow { } } + fn get_title(&self) -> String { + unsafe { + let title: id = msg_send![self.0.lock().native_window, title]; + if title.is_null() { + "".to_string() + } else { + title.to_str().to_string() + } + } + } + fn set_app_id(&mut self, _app_id: &str) {} fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) { let mut this = self.0.as_ref().lock(); - this.renderer - .update_transparency(background_appearance != WindowBackgroundAppearance::Opaque); - let blur_radius = if background_appearance == WindowBackgroundAppearance::Blurred { - 80 - } else { - 0 - }; - let opaque = (background_appearance == WindowBackgroundAppearance::Opaque).to_objc(); + let opaque = background_appearance == WindowBackgroundAppearance::Opaque; + this.renderer.update_transparency(!opaque); unsafe { - this.native_window.setOpaque_(opaque); - // Shadows for transparent windows cause artifacts and performance issues - this.native_window.setHasShadow_(opaque); - let clear_color = if opaque == YES { + this.native_window.setOpaque_(opaque as BOOL); + let background_color = if opaque { NSColor::colorWithSRGBRed_green_blue_alpha_(nil, 0f64, 0f64, 0f64, 1f64) } else { - NSColor::clearColor(nil) + // Not using `+[NSColor clearColor]` to avoid broken shadow. + NSColor::colorWithSRGBRed_green_blue_alpha_(nil, 0f64, 0f64, 0f64, 0.0001) }; - this.native_window.setBackgroundColor_(clear_color); - let window_number = this.native_window.windowNumber(); - CGSSetWindowBackgroundBlurRadius(CGSMainConnectionID(), window_number, blur_radius); + this.native_window.setBackgroundColor_(background_color); + + if NSAppKitVersionNumber < NSAppKitVersionNumber12_0 { + // Whether `-[NSVisualEffectView respondsToSelector:@selector(_updateProxyLayer)]`. + // On macOS Catalina/Big Sur `NSVisualEffectView` doesn’t own concrete sublayers + // but uses a `CAProxyLayer`. Use the legacy WindowServer API. + let blur_radius = if background_appearance == WindowBackgroundAppearance::Blurred { + 80 + } else { + 0 + }; + + let window_number = this.native_window.windowNumber(); + CGSSetWindowBackgroundBlurRadius(CGSMainConnectionID(), window_number, blur_radius); + } else { + // On newer macOS `NSVisualEffectView` manages the effect layer directly. Using it + // could have a better performance (it downsamples the backdrop) and more control + // over the effect layer. + if background_appearance != WindowBackgroundAppearance::Blurred { + if let Some(blur_view) = this.blurred_view { + NSView::removeFromSuperview(blur_view); + this.blurred_view = None; + } + } else if this.blurred_view == None { + let content_view = this.native_window.contentView(); + let frame = NSView::bounds(content_view); + let mut blur_view: id = msg_send![BLURRED_VIEW_CLASS, alloc]; + blur_view = NSView::initWithFrame_(blur_view, frame); + blur_view.setAutoresizingMask_(NSViewWidthSizable | NSViewHeightSizable); + + let _: () = msg_send![ + content_view, + addSubview: blur_view + positioned: NSWindowOrderingMode::NSWindowBelow + relativeTo: nil + ]; + this.blurred_view = Some(blur_view.autorelease()); + } + } } } @@ -1332,32 +1341,26 @@ impl PlatformWindow for MacWindow { self.0.lock().appearance_changed_callback = Some(callback); } - fn has_system_window_tabs(&self) -> bool { - self.0.lock().has_system_window_tabs + fn tab_group(&self) -> Option { + let mut this = self.0.lock(); + + unsafe { + let tabgroup: id = msg_send![this.native_window, tabGroup]; + let tabgroup_id = tabgroup as *const Object as usize; + Some(tabgroup_id) + } } - fn refresh_has_system_window_tabs(&self) { - let mut this = self.0.lock(); - let executor = this.executor.clone(); - let native_window = this.native_window; - let has_system_window_tabs = &mut this.has_system_window_tabs as *mut bool; - executor - .spawn(async move { - unsafe { - let tabbed_windows: id = msg_send![native_window, tabbedWindows]; - let tabbed_windows_count: NSUInteger = if !tabbed_windows.is_null() { - msg_send![tabbed_windows, count] - } else { - 0 - }; + fn on_select_next_tab(&self, callback: Box) { + self.0.as_ref().lock().select_next_tab_callback = Some(callback); + } - let should_have_tabs = tabbed_windows_count >= 2; - if *has_system_window_tabs != should_have_tabs { - *has_system_window_tabs = should_have_tabs; - } - } - }) - .detach(); + fn on_select_previous_tab(&self, callback: Box) { + self.0.as_ref().lock().select_previous_tab_callback = Some(callback); + } + + fn on_merge_all_windows(&self, _callback: Box) { + self.0.as_ref().lock().merge_all_windows_callback = Some(_callback); } fn draw(&self, scene: &crate::Scene) { @@ -1820,28 +1823,10 @@ extern "C" fn window_will_enter_fullscreen(this: &Object, _: Sel, _: id) { let min_version = NSOperatingSystemVersion::new(15, 3, 0); - if is_macos_version_at_least(min_version) && lock.transparent_titlebar { - // Execute the fullscreen titlebar color change asynchronously, - // to ensure the new titlebar is active. - let executor = lock.executor.clone(); - drop(lock); - executor - .spawn(async move { - let mut lock = window_state.as_ref().lock(); - if let Some(fullscreen_titlebar_color) = lock.fullscreen_titlebar_color { - unsafe { - let mut color = NSColor::colorWithSRGBRed_green_blue_alpha_( - nil, - fullscreen_titlebar_color.r as f64, - fullscreen_titlebar_color.g as f64, - fullscreen_titlebar_color.b as f64, - fullscreen_titlebar_color.a as f64, - ); - set_fullscreen_titlebar_color(color); - } - } - }) - .detach(); + if is_macos_version_at_least(min_version) { + unsafe { + lock.native_window.setTitlebarAppearsTransparent_(NO); + } } } @@ -1852,25 +1837,12 @@ extern "C" fn window_will_exit_fullscreen(this: &Object, _: Sel, _: id) { let min_version = NSOperatingSystemVersion::new(15, 3, 0); if is_macos_version_at_least(min_version) && lock.transparent_titlebar { - // Execute the fullscreen titlebar color change immediately, - // before the window exits fullscreen (and the titlebar is hidden). unsafe { - let color = NSColor::clearColor(nil); - set_fullscreen_titlebar_color(color); + lock.native_window.setTitlebarAppearsTransparent_(YES); } } } -extern "C" fn window_will_use_fullscreen_presentation_options( - _this: &Object, - _sel: Sel, - _window: id, - _proposed_options: NSUInteger, -) -> NSUInteger { - // NSApplicationPresentationAutoHideToolbar | NSApplicationPresentationAutoHideMenuBar | NSApplicationPresentationFullScreen - (1 << 11) | (1 << 2) | (1 << 10) -} - pub(crate) fn is_macos_version_at_least(version: NSOperatingSystemVersion) -> bool { unsafe { NSProcessInfo::processInfo(nil).isOperatingSystemAtLeastVersion(version) } } @@ -1893,7 +1865,7 @@ extern "C" fn window_did_change_screen(this: &Object, _: Sel, _: id) { extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) { let window_state = unsafe { get_window_state(this) }; - let mut lock = window_state.lock(); + let lock = window_state.lock(); let is_active = unsafe { lock.native_window.isKeyWindow() == YES }; // When opening a pop-up while the application isn't active, Cocoa sends a spurious @@ -1917,19 +1889,8 @@ extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) executor .spawn(async move { let mut lock = window_state.as_ref().lock(); - - unsafe { - let tabbed_windows: id = msg_send![lock.native_window, tabbedWindows]; - let tabbed_windows_count: NSUInteger = if !tabbed_windows.is_null() { - msg_send![tabbed_windows, count] - } else { - 0 - }; - - let should_have_tabs = tabbed_windows_count >= 2; - if lock.has_system_window_tabs != should_have_tabs { - lock.has_system_window_tabs = should_have_tabs; - } + if is_active { + lock.move_traffic_light(); } if let Some(mut callback) = lock.activate_callback.take() { @@ -2335,23 +2296,17 @@ extern "C" fn conclude_drag_operation(this: &Object, _: Sel, _: id) { extern "C" fn add_titlebar_accessory_view_controller(this: &Object, _: Sel, view_controller: id) { unsafe { let _: () = msg_send![super(this, class!(NSWindow)), addTitlebarAccessoryViewController: view_controller]; + let accessory_view: id = msg_send![view_controller, view]; + + // Hide the native tab bar and set its height to 0, since we render our own. + let _: () = msg_send![accessory_view, setHidden: YES]; + let mut frame: NSRect = msg_send![accessory_view, frame]; + frame.size.height = 0.0; + let _: () = msg_send![accessory_view, setFrame: frame]; + + let window_state = get_window_state(this); + window_state.as_ref().lock().move_traffic_light(); } - - let window_state = unsafe { get_window_state(this) }; - window_state.lock().has_system_window_tabs = true; -} - -extern "C" fn remove_titlebar_accessory_view_controller( - this: &Object, - _: Sel, - view_controller: id, -) { - unsafe { - let _: () = msg_send![super(this, class!(NSWindow)), removeTitlebarAccessoryViewController: view_controller]; - } - - let window_state = unsafe { get_window_state(this) }; - window_state.lock().has_system_window_tabs = false; } async fn synthetic_drag( @@ -2490,89 +2445,36 @@ unsafe fn remove_layer_background(layer: id) { } } -unsafe fn update_tab_bar_state(lock: &mut MacWindowState) { - let tabbed_windows: id = msg_send![lock.native_window, tabbedWindows]; - let tabbed_windows_count: NSUInteger = if !tabbed_windows.is_null() { - msg_send![tabbed_windows, count] - } else { - 0 - }; - - let should_have_tabs = tabbed_windows_count >= 2; - if lock.has_system_window_tabs != should_have_tabs { - lock.has_system_window_tabs = should_have_tabs; +extern "C" fn select_next_tab(this: &Object, _sel: Sel, _id: id) { + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.as_ref().lock(); + if let Some(mut callback) = lock.select_next_tab_callback.take() { + drop(lock); + callback(); + window_state.lock().select_next_tab_callback = Some(callback); } } -// By hiding the visual effect view, we allow the window's (or titlebar's in this case) -// background color to show through. If we were to set `titlebarAppearsTransparent` to true -// the selected tab would look fine, but the unselected ones and new tab button backgrounds -// would be an opaque color. When the titlebar isn't transparent, however, the system applies -// a compositing effect to the unselected tab backgrounds, which makes them blend with the -// titlebar's/window's background. -fn hide_titlebar_effect_view(native_window: id) { +extern "C" fn select_previous_tab(this: &Object, _sel: Sel, _id: id) { + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.as_ref().lock(); + if let Some(mut callback) = lock.select_previous_tab_callback.take() { + drop(lock); + callback(); + window_state.lock().select_previous_tab_callback = Some(callback); + } +} + +extern "C" fn merge_all_windows(this: &Object, _sel: Sel, _id: id) { unsafe { - let content_view: id = msg_send![native_window, contentView]; - let frame_view: id = msg_send![content_view, superview]; - - if let Some(titlebar_view) = find_view_by_class_name(frame_view, "NSTitlebarView") { - if let Some(effect_view) = find_view_by_class_name(titlebar_view, "NSVisualEffectView") - { - let _: () = msg_send![effect_view, setHidden: YES]; - } - } - } -} - -// When the window is fullscreen, the titlebar is only shown when moving the mouse to the top of the screen. -// The titlebar will then show as overlay on top of the window's content, so we need to make sure it's not transparent. -fn set_fullscreen_titlebar_color(color: *mut Object) { - unsafe { - let nsapp: id = msg_send![class!(NSApplication), sharedApplication]; - let windows: id = msg_send![nsapp, windows]; - let count: NSUInteger = msg_send![windows, count]; - - for i in 0..count { - let window: id = msg_send![windows, objectAtIndex: i]; - let class_name: id = msg_send![window, className]; - let class_name_str = class_name.to_str(); - - if class_name_str != "NSToolbarFullScreenWindow" { - continue; - } - - let content_view: id = msg_send![window, contentView]; - if let Some(titlebar_container) = - find_view_by_class_name(content_view, "NSTitlebarContainerView") - { - let _: () = msg_send![titlebar_container, setWantsLayer: YES]; - let layer: id = msg_send![titlebar_container, layer]; - let cg_color: *mut std::ffi::c_void = msg_send![color, CGColor]; - let _: () = msg_send![layer, setBackgroundColor: cg_color]; - } - } - } -} - -fn find_view_by_class_name(view: id, target_class: &str) -> Option { - unsafe { - let view_class: *const Class = msg_send![view, class]; - let class_name_ptr: *const c_char = class_getName(view_class); - let class_name = CStr::from_ptr(class_name_ptr).to_str().unwrap_or(""); - if class_name == target_class { - return Some(view); - } - - let subviews: id = msg_send![view, subviews]; - let count: usize = msg_send![subviews, count]; - - for i in 0..count { - let subview: id = msg_send![subviews, objectAtIndex: i]; - if let Some(found) = find_view_by_class_name(subview, target_class) { - return Some(found); - } - } + let _: () = msg_send![super(this, class!(NSWindow)), mergeAllWindows:nil]; } - None + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.as_ref().lock(); + if let Some(mut callback) = lock.merge_all_windows_callback.take() { + drop(lock); + callback(); + window_state.lock().merge_all_windows_callback = Some(callback); + } } diff --git a/crates/gpui/src/platform/mac/window_appearance.rs b/crates/gpui/src/platform/mac/window_appearance.rs index 81372119fc..65c409d30c 100644 --- a/crates/gpui/src/platform/mac/window_appearance.rs +++ b/crates/gpui/src/platform/mac/window_appearance.rs @@ -28,17 +28,6 @@ impl WindowAppearance { } } } - - pub(crate) unsafe fn into_native(self) -> id { - unsafe { - match self { - Self::VibrantLight => NSAppearanceNameVibrantLight, - Self::VibrantDark => NSAppearanceNameVibrantDark, - Self::Light => NSAppearanceNameAqua, - Self::Dark => NSAppearanceNameDarkAqua, - } - } - } } #[link(name = "AppKit", kind = "framework")] diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 00002fbae9..170e59aa65 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -11,12 +11,12 @@ use crate::{ MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, PromptButton, PromptLevel, Quad, Render, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Replay, ResizeEdge, - Rgba, SMOOTH_SVG_SCALE_FACTOR, SUBPIXEL_VARIANTS, ScaledPixels, Scene, Shadow, SharedString, - Size, StrikethroughStyle, Style, SubscriberSet, Subscription, TabHandles, TaffyLayoutEngine, - Task, TextStyle, TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle, - WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControls, WindowDecorations, - WindowOptions, WindowParams, WindowTextSystem, point, prelude::*, px, rems, size, - transparent_black, + SMOOTH_SVG_SCALE_FACTOR, SUBPIXEL_VARIANTS, ScaledPixels, Scene, Shadow, SharedString, Size, + StrikethroughStyle, Style, SubscriberSet, Subscription, SystemWindowTabController, TabHandles, + TaffyLayoutEngine, Task, TextStyle, TextStyleRefinement, TransformationMatrix, Underline, + UnderlineStyle, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControls, + WindowDecorations, WindowOptions, WindowParams, WindowTextSystem, point, prelude::*, px, rems, + size, transparent_black, }; use anyhow::{Context as _, Result, anyhow}; use collections::{FxHashMap, FxHashSet}; @@ -945,7 +945,6 @@ impl Window { window_min_size, window_decorations, allows_automatic_window_tabbing, - use_toolbar, } = options; let bounds = window_bounds @@ -963,7 +962,6 @@ impl Window { display_id, window_min_size, allows_automatic_window_tabbing, - use_toolbar, }, )?; let display_id = platform_window.display().map(|display| display.id()); @@ -995,19 +993,13 @@ impl Window { } platform_window.on_close(Box::new({ - let windows = cx.windows(); + let window_id = handle.window_id(); let mut cx = cx.to_async(); move || { let _ = handle.update(&mut cx, |_, window, _| window.remove_window()); - - windows - .into_iter() - .filter(|w| w.window_id() != handle.window_id()) - .for_each(|window| { - let _ = window.update(&mut cx, |_, window, _| { - window.refresh_has_system_window_tabs(); - }); - }); + let _ = cx.update(|cx| { + SystemWindowTabController::remove_window(cx, window_id); + }); } })); platform_window.on_request_frame(Box::new({ @@ -1096,6 +1088,18 @@ impl Window { .activation_observers .clone() .retain(&(), |callback| callback(window, cx)); + + let tab_group = window.tab_group(); + if let Some(tab_group) = tab_group { + SystemWindowTabController::add_window( + cx, + tab_group, + handle, + SharedString::from(window.window_title()), + ); + } + + window.bounds_changed(cx); window.refresh(); }) .log_err(); @@ -1137,12 +1141,61 @@ impl Window { .unwrap_or(None) }) }); + platform_window.on_select_next_tab({ + let mut cx = cx.to_async(); + Box::new(move || { + handle + .update(&mut cx, |_, window, cx| { + let window_id = handle.window_id(); + if let Some(tab_group) = window.tab_group() { + SystemWindowTabController::select_next_tab(cx, tab_group, window_id); + } + }) + .log_err(); + }) + }); + platform_window.on_select_previous_tab({ + let mut cx = cx.to_async(); + Box::new(move || { + handle + .update(&mut cx, |_, window, cx| { + let window_id = handle.window_id(); + if let Some(tab_group) = window.tab_group() { + SystemWindowTabController::select_previous_tab( + cx, tab_group, window_id, + ); + } + }) + .log_err(); + }) + }); + platform_window.on_merge_all_windows({ + let mut cx = cx.to_async(); + Box::new(move || { + handle + .update(&mut cx, |_, window, cx| { + if let Some(tab_group) = window.tab_group() { + SystemWindowTabController::merge_all_windows(cx, tab_group); + } + }) + .log_err(); + }) + }); if let Some(app_id) = app_id { platform_window.set_app_id(&app_id); } platform_window.map_window().unwrap(); + let tab_group = platform_window.tab_group(); + if let Some(tab_group) = tab_group { + SystemWindowTabController::add_window( + cx, + tab_group, + handle, + SharedString::from(platform_window.get_title()), + ); + } Ok(Window { handle, @@ -4294,41 +4347,16 @@ impl Window { self.platform_window.titlebar_double_click(); } - /// Sets the window appearance. + /// Gets the window's title at the platform level. /// This is macOS specific. - pub fn set_appearance(&self, appearance: WindowAppearance) { - self.platform_window.set_appearance(appearance); + pub fn window_title(&self) -> String { + self.platform_window.get_title() } - /// Set the background color of the titlebar. + /// Returns the tab group pointer of the window. /// This is macOS specific. - pub fn set_fullscreen_titlebar_background_color(&self, color: Rgba) { - self.platform_window - .set_fullscreen_titlebar_background_color(color); - } - - /// Returns whether the window has more then 1 tab (therefore showing the tab bar). - /// This is macOS specific. - pub fn has_system_window_tabs(&self) -> bool { - self.platform_window.has_system_window_tabs() - } - - /// Syncs the window's tab state. - /// This is macOS specific. - pub fn refresh_has_system_window_tabs(&self) { - self.platform_window.refresh_has_system_window_tabs(); - } - - /// Selects the next tab in the tab group in the trailing direction. - /// This is macOS specific. - pub fn show_next_window_tab(&self) { - self.platform_window.show_next_window_tab() - } - - /// Selects the previous tab in the tab group in the leading direction. - /// This is macOS specific. - pub fn show_previous_window_tab(&self) { - self.platform_window.show_previous_window_tab() + pub fn tab_group(&self) -> Option { + self.platform_window.tab_group() } /// Merges all open windows into a single tabbed window. @@ -4339,8 +4367,14 @@ impl Window { /// Moves the tab to a new containing window. /// This is macOS specific. - pub fn move_window_tab_to_new_window(&self) { - self.platform_window.move_window_tab_to_new_window() + pub fn move_tab_to_new_window(&self) { + self.platform_window.move_tab_to_new_window() + } + + /// Shows or hides the window tab overview. + /// This is macOS specific. + pub fn toggle_window_tab_overview(&self) { + self.platform_window.toggle_window_tab_overview() } /// Toggles the inspector mode on this window. diff --git a/crates/rules_library/src/rules_library.rs b/crates/rules_library/src/rules_library.rs index 5402f2a3f9..e1c67784e4 100644 --- a/crates/rules_library/src/rules_library.rs +++ b/crates/rules_library/src/rules_library.rs @@ -4,7 +4,7 @@ use editor::{CompletionProvider, SelectionEffects}; use editor::{CurrentLineHighlight, Editor, EditorElement, EditorEvent, EditorStyle, actions::Tab}; use gpui::{ Action, App, Bounds, Entity, EventEmitter, Focusable, PromptLevel, Subscription, Task, - TextStyle, TitlebarOptions, WindowBounds, WindowHandle, WindowOptions, actions, size, + TextStyle, TitlebarOptions, WindowBounds, WindowHandle, WindowOptions, actions, point, size, transparent_black, }; use language::{Buffer, LanguageRegistry, language_settings::SoftWrap}; @@ -130,7 +130,7 @@ pub fn open_rules_library( titlebar: Some(TitlebarOptions { title: Some("Rules Library".into()), appears_transparent: true, - traffic_light_position: Default::default(), + traffic_light_position: Some(point(px(9.0), px(9.0))), }), app_id: Some(app_id.to_owned()), window_bounds: Some(WindowBounds::Windowed(bounds)), @@ -414,7 +414,7 @@ impl RulesLibrary { }); Self { title_bar: if !cfg!(target_os = "macos") { - Some(cx.new(|_| PlatformTitleBar::new("rules-library-title-bar"))) + Some(cx.new(|cx| PlatformTitleBar::new("rules-library-title-bar", window, cx))) } else { None }, diff --git a/crates/title_bar/Cargo.toml b/crates/title_bar/Cargo.toml index cf178e2850..82a8b06f82 100644 --- a/crates/title_bar/Cargo.toml +++ b/crates/title_bar/Cargo.toml @@ -32,6 +32,7 @@ auto_update.workspace = true call.workspace = true chrono.workspace = true client.workspace = true +command_palette_hooks.workspace = true cloud_llm_client.workspace = true db.workspace = true gpui = { workspace = true, features = ["screen-capture"] } diff --git a/crates/title_bar/src/platform_title_bar.rs b/crates/title_bar/src/platform_title_bar.rs index e66b2852b9..409cb41c27 100644 --- a/crates/title_bar/src/platform_title_bar.rs +++ b/crates/title_bar/src/platform_title_bar.rs @@ -1,34 +1,41 @@ use gpui::{ - AnyElement, Context, Decorations, Hsla, InteractiveElement, IntoElement, MouseButton, + AnyElement, Context, Decorations, Entity, Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, StatefulInteractiveElement, Styled, Window, WindowControlArea, div, px, }; use smallvec::SmallVec; use std::mem; use ui::prelude::*; -use crate::platforms::{platform_linux, platform_mac, platform_windows}; +use crate::{ + platforms::{platform_linux, platform_mac, platform_windows}, + system_window_tabs::SystemWindowTabs, +}; pub struct PlatformTitleBar { id: ElementId, platform_style: PlatformStyle, children: SmallVec<[AnyElement; 2]>, should_move: bool, + system_window_tabs: Entity, } impl PlatformTitleBar { - pub fn new(id: impl Into) -> Self { + pub fn new(id: impl Into, window: &mut Window, cx: &mut Context) -> Self { let platform_style = PlatformStyle::platform(); + let system_window_tabs = cx.new(|cx| SystemWindowTabs::new(window, cx)); + Self { id: id.into(), platform_style, children: SmallVec::new(), should_move: false, + system_window_tabs, } } #[cfg(not(target_os = "windows"))] pub fn height(window: &mut Window) -> Pixels { - (1.75 * window.rem_size()).max(px(38.)) + (1.75 * window.rem_size()).max(px(34.)) } #[cfg(target_os = "windows")] @@ -62,21 +69,14 @@ impl Render for PlatformTitleBar { let supported_controls = window.window_controls(); let decorations = window.window_decorations(); let height = Self::height(window); - let tab_height = px(28.); let titlebar_color = self.title_bar_color(window, cx); let close_action = Box::new(workspace::CloseWindow); let children = mem::take(&mut self.children); - h_flex() + let title_bar = h_flex() .window_control_area(WindowControlArea::Drag) .w_full() - .map(|this| { - if window.has_system_window_tabs() && !window.is_fullscreen() { - this.h(height + tab_height).pb(tab_height) - } else { - this.h(height) - } - }) + .h(height) .map(|this| { if window.is_fullscreen() { this.pl_2() @@ -169,7 +169,12 @@ impl Render for PlatformTitleBar { title_bar.child(platform_windows::WindowsWindowControls::new(height)) } } - }) + }); + + v_flex() + .w_full() + .child(title_bar) + .child(self.system_window_tabs.clone().into_any_element()) } } diff --git a/crates/title_bar/src/system_window_tabs.rs b/crates/title_bar/src/system_window_tabs.rs new file mode 100644 index 0000000000..3ec1852ac2 --- /dev/null +++ b/crates/title_bar/src/system_window_tabs.rs @@ -0,0 +1,353 @@ +use command_palette_hooks::CommandPaletteFilter; +use std::any::TypeId; + +use gpui::{ + Context, InteractiveElement, ParentElement, ScrollHandle, Styled, Subscription, + SystemWindowTab, SystemWindowTabController, Window, WindowId, actions, canvas, div, +}; +use ui::{ + Color, ContextMenu, DynamicSpacing, IconButton, IconButtonShape, IconName, IconSize, Label, + LabelSize, Tab, h_flex, prelude::*, right_click_menu, +}; +use workspace::{CloseWindow, Workspace}; + +actions!( + window, + [ + ShowNextWindowTab, + ShowPreviousWindowTab, + MergeAllWindows, + MoveTabToNewWindow + ] +); + +#[derive(Clone)] +pub struct DraggedWindowTab { + pub id: WindowId, + pub title: String, + pub width: Pixels, + pub is_active: bool, +} + +pub struct SystemWindowTabs { + tab_group: Option, + tabs: Vec, + tab_bar_scroll_handle: ScrollHandle, + measured_tab_width: Pixels, + _subscriptions: Vec, +} + +impl SystemWindowTabs { + pub fn new(window: &mut Window, cx: &mut Context) -> Self { + let tab_group = window.tab_group(); + let mut subscriptions = Vec::new(); + + subscriptions.push(cx.observe_new( + |workspace: &mut Workspace, _window, _cx: &mut Context| { + workspace + .register_action(|_, _: &ShowNextWindowTab, window, cx| { + let window_id = window.window_handle().window_id(); + if let Some(tab_group) = window.tab_group() { + SystemWindowTabController::select_next_tab(cx, tab_group, window_id); + } + }) + .register_action(|_, _: &ShowPreviousWindowTab, window, cx| { + let window_id = window.window_handle().window_id(); + if let Some(tab_group) = window.tab_group() { + SystemWindowTabController::select_previous_tab( + cx, tab_group, window_id, + ); + } + }) + .register_action(|_, _: &MergeAllWindows, window, cx| { + if let Some(tab_group) = window.tab_group() { + SystemWindowTabController::merge_all_windows(cx, tab_group); + } + }) + .register_action(|_, _: &MoveTabToNewWindow, window, _cx| { + window.move_tab_to_new_window() + }); + }, + )); + + subscriptions.push(cx.observe_global::(|this, cx| { + if let Some(tab_group) = this.tab_group { + let controller = cx.global::(); + + let all_tab_groups = controller.tabs(); + let all_tabs = controller.windows(tab_group); + if let Some(tabs) = all_tabs { + let show_merge_all_windows = all_tab_groups.len() > 1; + let show_other_tab_actions = tabs.len() > 1; + + this.tabs = tabs.clone(); + cx.notify(); + + let merge_all_windows_action = TypeId::of::(); + let other_tab_actions = vec![ + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + ]; + + if show_merge_all_windows && show_other_tab_actions { + CommandPaletteFilter::update_global(cx, |filter, _cx| { + let mut all_actions = vec![merge_all_windows_action]; + all_actions.extend(other_tab_actions.iter().cloned()); + filter.show_action_types(all_actions.iter()); + }); + } else if show_merge_all_windows { + CommandPaletteFilter::update_global(cx, |filter, _cx| { + filter.show_action_types(std::iter::once(&merge_all_windows_action)); + filter.hide_action_types(&other_tab_actions); + }); + } else if show_other_tab_actions { + CommandPaletteFilter::update_global(cx, |filter, _cx| { + filter.show_action_types(other_tab_actions.iter()); + filter.hide_action_types(&[merge_all_windows_action]); + }); + } else { + CommandPaletteFilter::update_global(cx, |filter, _cx| { + let mut all_actions = vec![merge_all_windows_action]; + all_actions.extend(other_tab_actions.iter().cloned()); + filter.hide_action_types(&all_actions); + }); + } + } + } + })); + + subscriptions.push(cx.observe_window_activation(window, |this, window, cx| { + this.tab_group = window.tab_group(); + cx.notify(); + })); + + Self { + tab_group, + tabs: Vec::new(), + tab_bar_scroll_handle: ScrollHandle::new(), + measured_tab_width: window.bounds().size.width, + _subscriptions: subscriptions, + } + } + + fn render_tab( + &self, + ix: usize, + item: SystemWindowTab, + window: &mut Window, + cx: &mut Context, + ) -> impl IntoElement + use<> { + // let settings = ItemSettings::get_global(cx); + // let close_side = &settings.close_position; + // let show_close_button = &settings.show_close_button; + + let rem_size = window.rem_size(); + let width = self.measured_tab_width.max(rem_size * 10); + let is_active = window.window_handle().window_id() == item.id; + let title = item.title.to_string(); + + let label = Label::new(&title) + .size(LabelSize::Small) + .truncate() + .color(if is_active { + Color::Default + } else { + Color::Muted + }); + + let tab = h_flex() + .h_full() + .w(width) + .border_t_1() + .border_color(if is_active { + cx.theme().colors().title_bar_background + } else { + cx.theme().colors().border + }) + .child( + h_flex() + .id(ix) + .group("tab") + .w_full() + .h(Tab::content_height(cx)) + .relative() + .px(DynamicSpacing::Base16.px(cx)) + .justify_center() + .border_l_1() + .border_color(cx.theme().colors().border) + .when(is_active, |this| { + this.bg(cx.theme().colors().title_bar_background) + }) + .cursor_pointer() + .on_drag( + DraggedWindowTab { + id: item.id, + title: item.title.to_string(), + width, + is_active, + }, + |tab, _, _, cx| cx.new(|_| tab.clone()), + ) + .drag_over::(|element, _, _, cx| { + element.bg(cx.theme().colors().drop_target_background) + }) + .on_drop(cx.listener( + move |_this, dragged_tab: &DraggedWindowTab, _window, cx| { + Self::handle_tab_drop(dragged_tab, ix, cx); + }, + )) + .on_click(move |_, _, cx| { + let _ = item.handle.update(cx, |_, window, _| { + window.activate_window(); + }); + }) + .child(label) + .child( + div().absolute().top_2().right_1().w_4().h_4().child( + IconButton::new("close", IconName::Close) + .visible_on_hover("tab") + .shape(IconButtonShape::Square) + .icon_color(Color::Muted) + .icon_size(IconSize::XSmall) + .on_click(|_, window, cx| { + window.dispatch_action(Box::new(CloseWindow), cx); + }), + ), + ), + ) + .into_any(); + + let menu = right_click_menu(ix) + .trigger(|_, _, _| tab) + .menu(move |window, cx| { + let focus_handle = cx.focus_handle(); + + ContextMenu::build(window, cx, move |mut menu, _window_, _cx| { + menu = menu.entry("Close Tab", None, move |_window, _cx| { + // window.dispatch_action(Box::new(CloseWindow), cx); + }); + + menu = menu.entry("Close Other Tabs", None, move |_window, _cx| { + // window.dispatch_action(Box::new(CloseWindow), cx); + }); + + menu = menu.entry("Move Tab to New Window", None, move |_window, _cx| { + // window.move_tab_to_new_window(); + }); + + menu = menu.entry("Show All Tabs", None, move |_window, _cx| { + // window.toggle_window_tab_overview(); + }); + + menu.context(focus_handle.clone()) + }) + }); + + div().child(menu).size_full() + } + + fn handle_tab_drop(dragged_tab: &DraggedWindowTab, ix: usize, cx: &mut Context) { + SystemWindowTabController::update_window_position(cx, dragged_tab.id, ix); + } +} + +impl Render for SystemWindowTabs { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let entity = cx.entity(); + let number_of_tabs = self.tabs.len(); + let tab_items = self + .tabs + .iter() + .enumerate() + .map(|(ix, item)| self.render_tab(ix, item.clone(), window, cx)) + .collect::>(); + + h_flex() + .w_full() + .h(Tab::container_height(cx)) + .bg(cx.theme().colors().editor_background) + .child( + h_flex() + .id("window tabs") + .w_full() + .h_full() + .h(Tab::container_height(cx)) + .bg(cx.theme().colors().editor_background) + .overflow_x_scroll() + .w_full() + .track_scroll(&self.tab_bar_scroll_handle) + .children(tab_items) + .child( + canvas( + |_, _, _| (), + move |bounds, _, _, cx| { + let entity = entity.clone(); + entity.update(cx, |this, cx| { + let width = bounds.size.width / number_of_tabs as f32; + if width != this.measured_tab_width { + this.measured_tab_width = width; + cx.notify(); + } + }); + }, + ) + .absolute() + .size_full(), + ), + ) + .child( + h_flex() + .h_full() + .px(DynamicSpacing::Base06.rems(cx)) + .border_t_1() + .border_l_1() + .border_color(cx.theme().colors().border) + .child( + IconButton::new("plus", IconName::Plus) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .on_click(|_event, window, cx| { + window.dispatch_action( + Box::new(zed_actions::OpenRecent { + create_new_window: true, + }), + cx, + ); + }), + ), + ) + .into_any_element() + } +} + +impl Render for DraggedWindowTab { + fn render( + &mut self, + _window: &mut gpui::Window, + cx: &mut gpui::Context, + ) -> impl gpui::IntoElement { + let label = Label::new(self.title.clone()) + .size(LabelSize::Small) + .truncate() + .color(if self.is_active { + Color::Default + } else { + Color::Muted + }); + + h_flex() + .h(Tab::container_height(cx)) + .w(self.width) + .px(DynamicSpacing::Base16.px(cx)) + .justify_center() + .bg(if self.is_active { + cx.theme().colors().title_bar_background + } else { + cx.theme().colors().editor_background + }) + .border_1() + .border_color(cx.theme().colors().border) + .child(label) + } +} diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 5bd6a17e4b..ed27696f9f 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -3,6 +3,7 @@ mod collab; mod onboarding_banner; pub mod platform_title_bar; mod platforms; +mod system_window_tabs; mod title_bar_settings; #[cfg(feature = "stories")] @@ -284,7 +285,7 @@ impl TitleBar { ) }); - let platform_titlebar = cx.new(|_| PlatformTitleBar::new(id)); + let platform_titlebar = cx.new(|cx| PlatformTitleBar::new(id, window, cx)); Self { platform_titlebar, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 4b8d7434e9..a391af6b2c 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -39,9 +39,9 @@ use gpui::{ Action, AnyEntity, AnyView, AnyWeakView, App, AsyncApp, AsyncWindowContext, Bounds, Context, CursorStyle, Decorations, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle, Focusable, Global, HitboxBehavior, Hsla, KeyContext, Keystroke, ManagedView, MouseButton, - PathPromptOptions, Point, PromptLevel, Render, ResizeEdge, Size, Stateful, Subscription, Task, - Tiling, WeakEntity, WindowBounds, WindowHandle, WindowId, WindowOptions, actions, canvas, - point, relative, size, transparent_black, + PathPromptOptions, Point, PromptLevel, Render, ResizeEdge, Size, Stateful, Subscription, + SystemWindowTabController, Task, Tiling, WeakEntity, WindowBounds, WindowHandle, WindowId, + WindowOptions, actions, canvas, point, relative, size, transparent_black, }; pub use history_manager::*; pub use item::{ @@ -253,10 +253,6 @@ actions!( RestoreBanner, /// Toggles expansion of the selected item. ToggleExpandItem, - ShowNextWindowTab, - ShowPreviousWindowTab, - MergeAllWindows, - MoveWindowTabToNewWindow ] ); @@ -4397,6 +4393,11 @@ impl Workspace { return; } window.set_window_title(&title); + SystemWindowTabController::update_window_title( + cx, + window.window_handle().window_id(), + SharedString::from(&title), + ); self.last_window_title = Some(title); } @@ -5576,22 +5577,6 @@ impl Workspace { workspace.activate_previous_window(cx) }), ) - .on_action(cx.listener(|workspace, _: &ShowNextWindowTab, window, cx| { - workspace.show_next_window_tab(cx, window) - })) - .on_action( - cx.listener(|workspace, _: &ShowPreviousWindowTab, window, cx| { - workspace.show_previous_window_tab(cx, window) - }), - ) - .on_action(cx.listener(|workspace, _: &MergeAllWindows, window, cx| { - workspace.merge_all_windows(cx, window) - })) - .on_action( - cx.listener(|workspace, _: &MoveWindowTabToNewWindow, window, cx| { - workspace.move_window_tab_to_new_window(cx, window) - }), - ) .on_action(cx.listener(|workspace, _: &ActivatePaneLeft, window, cx| { workspace.activate_pane_in_direction(SplitDirection::Left, window, cx) })) @@ -5911,38 +5896,6 @@ impl Workspace { .ok(); } - pub fn show_next_window_tab(&mut self, cx: &mut Context, window: &mut Window) { - cx.spawn_in(window, async move |_, cx| { - cx.update(|window, _cx| window.show_next_window_tab())?; - anyhow::Ok(()) - }) - .detach_and_log_err(cx) - } - - pub fn show_previous_window_tab(&mut self, cx: &mut Context, window: &mut Window) { - cx.spawn_in(window, async move |_, cx| { - cx.update(|window, _cx| window.show_previous_window_tab())?; - anyhow::Ok(()) - }) - .detach_and_log_err(cx) - } - - pub fn merge_all_windows(&mut self, cx: &mut Context, window: &mut Window) { - cx.spawn_in(window, async move |_, cx| { - cx.update(|window, _cx| window.merge_all_windows())?; - anyhow::Ok(()) - }) - .detach_and_log_err(cx) - } - - pub fn move_window_tab_to_new_window(&mut self, cx: &mut Context, window: &mut Window) { - cx.spawn_in(window, async move |_, cx| { - cx.update(|window, _cx| window.move_window_tab_to_new_window())?; - anyhow::Ok(()) - }) - .detach_and_log_err(cx) - } - pub fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context) { if cx.stop_active_drag(window) { return; diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index bec68bae99..d69efaf6c0 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -43,7 +43,6 @@ client.workspace = true collab_ui.workspace = true collections.workspace = true command_palette.workspace = true -command_palette_hooks.workspace = true component.workspace = true copilot.workspace = true crashes.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 9c6262c0d5..df30d4dd7b 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -8,7 +8,6 @@ use cli::FORCE_CLI_MODE_ENV_VAR_NAME; use client::{Client, ProxySettings, UserStore, parse_zed_link}; use collab_ui::channel_view::ChannelView; use collections::HashMap; -use command_palette_hooks::CommandPaletteFilter; use crashes::InitCrashHandler; use db::kvp::{GLOBAL_KEY_VALUE_STORE, KEY_VALUE_STORE}; use editor::Editor; @@ -17,10 +16,7 @@ use extension_host::ExtensionStore; use fs::{Fs, RealFs}; use futures::{StreamExt, channel::oneshot, future}; use git::GitHostingProviderRegistry; -use gpui::{ - App, AppContext as _, Application, AsyncApp, Focusable as _, UpdateGlobal as _, - WindowAppearance, -}; +use gpui::{App, AppContext as _, Application, AsyncApp, Focusable as _, UpdateGlobal as _}; use gpui_tokio::Tokio; use http_client::{Url, read_proxy_from_env}; @@ -38,7 +34,6 @@ use release_channel::{AppCommitSha, AppVersion, ReleaseChannel}; use session::{AppSession, Session}; use settings::{BaseKeymap, Settings, SettingsStore, watch_config_file}; use std::{ - any::TypeId, env, io::{self, IsTerminal}, path::{Path, PathBuf}, @@ -46,14 +41,13 @@ use std::{ sync::Arc, }; use theme::{ - ActiveTheme, Appearance, IconThemeNotFoundError, SystemAppearance, ThemeNotFoundError, - ThemeRegistry, ThemeSettings, + ActiveTheme, IconThemeNotFoundError, SystemAppearance, ThemeNotFoundError, ThemeRegistry, + ThemeSettings, }; use util::{ResultExt, TryFutureExt, maybe}; use uuid::Uuid; use workspace::{ - AppState, MergeAllWindows, MoveWindowTabToNewWindow, SerializedWorkspaceLocation, - ShowNextWindowTab, ShowPreviousWindowTab, Toast, Workspace, WorkspaceSettings, WorkspaceStore, + AppState, SerializedWorkspaceLocation, Toast, Workspace, WorkspaceSettings, WorkspaceStore, notifications::NotificationId, }; use zed::{ @@ -386,22 +380,6 @@ pub fn main() { .detach(); } }); - app.new_window_for_tab(move |cx| { - for workspace in workspace::local_workspace_windows(cx) { - workspace - .update(cx, |_view, window, cx| { - if window.is_window_active() { - window.dispatch_action( - Box::new(zed_actions::OpenRecent { - create_new_window: true, - }), - cx, - ); - } - }) - .log_err(); - } - }); app.run(move |cx| { menu::init(); @@ -667,19 +645,10 @@ pub fn main() { let client = app_state.client.clone(); move |cx| { for &mut window in cx.windows().iter_mut() { - let title_bar_background = cx.theme().colors().title_bar_background.to_rgb(); let background_appearance = cx.theme().window_background_appearance(); - let appearance = cx.theme().appearance(); - window .update(cx, |_, window, _| { - window.set_fullscreen_titlebar_background_color(title_bar_background); - window.set_background_appearance(background_appearance); - - match appearance { - Appearance::Light => window.set_appearance(WindowAppearance::Light), - Appearance::Dark => window.set_appearance(WindowAppearance::Dark), - } + window.set_background_appearance(background_appearance) }) .ok(); } @@ -694,25 +663,6 @@ pub fn main() { client.reconnect(&cx.to_async()); } } - - let use_system_window_tabs = - WorkspaceSettings::get_global(cx).use_system_window_tabs; - let system_window_tab_actions = [ - TypeId::of::(), - TypeId::of::(), - TypeId::of::(), - TypeId::of::(), - ]; - - if use_system_window_tabs { - CommandPaletteFilter::update_global(cx, |filter, _cx| { - filter.show_action_types(system_window_tab_actions.iter()); - }); - } else { - CommandPaletteFilter::update_global(cx, |filter, _cx| { - filter.hide_action_types(&system_window_tab_actions); - }); - } } }) .detach(); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index e52352b0d0..35cf78823c 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -27,8 +27,8 @@ use git_ui::project_diff::ProjectDiffToolbar; use gpui::{ Action, App, AppContext as _, Context, DismissEvent, Element, Entity, Focusable, KeyBinding, ParentElement, PathPromptOptions, PromptLevel, ReadGlobal, SharedString, Styled, Task, - TitlebarOptions, UpdateGlobal, Window, WindowKind, WindowOptions, actions, image_cache, px, - retain_all, + TitlebarOptions, UpdateGlobal, Window, WindowKind, WindowOptions, actions, image_cache, point, + px, retain_all, }; use image_viewer::ImageInfo; use language::Capability; @@ -288,7 +288,7 @@ pub fn build_window_options(display_uuid: Option, cx: &mut App) -> WindowO titlebar: Some(TitlebarOptions { title: None, appears_transparent: true, - traffic_light_position: Default::default(), + traffic_light_position: Some(point(px(9.0), px(9.0))), }), window_bounds: None, focus: false, @@ -304,7 +304,6 @@ pub fn build_window_options(display_uuid: Option, cx: &mut App) -> WindowO height: px(240.0), }), allows_automatic_window_tabbing: Some(use_system_window_tabs), - use_toolbar: Some(true), } } From 1ad933bd3e3e08b7e7fc943f45f68ec1b4288618 Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Sat, 12 Jul 2025 23:18:29 +0200 Subject: [PATCH 18/42] Close button based on user settings --- crates/title_bar/src/system_window_tabs.rs | 49 +++++++++++++++------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/crates/title_bar/src/system_window_tabs.rs b/crates/title_bar/src/system_window_tabs.rs index 3ec1852ac2..76cbfbeb8c 100644 --- a/crates/title_bar/src/system_window_tabs.rs +++ b/crates/title_bar/src/system_window_tabs.rs @@ -1,4 +1,5 @@ use command_palette_hooks::CommandPaletteFilter; +use settings::Settings; use std::any::TypeId; use gpui::{ @@ -9,7 +10,10 @@ use ui::{ Color, ContextMenu, DynamicSpacing, IconButton, IconButtonShape, IconName, IconSize, Label, LabelSize, Tab, h_flex, prelude::*, right_click_menu, }; -use workspace::{CloseWindow, Workspace}; +use workspace::{ + CloseWindow, ItemSettings, Workspace, + item::{ClosePosition, ShowCloseButton}, +}; actions!( window, @@ -138,9 +142,9 @@ impl SystemWindowTabs { window: &mut Window, cx: &mut Context, ) -> impl IntoElement + use<> { - // let settings = ItemSettings::get_global(cx); - // let close_side = &settings.close_position; - // let show_close_button = &settings.show_close_button; + let settings = ItemSettings::get_global(cx); + let close_side = &settings.close_position; + let show_close_button = &settings.show_close_button; let rem_size = window.rem_size(); let width = self.measured_tab_width.max(rem_size * 10); @@ -203,18 +207,33 @@ impl SystemWindowTabs { }); }) .child(label) - .child( - div().absolute().top_2().right_1().w_4().h_4().child( - IconButton::new("close", IconName::Close) - .visible_on_hover("tab") - .shape(IconButtonShape::Square) - .icon_color(Color::Muted) - .icon_size(IconSize::XSmall) - .on_click(|_, window, cx| { - window.dispatch_action(Box::new(CloseWindow), cx); - }), + .map(|this| match show_close_button { + ShowCloseButton::Hidden => this, + _ => this.child( + div() + .absolute() + .top_2() + .w_4() + .h_4() + .map(|this| match close_side { + ClosePosition::Left => this.left_1(), + ClosePosition::Right => this.right_1(), + }) + .child( + IconButton::new("close", IconName::Close) + .shape(IconButtonShape::Square) + .icon_color(Color::Muted) + .icon_size(IconSize::XSmall) + .on_click(|_, window, cx| { + window.dispatch_action(Box::new(CloseWindow), cx); + }) + .map(|this| match show_close_button { + ShowCloseButton::Hover => this.visible_on_hover("tab"), + _ => this, + }), + ), ), - ), + }), ) .into_any(); From 453006241d5314191c1eb3061987dd68d8143bb3 Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Sun, 13 Jul 2025 13:39:28 +0200 Subject: [PATCH 19/42] Use tab_bar_background for system window tabs --- crates/title_bar/src/system_window_tabs.rs | 39 ++++++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/crates/title_bar/src/system_window_tabs.rs b/crates/title_bar/src/system_window_tabs.rs index 76cbfbeb8c..f78cc6bd4f 100644 --- a/crates/title_bar/src/system_window_tabs.rs +++ b/crates/title_bar/src/system_window_tabs.rs @@ -3,8 +3,9 @@ use settings::Settings; use std::any::TypeId; use gpui::{ - Context, InteractiveElement, ParentElement, ScrollHandle, Styled, Subscription, - SystemWindowTab, SystemWindowTabController, Window, WindowId, actions, canvas, div, + Context, Hsla, InteractiveElement, ParentElement, ScrollHandle, Styled, Subscription, + SystemWindowTab, SystemWindowTabController, Window, WindowId, actions, black, canvas, div, + white, }; use ui::{ Color, ContextMenu, DynamicSpacing, IconButton, IconButtonShape, IconName, IconSize, Label, @@ -31,6 +32,8 @@ pub struct DraggedWindowTab { pub title: String, pub width: Pixels, pub is_active: bool, + pub active_background_color: Hsla, + pub inactive_background_color: Hsla, } pub struct SystemWindowTabs { @@ -139,6 +142,8 @@ impl SystemWindowTabs { &self, ix: usize, item: SystemWindowTab, + active_background_color: Hsla, + inactive_background_color: Hsla, window: &mut Window, cx: &mut Context, ) -> impl IntoElement + use<> { @@ -165,7 +170,7 @@ impl SystemWindowTabs { .w(width) .border_t_1() .border_color(if is_active { - cx.theme().colors().title_bar_background + active_background_color } else { cx.theme().colors().border }) @@ -180,9 +185,7 @@ impl SystemWindowTabs { .justify_center() .border_l_1() .border_color(cx.theme().colors().border) - .when(is_active, |this| { - this.bg(cx.theme().colors().title_bar_background) - }) + .when(is_active, |this| this.bg(active_background_color)) .cursor_pointer() .on_drag( DraggedWindowTab { @@ -190,6 +193,8 @@ impl SystemWindowTabs { title: item.title.to_string(), width, is_active, + active_background_color, + inactive_background_color, }, |tab, _, _, cx| cx.new(|_| tab.clone()), ) @@ -273,26 +278,38 @@ impl SystemWindowTabs { impl Render for SystemWindowTabs { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let active_background_color = cx.theme().colors().title_bar_background; + let inactive_background_color = cx.theme().colors().tab_bar_background; + let entity = cx.entity(); let number_of_tabs = self.tabs.len(); let tab_items = self .tabs .iter() .enumerate() - .map(|(ix, item)| self.render_tab(ix, item.clone(), window, cx)) + .map(|(ix, item)| { + self.render_tab( + ix, + item.clone(), + active_background_color, + inactive_background_color, + window, + cx, + ) + }) .collect::>(); h_flex() .w_full() .h(Tab::container_height(cx)) - .bg(cx.theme().colors().editor_background) + .bg(inactive_background_color) .child( h_flex() .id("window tabs") .w_full() .h_full() .h(Tab::container_height(cx)) - .bg(cx.theme().colors().editor_background) + .bg(inactive_background_color) .overflow_x_scroll() .w_full() .track_scroll(&self.tab_bar_scroll_handle) @@ -361,9 +378,9 @@ impl Render for DraggedWindowTab { .px(DynamicSpacing::Base16.px(cx)) .justify_center() .bg(if self.is_active { - cx.theme().colors().title_bar_background + self.active_background_color } else { - cx.theme().colors().editor_background + self.inactive_background_color }) .border_1() .border_color(cx.theme().colors().border) From acfb5da472025595e1c73031a1b3c6b47de51224 Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Sun, 13 Jul 2025 20:39:58 +0200 Subject: [PATCH 20/42] Simplify tab state tracking --- crates/gpui/src/app.rs | 45 +++++---- crates/gpui/src/platform.rs | 1 + crates/gpui/src/platform/mac/window.rs | 25 +++++ crates/gpui/src/window.rs | 36 +++----- crates/title_bar/src/system_window_tabs.rs | 102 +++++++++++---------- 5 files changed, 122 insertions(+), 87 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index a086c571f9..217efccebb 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -284,28 +284,20 @@ impl SystemWindowTabController { } /// Add a window to a tab group. - pub fn add_window( - cx: &mut App, - tab_group: usize, - handle: AnyWindowHandle, - title: SharedString, - ) { + pub fn add_window(cx: &mut App, window: &Window) { let mut controller = cx.global_mut::(); - let id = handle.window_id(); - for (existing_group, windows) in controller.tabs.iter_mut() { - if *existing_group != tab_group { - if let Some(pos) = windows.iter().position(|tab| tab.id == id) { - windows.remove(pos); - } + let tab_group = window.tab_group(); + let title = SharedString::from(window.window_title()); + let handle = window.window_handle(); + let id = handle.id; + + if let Some(tab_group) = tab_group { + let windows = controller.tabs.entry(tab_group).or_insert_with(Vec::new); + if !windows.iter().any(|tab| tab.id == id) { + windows.push(SystemWindowTab::new(id, title, handle)); } } - - controller.tabs.retain(|_, windows| !windows.is_empty()); - let windows = controller.tabs.entry(tab_group).or_insert_with(Vec::new); - if !windows.iter().any(|tab| tab.id == id) { - windows.push(SystemWindowTab::new(id, title, handle)); - } } /// Remove a window from a tab group. @@ -359,6 +351,23 @@ impl SystemWindowTabController { } } + /// Sync the system window tab groups with the application's tab groups. + pub fn sync_system_window_tab_groups(cx: &mut App, window: &Window) { + let mut controller = cx.global_mut::(); + controller.tabs.clear(); + + let windows = cx.windows(); + for w in windows { + if w.id == window.window_handle().id { + SystemWindowTabController::add_window(cx, &window); + } else { + let _ = w.update(cx, |_, window, cx| { + SystemWindowTabController::add_window(cx, &window); + }); + } + } + } + /// Selects the next tab in the tab group in the trailing direction. pub fn select_next_tab(cx: &mut App, tab_group: usize, id: WindowId) { let mut controller = cx.global_mut::(); diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index b6e8068262..c02f74080c 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -509,6 +509,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle { fn on_select_previous_tab(&self, _callback: Box) {} fn on_select_next_tab(&self, _callback: Box) {} fn on_merge_all_windows(&self, _callback: Box) {} + fn on_move_tab_to_new_window(&self, _callback: Box) {} fn merge_all_windows(&self) {} fn move_tab_to_new_window(&self) {} fn toggle_window_tab_overview(&self) {} diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index ecc9ef2c3a..da99afcef7 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -371,6 +371,11 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C merge_all_windows as extern "C" fn(&Object, Sel, id), ); + decl.add_method( + sel!(moveTabToNewWindow:), + move_tab_to_new_window as extern "C" fn(&Object, Sel, id), + ); + decl.register() } } @@ -406,6 +411,7 @@ struct MacWindowState { select_next_tab_callback: Option>, select_previous_tab_callback: Option>, merge_all_windows_callback: Option>, + move_tab_to_new_window_callback: Option>, } impl MacWindowState { @@ -700,6 +706,7 @@ impl MacWindow { select_next_tab_callback: None, select_previous_tab_callback: None, merge_all_windows_callback: None, + move_tab_to_new_window_callback: None, }))); (*native_window).set_ivar( @@ -1363,6 +1370,10 @@ impl PlatformWindow for MacWindow { self.0.as_ref().lock().merge_all_windows_callback = Some(_callback); } + fn on_move_tab_to_new_window(&self, _callback: Box) { + self.0.as_ref().lock().move_tab_to_new_window_callback = Some(_callback); + } + fn draw(&self, scene: &crate::Scene) { let mut this = self.0.lock(); this.renderer.draw(scene); @@ -2478,3 +2489,17 @@ extern "C" fn merge_all_windows(this: &Object, _sel: Sel, _id: id) { window_state.lock().merge_all_windows_callback = Some(callback); } } + +extern "C" fn move_tab_to_new_window(this: &Object, _sel: Sel, _id: id) { + unsafe { + let _: () = msg_send![super(this, class!(NSWindow)), moveTabToNewWindow:nil]; + } + + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.as_ref().lock(); + if let Some(mut callback) = lock.move_tab_to_new_window_callback.take() { + drop(lock); + callback(); + window_state.lock().move_tab_to_new_window_callback = Some(callback); + } +} diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 170e59aa65..8b78112c2d 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -1089,16 +1089,6 @@ impl Window { .clone() .retain(&(), |callback| callback(window, cx)); - let tab_group = window.tab_group(); - if let Some(tab_group) = tab_group { - SystemWindowTabController::add_window( - cx, - tab_group, - handle, - SharedString::from(window.window_title()), - ); - } - window.bounds_changed(cx); window.refresh(); }) @@ -1181,23 +1171,24 @@ impl Window { .log_err(); }) }); + platform_window.on_move_tab_to_new_window({ + let mut cx = cx.to_async(); + Box::new(move || { + handle + .update(&mut cx, |_, window, cx| { + SystemWindowTabController::sync_system_window_tab_groups(cx, window); + }) + .log_err(); + }) + }); if let Some(app_id) = app_id { platform_window.set_app_id(&app_id); } platform_window.map_window().unwrap(); - let tab_group = platform_window.tab_group(); - if let Some(tab_group) = tab_group { - SystemWindowTabController::add_window( - cx, - tab_group, - handle, - SharedString::from(platform_window.get_title()), - ); - } - Ok(Window { + let window = Window { handle, invalidator, removed: false, @@ -1251,7 +1242,10 @@ impl Window { image_cache_stack: Vec::new(), #[cfg(any(feature = "inspector", debug_assertions))] inspector: None, - }) + }; + + SystemWindowTabController::add_window(cx, &window); + Ok(window) } pub(crate) fn new_focus_listener( diff --git a/crates/title_bar/src/system_window_tabs.rs b/crates/title_bar/src/system_window_tabs.rs index f78cc6bd4f..c229e35e4c 100644 --- a/crates/title_bar/src/system_window_tabs.rs +++ b/crates/title_bar/src/system_window_tabs.rs @@ -4,8 +4,7 @@ use std::any::TypeId; use gpui::{ Context, Hsla, InteractiveElement, ParentElement, ScrollHandle, Styled, Subscription, - SystemWindowTab, SystemWindowTabController, Window, WindowId, actions, black, canvas, div, - white, + SystemWindowTab, SystemWindowTabController, Window, WindowId, actions, canvas, div, }; use ui::{ Color, ContextMenu, DynamicSpacing, IconButton, IconButtonShape, IconName, IconSize, Label, @@ -46,6 +45,7 @@ pub struct SystemWindowTabs { impl SystemWindowTabs { pub fn new(window: &mut Window, cx: &mut Context) -> Self { + let window_id = window.window_handle().window_id(); let tab_group = window.tab_group(); let mut subscriptions = Vec::new(); @@ -67,67 +67,73 @@ impl SystemWindowTabs { } }) .register_action(|_, _: &MergeAllWindows, window, cx| { + window.merge_all_windows(); if let Some(tab_group) = window.tab_group() { SystemWindowTabController::merge_all_windows(cx, tab_group); } }) - .register_action(|_, _: &MoveTabToNewWindow, window, _cx| { - window.move_tab_to_new_window() + .register_action(|_, _: &MoveTabToNewWindow, window, cx| { + window.move_tab_to_new_window(); + SystemWindowTabController::sync_system_window_tab_groups(cx, window) }); }, )); - subscriptions.push(cx.observe_global::(|this, cx| { - if let Some(tab_group) = this.tab_group { + subscriptions.push( + cx.observe_global::(move |this, cx| { let controller = cx.global::(); + let tab_group = controller.tabs().iter().find_map(|(group, windows)| { + windows + .iter() + .find(|tab| tab.id == window_id) + .map(|_| *group) + }); - let all_tab_groups = controller.tabs(); - let all_tabs = controller.windows(tab_group); - if let Some(tabs) = all_tabs { - let show_merge_all_windows = all_tab_groups.len() > 1; - let show_other_tab_actions = tabs.len() > 1; + if let Some(tab_group) = tab_group { + let all_tab_groups = controller.tabs(); + let all_tabs = controller.windows(tab_group); + if let Some(tabs) = all_tabs { + let show_merge_all_windows = all_tab_groups.len() > 1; + let show_other_tab_actions = tabs.len() > 1; - this.tabs = tabs.clone(); - cx.notify(); + this.tabs = tabs.clone(); + cx.notify(); - let merge_all_windows_action = TypeId::of::(); - let other_tab_actions = vec![ - TypeId::of::(), - TypeId::of::(), - TypeId::of::(), - ]; + let merge_all_windows_action = TypeId::of::(); + let other_tab_actions = vec![ + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + ]; - if show_merge_all_windows && show_other_tab_actions { - CommandPaletteFilter::update_global(cx, |filter, _cx| { - let mut all_actions = vec![merge_all_windows_action]; - all_actions.extend(other_tab_actions.iter().cloned()); - filter.show_action_types(all_actions.iter()); - }); - } else if show_merge_all_windows { - CommandPaletteFilter::update_global(cx, |filter, _cx| { - filter.show_action_types(std::iter::once(&merge_all_windows_action)); - filter.hide_action_types(&other_tab_actions); - }); - } else if show_other_tab_actions { - CommandPaletteFilter::update_global(cx, |filter, _cx| { - filter.show_action_types(other_tab_actions.iter()); - filter.hide_action_types(&[merge_all_windows_action]); - }); - } else { - CommandPaletteFilter::update_global(cx, |filter, _cx| { - let mut all_actions = vec![merge_all_windows_action]; - all_actions.extend(other_tab_actions.iter().cloned()); - filter.hide_action_types(&all_actions); - }); + if show_merge_all_windows && show_other_tab_actions { + CommandPaletteFilter::update_global(cx, |filter, _cx| { + let mut all_actions = vec![merge_all_windows_action]; + all_actions.extend(other_tab_actions.iter().cloned()); + filter.show_action_types(all_actions.iter()); + }); + } else if show_merge_all_windows { + CommandPaletteFilter::update_global(cx, |filter, _cx| { + filter + .show_action_types(std::iter::once(&merge_all_windows_action)); + filter.hide_action_types(&other_tab_actions); + }); + } else if show_other_tab_actions { + CommandPaletteFilter::update_global(cx, |filter, _cx| { + filter.show_action_types(other_tab_actions.iter()); + filter.hide_action_types(&[merge_all_windows_action]); + }); + } else { + CommandPaletteFilter::update_global(cx, |filter, _cx| { + let mut all_actions = vec![merge_all_windows_action]; + all_actions.extend(other_tab_actions.iter().cloned()); + filter.hide_action_types(&all_actions); + }); + } } } - } - })); - - subscriptions.push(cx.observe_window_activation(window, |this, window, cx| { - this.tab_group = window.tab_group(); - cx.notify(); - })); + }), + ); Self { tab_group, From 69b14f928a160dc279b472b949e7dc6583f9a99f Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Wed, 16 Jul 2025 00:23:48 +0200 Subject: [PATCH 21/42] Simplify macOS tabbing mode --- crates/agent_ui/src/ui/agent_notification.rs | 2 +- crates/collab_ui/src/collab_ui.rs | 2 +- crates/gpui/src/app.rs | 14 ++++ crates/gpui/src/platform.rs | 8 +- crates/gpui/src/platform/mac/window.rs | 78 +++----------------- crates/gpui/src/window.rs | 11 +-- crates/title_bar/src/system_window_tabs.rs | 3 - crates/zed/src/zed.rs | 6 +- 8 files changed, 39 insertions(+), 85 deletions(-) diff --git a/crates/agent_ui/src/ui/agent_notification.rs b/crates/agent_ui/src/ui/agent_notification.rs index 05a2835c57..948552540a 100644 --- a/crates/agent_ui/src/ui/agent_notification.rs +++ b/crates/agent_ui/src/ui/agent_notification.rs @@ -62,7 +62,7 @@ impl AgentNotification { app_id: Some(app_id.to_owned()), window_min_size: None, window_decorations: Some(WindowDecorations::Client), - allows_automatic_window_tabbing: None, + tabbing_identifier: None, } } } diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index a8d7f308a9..dbb9106e17 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -66,6 +66,6 @@ fn notification_window_options( app_id: Some(app_id.to_owned()), window_min_size: None, window_decorations: Some(WindowDecorations::Client), - allows_automatic_window_tabbing: None, + tabbing_identifier: None, } } diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 217efccebb..51172faa9d 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -819,6 +819,20 @@ impl App { cx.window_update_stack.pop(); window.root.replace(root_view.into()); window.defer(cx, |window: &mut Window, cx| window.appearance_changed(cx)); + + // TODO: Find a less hacky way to get the tab group after window creation, + // without interfering with the automatic window tabbing. + window + .spawn(cx, async move |cx| { + cx.background_executor() + .timer(Duration::from_millis(200)) + .await; + cx.update(|window, cx| { + SystemWindowTabController::add_window(cx, window); + }) + }) + .detach(); + cx.window_handles.insert(id, window.handle); cx.windows.get_mut(id).unwrap().replace(window); Ok(handle) diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index c02f74080c..1372cb440b 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -1119,8 +1119,8 @@ pub struct WindowOptions { /// Note that this may be ignored. pub window_decorations: Option, - /// Whether to allow automatic window tabbing. macOS only. - pub allows_automatic_window_tabbing: Option, + /// Tab group name, allows opening the window as a native tab on macOS 10.12+. Windows with the same tabbing identifier will be grouped together. + pub tabbing_identifier: Option, } /// The variables that can be configured when creating a new window @@ -1160,7 +1160,7 @@ pub(crate) struct WindowParams { pub display_id: Option, pub window_min_size: Option>, - pub allows_automatic_window_tabbing: Option, + pub tabbing_identifier: Option, } /// Represents the status of how a window should be opened. @@ -1211,7 +1211,7 @@ impl Default for WindowOptions { app_id: None, window_min_size: None, window_decorations: None, - allows_automatic_window_tabbing: None, + tabbing_identifier: None, } } } diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index da99afcef7..5a5eaeeef0 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -571,7 +571,7 @@ impl MacWindow { show, display_id, window_min_size, - allows_automatic_window_tabbing, + tabbing_identifier, }: WindowParams, executor: ForegroundExecutor, renderer_context: renderer::Context, @@ -579,13 +579,6 @@ impl MacWindow { unsafe { let pool = NSAutoreleasePool::new(nil); - let allows_automatic_window_tabbing = allows_automatic_window_tabbing.unwrap_or(false); - if allows_automatic_window_tabbing { - let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: YES]; - } else { - let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: NO]; - } - let mut style_mask; if let Some(titlebar) = titlebar.as_ref() { style_mask = NSWindowStyleMask::NSClosableWindowMask @@ -719,6 +712,14 @@ impl MacWindow { Arc::into_raw(window.0.clone()) as *const c_void, ); + 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]; + let _: () = msg_send![native_window, setTabbingMode: 0]; + } else { + let _: () = msg_send![native_window, setTabbingMode: 1]; + } + if let Some(title) = titlebar .as_ref() .and_then(|t| t.title.as_ref().map(AsRef::as_ref)) @@ -761,12 +762,6 @@ impl MacWindow { WindowKind::Normal => { native_window.setLevel_(NSNormalWindowLevel); native_window.setAcceptsMouseMovedEvents_(YES); - - // Set tabbing identifier so "Merge All Windows" menu works - if allows_automatic_window_tabbing { - let tabbing_id = NSString::alloc(nil).init_str("zed-window"); - let _: () = msg_send![native_window, setTabbingIdentifier: tabbing_id]; - } } WindowKind::PopUp => { // Use a tracking area to allow receiving MouseMoved events even when @@ -795,30 +790,6 @@ impl MacWindow { } } - let app = NSApplication::sharedApplication(nil); - let main_window: id = msg_send![app, mainWindow]; - - if !main_window.is_null() && main_window != native_window { - let main_window_is_fullscreen = main_window - .styleMask() - .contains(NSWindowStyleMask::NSFullScreenWindowMask); - let user_tabbing_preference = Self::get_user_tabbing_preference() - .unwrap_or(UserTabbingPreference::InFullScreen); - let should_add_as_tab = user_tabbing_preference == UserTabbingPreference::Always - || user_tabbing_preference == UserTabbingPreference::InFullScreen - && main_window_is_fullscreen; - - if allows_automatic_window_tabbing && should_add_as_tab { - let main_window_can_tab: BOOL = - msg_send![main_window, respondsToSelector: sel!(addTabbedWindow:ordered:)]; - let main_window_visible: BOOL = msg_send![main_window, isVisible]; - - if main_window_can_tab == YES && main_window_visible == YES { - let _: () = msg_send![main_window, addTabbedWindow: native_window ordered: NSWindowOrderingMode::NSWindowAbove]; - } - } - } - if focus && show { native_window.makeKeyAndOrderFront_(nil); } else if show { @@ -873,33 +844,6 @@ impl MacWindow { window_handles } } - - pub fn get_user_tabbing_preference() -> Option { - unsafe { - let defaults: id = NSUserDefaults::standardUserDefaults(); - let domain = NSString::alloc(nil).init_str("NSGlobalDomain"); - let key = NSString::alloc(nil).init_str("AppleWindowTabbingMode"); - - let dict: id = msg_send![defaults, persistentDomainForName: domain]; - let value: id = if !dict.is_null() { - msg_send![dict, objectForKey: key] - } else { - nil - }; - - let value_str = if !value.is_null() { - CStr::from_ptr(NSString::UTF8String(value)).to_string_lossy() - } else { - "".into() - }; - - match value_str.as_ref() { - "manual" => Some(UserTabbingPreference::Never), - "always" => Some(UserTabbingPreference::Always), - _ => Some(UserTabbingPreference::InFullScreen), - } - } - } } impl Drop for MacWindow { @@ -1349,10 +1293,8 @@ impl PlatformWindow for MacWindow { } fn tab_group(&self) -> Option { - let mut this = self.0.lock(); - unsafe { - let tabgroup: id = msg_send![this.native_window, tabGroup]; + let tabgroup: id = msg_send![self.0.lock().native_window, tabGroup]; let tabgroup_id = tabgroup as *const Object as usize; Some(tabgroup_id) } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 8b78112c2d..56cae2ae61 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -944,7 +944,7 @@ impl Window { app_id, window_min_size, window_decorations, - allows_automatic_window_tabbing, + tabbing_identifier, } = options; let bounds = window_bounds @@ -961,7 +961,7 @@ impl Window { show, display_id, window_min_size, - allows_automatic_window_tabbing, + tabbing_identifier, }, )?; let display_id = platform_window.display().map(|display| display.id()); @@ -1188,7 +1188,7 @@ impl Window { platform_window.map_window().unwrap(); - let window = Window { + Ok(Window { handle, invalidator, removed: false, @@ -1242,10 +1242,7 @@ impl Window { image_cache_stack: Vec::new(), #[cfg(any(feature = "inspector", debug_assertions))] inspector: None, - }; - - SystemWindowTabController::add_window(cx, &window); - Ok(window) + }) } pub(crate) fn new_focus_listener( diff --git a/crates/title_bar/src/system_window_tabs.rs b/crates/title_bar/src/system_window_tabs.rs index c229e35e4c..bb9e81409f 100644 --- a/crates/title_bar/src/system_window_tabs.rs +++ b/crates/title_bar/src/system_window_tabs.rs @@ -36,7 +36,6 @@ pub struct DraggedWindowTab { } pub struct SystemWindowTabs { - tab_group: Option, tabs: Vec, tab_bar_scroll_handle: ScrollHandle, measured_tab_width: Pixels, @@ -46,7 +45,6 @@ pub struct SystemWindowTabs { impl SystemWindowTabs { pub fn new(window: &mut Window, cx: &mut Context) -> Self { let window_id = window.window_handle().window_id(); - let tab_group = window.tab_group(); let mut subscriptions = Vec::new(); subscriptions.push(cx.observe_new( @@ -136,7 +134,6 @@ impl SystemWindowTabs { ); Self { - tab_group, tabs: Vec::new(), tab_bar_scroll_handle: ScrollHandle::new(), measured_tab_width: window.bounds().size.width, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 35cf78823c..62f6cac68b 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -303,7 +303,11 @@ pub fn build_window_options(display_uuid: Option, cx: &mut App) -> WindowO width: px(360.0), height: px(240.0), }), - allows_automatic_window_tabbing: Some(use_system_window_tabs), + tabbing_identifier: if use_system_window_tabs { + Some(String::from("zed")) + } else { + None + }, } } From accbe1a7bdd85f3e29be8ba3af97289f29cf0a78 Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Fri, 18 Jul 2025 14:06:19 +0200 Subject: [PATCH 22/42] Imlement KVO for tab group changes --- crates/gpui/src/app.rs | 28 ------- crates/gpui/src/platform.rs | 3 +- crates/gpui/src/platform/mac/window.rs | 91 +++++++++++----------- crates/gpui/src/window.rs | 16 +--- crates/title_bar/src/system_window_tabs.rs | 12 +-- 5 files changed, 56 insertions(+), 94 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 51172faa9d..9631f3a683 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -337,20 +337,6 @@ impl SystemWindowTabController { } } - /// Merge all windows to a single tab group. - pub fn merge_all_windows(cx: &mut App, tab_group: usize) { - let mut controller = cx.global_mut::(); - let mut all_windows = Vec::new(); - for windows in controller.tabs.values() { - all_windows.extend(windows.iter().cloned()); - } - - controller.tabs.clear(); - if !all_windows.is_empty() { - controller.tabs.insert(tab_group, all_windows); - } - } - /// Sync the system window tab groups with the application's tab groups. pub fn sync_system_window_tab_groups(cx: &mut App, window: &Window) { let mut controller = cx.global_mut::(); @@ -819,20 +805,6 @@ impl App { cx.window_update_stack.pop(); window.root.replace(root_view.into()); window.defer(cx, |window: &mut Window, cx| window.appearance_changed(cx)); - - // TODO: Find a less hacky way to get the tab group after window creation, - // without interfering with the automatic window tabbing. - window - .spawn(cx, async move |cx| { - cx.background_executor() - .timer(Duration::from_millis(200)) - .await; - cx.update(|window, cx| { - SystemWindowTabController::add_window(cx, window); - }) - }) - .detach(); - cx.window_handles.insert(id, window.handle); cx.windows.get_mut(id).unwrap().replace(window); Ok(handle) diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 1372cb440b..c3eba656cf 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -508,8 +508,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle { fn titlebar_double_click(&self) {} fn on_select_previous_tab(&self, _callback: Box) {} fn on_select_next_tab(&self, _callback: Box) {} - fn on_merge_all_windows(&self, _callback: Box) {} - fn on_move_tab_to_new_window(&self, _callback: Box) {} + fn on_tab_group_changed(&self, _callback: Box) {} fn merge_all_windows(&self) {} fn move_tab_to_new_window(&self) {} fn toggle_window_tab_overview(&self) {} diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 5a5eaeeef0..6ad6b67a35 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -84,13 +84,6 @@ const NSDragOperationNone: NSDragOperation = 0; #[allow(non_upper_case_globals)] const NSDragOperationCopy: NSDragOperation = 1; -#[derive(PartialEq)] -pub enum UserTabbingPreference { - Never, - Always, - InFullScreen, -} - #[link(name = "CoreGraphics", kind = "framework")] unsafe extern "C" { // Widely used private APIs; Apple uses them for their Terminal.app. @@ -366,14 +359,10 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C select_previous_tab as extern "C" fn(&Object, Sel, id), ); + // Add this to your window class decl.add_method( - sel!(mergeAllWindows:), - merge_all_windows as extern "C" fn(&Object, Sel, id), - ); - - decl.add_method( - sel!(moveTabToNewWindow:), - move_tab_to_new_window as extern "C" fn(&Object, Sel, id), + sel!(observeValueForKeyPath:ofObject:change:context:), + observe_value_for_key_path as extern "C" fn(&Object, Sel, id, id, id, *mut c_void), ); decl.register() @@ -410,8 +399,7 @@ struct MacWindowState { fullscreen_restore_bounds: Bounds, select_next_tab_callback: Option>, select_previous_tab_callback: Option>, - merge_all_windows_callback: Option>, - move_tab_to_new_window_callback: Option>, + tab_group_changed_callback: Option>, } impl MacWindowState { @@ -698,8 +686,7 @@ impl MacWindow { fullscreen_restore_bounds: Bounds::default(), select_next_tab_callback: None, select_previous_tab_callback: None, - merge_all_windows_callback: None, - move_tab_to_new_window_callback: None, + tab_group_changed_callback: None, }))); (*native_window).set_ivar( @@ -716,6 +703,7 @@ impl MacWindow { let tabbing_id = NSString::alloc(nil).init_str(tabbing_identifier.as_str()); let _: () = msg_send![native_window, setTabbingIdentifier: tabbing_id]; let _: () = msg_send![native_window, setTabbingMode: 0]; + init_tab_group_observer(native_window) } else { let _: () = msg_send![native_window, setTabbingMode: 1]; } @@ -853,6 +841,7 @@ impl Drop for MacWindow { let window = this.native_window; this.display_link.take(); unsafe { + remove_tab_group_kvo_observer(window); this.native_window.setDelegate_(nil); } this.input_handler.take(); @@ -1308,12 +1297,8 @@ impl PlatformWindow for MacWindow { self.0.as_ref().lock().select_previous_tab_callback = Some(callback); } - fn on_merge_all_windows(&self, _callback: Box) { - self.0.as_ref().lock().merge_all_windows_callback = Some(_callback); - } - - fn on_move_tab_to_new_window(&self, _callback: Box) { - self.0.as_ref().lock().move_tab_to_new_window_callback = Some(_callback); + fn on_tab_group_changed(&self, _callback: Box) { + self.0.as_ref().lock().tab_group_changed_callback = Some(_callback); } fn draw(&self, scene: &crate::Scene) { @@ -2418,30 +2403,48 @@ extern "C" fn select_previous_tab(this: &Object, _sel: Sel, _id: id) { } } -extern "C" fn merge_all_windows(this: &Object, _sel: Sel, _id: id) { +unsafe fn init_tab_group_observer(this: *mut Object) { unsafe { - let _: () = msg_send![super(this, class!(NSWindow)), mergeAllWindows:nil]; - } - - let window_state = unsafe { get_window_state(this) }; - let mut lock = window_state.as_ref().lock(); - if let Some(mut callback) = lock.merge_all_windows_callback.take() { - drop(lock); - callback(); - window_state.lock().merge_all_windows_callback = Some(callback); + let _: () = msg_send![this, + addObserver:this + forKeyPath:ns_string("tabGroup") + options:1u64 // NSKeyValueObservingOptionNew + context:std::ptr::null_mut::()]; } } -extern "C" fn move_tab_to_new_window(this: &Object, _sel: Sel, _id: id) { +unsafe fn remove_tab_group_kvo_observer(this: *mut Object) { unsafe { - let _: () = msg_send![super(this, class!(NSWindow)), moveTabToNewWindow:nil]; - } - - let window_state = unsafe { get_window_state(this) }; - let mut lock = window_state.as_ref().lock(); - if let Some(mut callback) = lock.move_tab_to_new_window_callback.take() { - drop(lock); - callback(); - window_state.lock().move_tab_to_new_window_callback = Some(callback); + let _: () = msg_send![this, + removeObserver:this + forKeyPath:ns_string("tabGroup") + context:std::ptr::null_mut::()]; + } +} + +extern "C" fn observe_value_for_key_path( + this: &Object, + _sel: Sel, + key_path: id, + _object: id, + change: id, + _context: *mut c_void, +) { + unsafe { + if key_path.isEqualToString("tabGroup") { + let tabgroup_id = change as *const Object as usize; + let window_state = get_window_state(this); + let queue: id = msg_send![class!(NSOperationQueue), mainQueue]; + let block = ConcreteBlock::new(move || { + let mut lock = window_state.as_ref().lock(); + if let Some(mut callback) = lock.tab_group_changed_callback.take() { + drop(lock); + callback(tabgroup_id); + window_state.lock().tab_group_changed_callback = Some(callback); + } + }) + .copy(); + let _: () = msg_send![queue, addOperationWithBlock: &*block]; + } } } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 56cae2ae61..919c0362d0 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -1159,21 +1159,9 @@ impl Window { .log_err(); }) }); - platform_window.on_merge_all_windows({ + platform_window.on_tab_group_changed({ let mut cx = cx.to_async(); - Box::new(move || { - handle - .update(&mut cx, |_, window, cx| { - if let Some(tab_group) = window.tab_group() { - SystemWindowTabController::merge_all_windows(cx, tab_group); - } - }) - .log_err(); - }) - }); - platform_window.on_move_tab_to_new_window({ - let mut cx = cx.to_async(); - Box::new(move || { + Box::new(move |_tab_group| { handle .update(&mut cx, |_, window, cx| { SystemWindowTabController::sync_system_window_tab_groups(cx, window); diff --git a/crates/title_bar/src/system_window_tabs.rs b/crates/title_bar/src/system_window_tabs.rs index bb9e81409f..855c5a4c6e 100644 --- a/crates/title_bar/src/system_window_tabs.rs +++ b/crates/title_bar/src/system_window_tabs.rs @@ -64,15 +64,11 @@ impl SystemWindowTabs { ); } }) - .register_action(|_, _: &MergeAllWindows, window, cx| { + .register_action(|_, _: &MergeAllWindows, window, _cx| { window.merge_all_windows(); - if let Some(tab_group) = window.tab_group() { - SystemWindowTabController::merge_all_windows(cx, tab_group); - } }) - .register_action(|_, _: &MoveTabToNewWindow, window, cx| { + .register_action(|_, _: &MoveTabToNewWindow, window, _cx| { window.move_tab_to_new_window(); - SystemWindowTabController::sync_system_window_tab_groups(cx, window) }); }, )); @@ -302,6 +298,10 @@ impl Render for SystemWindowTabs { }) .collect::>(); + if number_of_tabs < 2 { + return h_flex().into_any_element(); + } + h_flex() .w_full() .h(Tab::container_height(cx)) From 9e28fd0ce4ec8b45401a986e20f61f3c06af0f1c Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Sun, 20 Jul 2025 18:04:24 +0200 Subject: [PATCH 23/42] Improve window restoration --- crates/gpui/src/app.rs | 66 +++++++++----------- crates/gpui/src/platform/mac/window.rs | 86 ++++++++++++++++++++++---- crates/gpui/src/window.rs | 4 +- crates/zed/src/main.rs | 19 ++++-- 4 files changed, 122 insertions(+), 53 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 9631f3a683..e732de15eb 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -247,8 +247,12 @@ pub struct SystemWindowTab { impl SystemWindowTab { /// Create a new instance of the window tab. - pub fn new(id: WindowId, title: SharedString, handle: AnyWindowHandle) -> Self { - Self { id, title, handle } + pub fn new(title: SharedString, handle: AnyWindowHandle) -> Self { + Self { + id: handle.id, + title, + handle, + } } } @@ -283,21 +287,25 @@ impl SystemWindowTabController { self.tabs.get(&tab_group) } - /// Add a window to a tab group. - pub fn add_window(cx: &mut App, window: &Window) { + /// Insert a window into a tab group. + pub fn insert_window(cx: &mut App, window: &Window, tab_group: usize) { let mut controller = cx.global_mut::(); - let tab_group = window.tab_group(); let title = SharedString::from(window.window_title()); let handle = window.window_handle(); - let id = handle.id; - if let Some(tab_group) = tab_group { - let windows = controller.tabs.entry(tab_group).or_insert_with(Vec::new); - if !windows.iter().any(|tab| tab.id == id) { - windows.push(SystemWindowTab::new(id, title, handle)); + for windows in controller.tabs.values_mut() { + if let Some(pos) = windows.iter().position(|tab| tab.id == handle.id) { + windows.remove(pos); } } + + controller.tabs.retain(|_, windows| !windows.is_empty()); + + let windows = controller.tabs.entry(tab_group).or_insert_with(Vec::new); + if !windows.iter().any(|tab| tab.id == handle.id) { + windows.push(SystemWindowTab::new(title, handle)); + } } /// Remove a window from a tab group. @@ -327,6 +335,17 @@ impl SystemWindowTabController { /// Update the title of a window. pub fn update_window_title(cx: &mut App, id: WindowId, title: SharedString) { + let controller = cx.global::(); + let tab = controller + .tabs + .values() + .flat_map(|windows| windows.iter()) + .find(|tab| tab.id == id); + + if tab.map_or(true, |t| t.title == title) { + return; + } + let mut controller = cx.global_mut::(); for windows in controller.tabs.values_mut() { for tab in windows.iter_mut() { @@ -337,23 +356,6 @@ impl SystemWindowTabController { } } - /// Sync the system window tab groups with the application's tab groups. - pub fn sync_system_window_tab_groups(cx: &mut App, window: &Window) { - let mut controller = cx.global_mut::(); - controller.tabs.clear(); - - let windows = cx.windows(); - for w in windows { - if w.id == window.window_handle().id { - SystemWindowTabController::add_window(cx, &window); - } else { - let _ = w.update(cx, |_, window, cx| { - SystemWindowTabController::add_window(cx, &window); - }); - } - } - } - /// Selects the next tab in the tab group in the trailing direction. pub fn select_next_tab(cx: &mut App, tab_group: usize, id: WindowId) { let mut controller = cx.global_mut::(); @@ -368,26 +370,18 @@ impl SystemWindowTabController { /// Selects the previous tab in the tab group in the leading direction. pub fn select_previous_tab(cx: &mut App, tab_group: usize, id: WindowId) { - log::info!("select_previous_tab"); let mut controller = cx.global_mut::(); let windows = controller.tabs.get_mut(&tab_group).unwrap(); let current_index = windows.iter().position(|tab| tab.id == id).unwrap(); - log::info!("current_index: {}", current_index); let previous_index = if current_index == 0 { windows.len() - 1 } else { current_index - 1 }; - log::info!("previous_index: {}", previous_index); - let result = &windows[previous_index].handle.update(cx, |_, window, _| { - log::info!("activate_window"); + let _ = &windows[previous_index].handle.update(cx, |_, window, _| { window.activate_window(); }); - - if let Err(err) = result { - log::info!("Error activating window: {}", err); - } } } diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 6ad6b67a35..d413a0216e 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -83,6 +83,12 @@ type NSDragOperation = NSUInteger; const NSDragOperationNone: NSDragOperation = 0; #[allow(non_upper_case_globals)] const NSDragOperationCopy: NSDragOperation = 1; +#[derive(PartialEq)] +pub enum UserTabbingPreference { + Never, + Always, + InFullScreen, +} #[link(name = "CoreGraphics", kind = "framework")] unsafe extern "C" { @@ -567,6 +573,13 @@ impl MacWindow { unsafe { let pool = NSAutoreleasePool::new(nil); + 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]; + } + let mut style_mask; if let Some(titlebar) = titlebar.as_ref() { style_mask = NSWindowStyleMask::NSClosableWindowMask @@ -699,15 +712,6 @@ impl MacWindow { Arc::into_raw(window.0.clone()) as *const c_void, ); - 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]; - let _: () = msg_send![native_window, setTabbingMode: 0]; - init_tab_group_observer(native_window) - } else { - let _: () = msg_send![native_window, setTabbingMode: 1]; - } - if let Some(title) = titlebar .as_ref() .and_then(|t| t.title.as_ref().map(AsRef::as_ref)) @@ -750,6 +754,11 @@ impl MacWindow { WindowKind::Normal => { native_window.setLevel_(NSNormalWindowLevel); native_window.setAcceptsMouseMovedEvents_(YES); + + if let Some(tabbing_identifier) = tabbing_identifier.clone() { + let tabbing_id = NSString::alloc(nil).init_str(tabbing_identifier.as_str()); + let _: () = msg_send![native_window, setTabbingIdentifier: tabbing_id]; + } } WindowKind::PopUp => { // Use a tracking area to allow receiving MouseMoved events even when @@ -778,6 +787,34 @@ impl MacWindow { } } + init_tab_group_observer(native_window); + let app = NSApplication::sharedApplication(nil); + let main_window: id = msg_send![app, mainWindow]; + if allows_automatic_window_tabbing + && !main_window.is_null() + && main_window != native_window + { + let main_window_is_fullscreen = main_window + .styleMask() + .contains(NSWindowStyleMask::NSFullScreenWindowMask); + let user_tabbing_preference = Self::get_user_tabbing_preference() + .unwrap_or(UserTabbingPreference::InFullScreen); + let should_add_as_tab = user_tabbing_preference == UserTabbingPreference::Always + || user_tabbing_preference == UserTabbingPreference::InFullScreen + && main_window_is_fullscreen; + + if should_add_as_tab { + let main_window_can_tab: BOOL = + msg_send![main_window, respondsToSelector: sel!(addTabbedWindow:ordered:)]; + let main_window_visible: BOOL = msg_send![main_window, isVisible]; + + if main_window_can_tab == YES && main_window_visible == YES { + let _: () = msg_send![main_window, addTabbedWindow: native_window ordered: NSWindowOrderingMode::NSWindowAbove]; + let _: () = msg_send![native_window, orderFront: nil]; + } + } + } + if focus && show { native_window.makeKeyAndOrderFront_(nil); } else if show { @@ -832,6 +869,33 @@ impl MacWindow { window_handles } } + + pub fn get_user_tabbing_preference() -> Option { + unsafe { + let defaults: id = NSUserDefaults::standardUserDefaults(); + let domain = NSString::alloc(nil).init_str("NSGlobalDomain"); + let key = NSString::alloc(nil).init_str("AppleWindowTabbingMode"); + + let dict: id = msg_send![defaults, persistentDomainForName: domain]; + let value: id = if !dict.is_null() { + msg_send![dict, objectForKey: key] + } else { + nil + }; + + let value_str = if !value.is_null() { + CStr::from_ptr(NSString::UTF8String(value)).to_string_lossy() + } else { + "".into() + }; + + match value_str.as_ref() { + "manual" => Some(UserTabbingPreference::Never), + "always" => Some(UserTabbingPreference::Always), + _ => Some(UserTabbingPreference::InFullScreen), + } + } + } } impl Drop for MacWindow { @@ -2432,14 +2496,14 @@ extern "C" fn observe_value_for_key_path( ) { unsafe { if key_path.isEqualToString("tabGroup") { - let tabgroup_id = change as *const Object as usize; + let tabgroup_id: id = msg_send![change, objectForKey: ns_string("new")]; let window_state = get_window_state(this); let queue: id = msg_send![class!(NSOperationQueue), mainQueue]; let block = ConcreteBlock::new(move || { let mut lock = window_state.as_ref().lock(); if let Some(mut callback) = lock.tab_group_changed_callback.take() { drop(lock); - callback(tabgroup_id); + callback(tabgroup_id as usize); window_state.lock().tab_group_changed_callback = Some(callback); } }) diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 919c0362d0..9a8440658e 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -1161,10 +1161,10 @@ impl Window { }); platform_window.on_tab_group_changed({ let mut cx = cx.to_async(); - Box::new(move |_tab_group| { + Box::new(move |tab_group| { handle .update(&mut cx, |_, window, cx| { - SystemWindowTabController::sync_system_window_tab_groups(cx, window); + SystemWindowTabController::insert_window(cx, window, tab_group); }) .log_err(); }) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index df30d4dd7b..d7f96bd482 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -2,7 +2,7 @@ mod reliability; mod zed; use agent_ui::AgentPanel; -use anyhow::{Context as _, Result}; +use anyhow::{Context as _, Error, Result}; use clap::{Parser, command}; use cli::FORCE_CLI_MODE_ENV_VAR_NAME; use client::{Client, ProxySettings, UserStore, parse_zed_link}; @@ -950,9 +950,13 @@ 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) + .unwrap_or(false); + let mut results: Vec> = Vec::new(); let mut tasks = Vec::new(); - for location in locations { + for (index, location) in locations.into_iter().enumerate() { match location { SerializedWorkspaceLocation::Local(location, _) => { let app_state = app_state.clone(); @@ -968,7 +972,14 @@ async fn restore_or_create_workspace(app_state: Arc, cx: &mut AsyncApp })?; open_task.await.map(|_| ()) }); - tasks.push(task); + + // If we're using system window tabs and this is the first workspace, + // wait for it to finish so that the other windows can be added as tabs. + if use_system_window_tabs && index == 0 { + results.push(task.await); + } else { + tasks.push(task); + } } SerializedWorkspaceLocation::Ssh(ssh) => { let app_state = app_state.clone(); @@ -1002,7 +1013,7 @@ async fn restore_or_create_workspace(app_state: Arc, cx: &mut AsyncApp } // Wait for all workspaces to open concurrently - let results = future::join_all(tasks).await; + results.extend(future::join_all(tasks).await); // Show notifications for any errors that occurred let mut error_count = 0; From d59d7a0e15b7568881c0fba8ac91710b92d9bb74 Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Sun, 20 Jul 2025 18:56:22 +0200 Subject: [PATCH 24/42] Implement tab right click actions --- crates/title_bar/src/system_window_tabs.rs | 76 +++++++++++++++++++--- 1 file changed, 68 insertions(+), 8 deletions(-) diff --git a/crates/title_bar/src/system_window_tabs.rs b/crates/title_bar/src/system_window_tabs.rs index 855c5a4c6e..e979f51a9b 100644 --- a/crates/title_bar/src/system_window_tabs.rs +++ b/crates/title_bar/src/system_window_tabs.rs @@ -241,26 +241,63 @@ impl SystemWindowTabs { ) .into_any(); + let tabs = self.tabs.clone(); let menu = right_click_menu(ix) .trigger(|_, _, _| tab) .menu(move |window, cx| { let focus_handle = cx.focus_handle(); + let tabs = tabs.clone(); + let other_tabs = tabs.clone(); + let move_tabs = tabs.clone(); + let merge_tabs = tabs.clone(); ContextMenu::build(window, cx, move |mut menu, _window_, _cx| { - menu = menu.entry("Close Tab", None, move |_window, _cx| { - // window.dispatch_action(Box::new(CloseWindow), cx); + menu = menu.entry("Close Tab", None, move |window, cx| { + Self::handle_right_click_action( + cx, + window, + &tabs, + |tab| tab.id == item.id, + |window, cx| { + window.dispatch_action(Box::new(CloseWindow), cx); + }, + ); }); - menu = menu.entry("Close Other Tabs", None, move |_window, _cx| { - // window.dispatch_action(Box::new(CloseWindow), cx); + menu = menu.entry("Close Other Tabs", None, move |window, cx| { + Self::handle_right_click_action( + cx, + window, + &other_tabs, + |tab| tab.id != item.id, + |window, cx| { + window.dispatch_action(Box::new(CloseWindow), cx); + }, + ); }); - menu = menu.entry("Move Tab to New Window", None, move |_window, _cx| { - // window.move_tab_to_new_window(); + menu = menu.entry("Move Tab to New Window", None, move |window, cx| { + Self::handle_right_click_action( + cx, + window, + &move_tabs, + |tab| tab.id == item.id, + |window, _cx| { + window.move_tab_to_new_window(); + }, + ); }); - menu = menu.entry("Show All Tabs", None, move |_window, _cx| { - // window.toggle_window_tab_overview(); + menu = menu.entry("Show All Tabs", None, move |window, cx| { + Self::handle_right_click_action( + cx, + window, + &merge_tabs, + |tab| tab.id == item.id, + |window, _cx| { + window.toggle_window_tab_overview(); + }, + ); }); menu.context(focus_handle.clone()) @@ -273,6 +310,29 @@ impl SystemWindowTabs { fn handle_tab_drop(dragged_tab: &DraggedWindowTab, ix: usize, cx: &mut Context) { SystemWindowTabController::update_window_position(cx, dragged_tab.id, ix); } + + fn handle_right_click_action( + cx: &mut App, + window: &mut Window, + tabs: &Vec, + predicate: P, + mut action: F, + ) where + P: Fn(&SystemWindowTab) -> bool, + F: FnMut(&mut Window, &mut App), + { + for tab in tabs { + if predicate(tab) { + if tab.id == window.window_handle().window_id() { + action(window, cx); + } else { + let _ = tab.handle.update(cx, |_view, window, cx| { + action(window, cx); + }); + } + } + } + } } impl Render for SystemWindowTabs { From ba04c3beadcbb4f51d53b97f5c47bf6da2ab56aa Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Sun, 20 Jul 2025 19:38:35 +0200 Subject: [PATCH 25/42] Support toggling the tab bar visibility --- crates/gpui/src/platform.rs | 3 ++ crates/gpui/src/platform/mac/window.rs | 59 ++++++++++++++-------- crates/gpui/src/window.rs | 6 +++ crates/title_bar/src/system_window_tabs.rs | 2 +- 4 files changed, 49 insertions(+), 21 deletions(-) diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index c3eba656cf..eb65f7e648 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -503,6 +503,9 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle { fn get_title(&self) -> String { String::new() } + fn get_tab_bar_visible(&self) -> bool { + false + } fn set_edited(&mut self, _edited: bool) {} fn show_character_palette(&self) {} fn titlebar_double_click(&self) {} diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index d413a0216e..4689b5a995 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -365,7 +365,11 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C select_previous_tab as extern "C" fn(&Object, Sel, id), ); - // Add this to your window class + decl.add_method( + sel!(toggleTabBar:), + toggle_tab_bar as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( sel!(observeValueForKeyPath:ofObject:change:context:), observe_value_for_key_path as extern "C" fn(&Object, Sel, id, id, id, *mut c_void), @@ -1353,6 +1357,18 @@ impl PlatformWindow for MacWindow { } } + fn get_tab_bar_visible(&self) -> bool { + unsafe { + let tab_group: id = msg_send![self.0.lock().native_window, tabGroup]; + if tab_group.is_null() { + false + } else { + let tab_bar_visible: BOOL = msg_send![tab_group, isTabBarVisible]; + tab_bar_visible == YES + } + } + } + fn on_select_next_tab(&self, callback: Box) { self.0.as_ref().lock().select_next_tab_callback = Some(callback); } @@ -1891,9 +1907,6 @@ extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) executor .spawn(async move { let mut lock = window_state.as_ref().lock(); - if is_active { - lock.move_traffic_light(); - } if let Some(mut callback) = lock.activate_callback.take() { drop(lock); @@ -2295,22 +2308,6 @@ extern "C" fn conclude_drag_operation(this: &Object, _: Sel, _: id) { ); } -extern "C" fn add_titlebar_accessory_view_controller(this: &Object, _: Sel, view_controller: id) { - unsafe { - let _: () = msg_send![super(this, class!(NSWindow)), addTitlebarAccessoryViewController: view_controller]; - let accessory_view: id = msg_send![view_controller, view]; - - // Hide the native tab bar and set its height to 0, since we render our own. - let _: () = msg_send![accessory_view, setHidden: YES]; - let mut frame: NSRect = msg_send![accessory_view, frame]; - frame.size.height = 0.0; - let _: () = msg_send![accessory_view, setFrame: frame]; - - let window_state = get_window_state(this); - window_state.as_ref().lock().move_traffic_light(); - } -} - async fn synthetic_drag( window_state: Weak>, drag_id: usize, @@ -2447,6 +2444,19 @@ unsafe fn remove_layer_background(layer: id) { } } +extern "C" fn add_titlebar_accessory_view_controller(this: &Object, _: Sel, view_controller: id) { + unsafe { + let _: () = msg_send![super(this, class!(NSWindow)), addTitlebarAccessoryViewController: view_controller]; + + // Hide the native tab bar and set its height to 0, since we render our own. + let accessory_view: id = msg_send![view_controller, view]; + let _: () = msg_send![accessory_view, setHidden: YES]; + let mut frame: NSRect = msg_send![accessory_view, frame]; + frame.size.height = 0.0; + let _: () = msg_send![accessory_view, setFrame: frame]; + } +} + extern "C" fn select_next_tab(this: &Object, _sel: Sel, _id: id) { let window_state = unsafe { get_window_state(this) }; let mut lock = window_state.as_ref().lock(); @@ -2467,6 +2477,15 @@ extern "C" fn select_previous_tab(this: &Object, _sel: Sel, _id: id) { } } +extern "C" fn toggle_tab_bar(this: &Object, _sel: Sel, _id: id) { + unsafe { + let _: () = msg_send![super(this, class!(NSWindow)), toggleTabBar:nil]; + + let window_state = get_window_state(this); + window_state.as_ref().lock().move_traffic_light(); + } +} + unsafe fn init_tab_group_observer(this: *mut Object) { unsafe { let _: () = msg_send![this, diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 9a8440658e..a5a6da64d4 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -4332,6 +4332,12 @@ impl Window { self.platform_window.get_title() } + /// Gets the visibility of the tab bar at the platform level. + /// This is macOS specific. + pub fn tab_bar_visible(&self) -> bool { + self.platform_window.get_tab_bar_visible() + } + /// Returns the tab group pointer of the window. /// This is macOS specific. pub fn tab_group(&self) -> Option { diff --git a/crates/title_bar/src/system_window_tabs.rs b/crates/title_bar/src/system_window_tabs.rs index e979f51a9b..07410067fd 100644 --- a/crates/title_bar/src/system_window_tabs.rs +++ b/crates/title_bar/src/system_window_tabs.rs @@ -358,7 +358,7 @@ impl Render for SystemWindowTabs { }) .collect::>(); - if number_of_tabs < 2 { + if !window.tab_bar_visible() { return h_flex().into_any_element(); } From 64ec858867c5c9d205c650fa578de6377d24ccd5 Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Sun, 20 Jul 2025 21:13:34 +0200 Subject: [PATCH 26/42] Fix full screen window tabbing --- crates/gpui/src/platform/mac/window.rs | 10 ++- crates/title_bar/src/system_window_tabs.rs | 71 +++++++++++----------- 2 files changed, 44 insertions(+), 37 deletions(-) diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 4689b5a995..e5a6a1b94e 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -814,7 +814,12 @@ impl MacWindow { if main_window_can_tab == YES && main_window_visible == YES { let _: () = msg_send![main_window, addTabbedWindow: native_window ordered: NSWindowOrderingMode::NSWindowAbove]; - let _: () = msg_send![native_window, orderFront: nil]; + + // Ensure the window is visible immediately after adding the tab, since the tab bar is updated with a new entry at this point. + // Note: Calling orderFront here can break fullscreen mode (makes fullscreen windows exit fullscreen), so only do this if the main window is not fullscreen. + if !main_window_is_fullscreen { + let _: () = msg_send![native_window, orderFront: nil]; + } } } } @@ -1907,6 +1912,9 @@ extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) executor .spawn(async move { let mut lock = window_state.as_ref().lock(); + if is_active { + lock.move_traffic_light(); + } if let Some(mut callback) = lock.activate_callback.take() { drop(lock); diff --git a/crates/title_bar/src/system_window_tabs.rs b/crates/title_bar/src/system_window_tabs.rs index 07410067fd..5f38f89acd 100644 --- a/crates/title_bar/src/system_window_tabs.rs +++ b/crates/title_bar/src/system_window_tabs.rs @@ -85,45 +85,44 @@ impl SystemWindowTabs { if let Some(tab_group) = tab_group { let all_tab_groups = controller.tabs(); - let all_tabs = controller.windows(tab_group); - if let Some(tabs) = all_tabs { - let show_merge_all_windows = all_tab_groups.len() > 1; - let show_other_tab_actions = tabs.len() > 1; - + let tabs = controller.windows(tab_group); + let show_merge_all_windows = all_tab_groups.len() > 1; + let show_other_tab_actions = if let Some(tabs) = tabs { this.tabs = tabs.clone(); - cx.notify(); + tabs.len() > 1 + } else { + false + }; - let merge_all_windows_action = TypeId::of::(); - let other_tab_actions = vec![ - TypeId::of::(), - TypeId::of::(), - TypeId::of::(), - ]; + let merge_all_windows_action = TypeId::of::(); + let other_tab_actions = vec![ + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + ]; - if show_merge_all_windows && show_other_tab_actions { - CommandPaletteFilter::update_global(cx, |filter, _cx| { - let mut all_actions = vec![merge_all_windows_action]; - all_actions.extend(other_tab_actions.iter().cloned()); - filter.show_action_types(all_actions.iter()); - }); - } else if show_merge_all_windows { - CommandPaletteFilter::update_global(cx, |filter, _cx| { - filter - .show_action_types(std::iter::once(&merge_all_windows_action)); - filter.hide_action_types(&other_tab_actions); - }); - } else if show_other_tab_actions { - CommandPaletteFilter::update_global(cx, |filter, _cx| { - filter.show_action_types(other_tab_actions.iter()); - filter.hide_action_types(&[merge_all_windows_action]); - }); - } else { - CommandPaletteFilter::update_global(cx, |filter, _cx| { - let mut all_actions = vec![merge_all_windows_action]; - all_actions.extend(other_tab_actions.iter().cloned()); - filter.hide_action_types(&all_actions); - }); - } + if show_merge_all_windows && show_other_tab_actions { + CommandPaletteFilter::update_global(cx, |filter, _cx| { + let mut all_actions = vec![merge_all_windows_action]; + all_actions.extend(other_tab_actions.iter().cloned()); + filter.show_action_types(all_actions.iter()); + }); + } else if show_merge_all_windows { + CommandPaletteFilter::update_global(cx, |filter, _cx| { + filter.show_action_types(std::iter::once(&merge_all_windows_action)); + filter.hide_action_types(&other_tab_actions); + }); + } else if show_other_tab_actions { + CommandPaletteFilter::update_global(cx, |filter, _cx| { + filter.show_action_types(other_tab_actions.iter()); + filter.hide_action_types(&[merge_all_windows_action]); + }); + } else { + CommandPaletteFilter::update_global(cx, |filter, _cx| { + let mut all_actions = vec![merge_all_windows_action]; + all_actions.extend(other_tab_actions.iter().cloned()); + filter.hide_action_types(&all_actions); + }); } } }), From 577314977d7bbfc33b25132c017e3cd90574d360 Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Sun, 20 Jul 2025 22:33:59 +0200 Subject: [PATCH 27/42] Fix conditional commands not showing up --- crates/title_bar/src/system_window_tabs.rs | 51 +++++++++++----------- crates/title_bar/src/title_bar.rs | 2 + 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/crates/title_bar/src/system_window_tabs.rs b/crates/title_bar/src/system_window_tabs.rs index 5f38f89acd..4c92760af0 100644 --- a/crates/title_bar/src/system_window_tabs.rs +++ b/crates/title_bar/src/system_window_tabs.rs @@ -47,32 +47,6 @@ impl SystemWindowTabs { let window_id = window.window_handle().window_id(); let mut subscriptions = Vec::new(); - subscriptions.push(cx.observe_new( - |workspace: &mut Workspace, _window, _cx: &mut Context| { - workspace - .register_action(|_, _: &ShowNextWindowTab, window, cx| { - let window_id = window.window_handle().window_id(); - if let Some(tab_group) = window.tab_group() { - SystemWindowTabController::select_next_tab(cx, tab_group, window_id); - } - }) - .register_action(|_, _: &ShowPreviousWindowTab, window, cx| { - let window_id = window.window_handle().window_id(); - if let Some(tab_group) = window.tab_group() { - SystemWindowTabController::select_previous_tab( - cx, tab_group, window_id, - ); - } - }) - .register_action(|_, _: &MergeAllWindows, window, _cx| { - window.merge_all_windows(); - }) - .register_action(|_, _: &MoveTabToNewWindow, window, _cx| { - window.move_tab_to_new_window(); - }); - }, - )); - subscriptions.push( cx.observe_global::(move |this, cx| { let controller = cx.global::(); @@ -136,6 +110,31 @@ impl SystemWindowTabs { } } + pub fn init(cx: &mut App) { + cx.observe_new(|workspace: &mut Workspace, _, _| { + workspace + .register_action(|_, _: &ShowNextWindowTab, window, cx| { + let window_id = window.window_handle().window_id(); + if let Some(tab_group) = window.tab_group() { + SystemWindowTabController::select_next_tab(cx, tab_group, window_id); + } + }) + .register_action(|_, _: &ShowPreviousWindowTab, window, cx| { + let window_id = window.window_handle().window_id(); + if let Some(tab_group) = window.tab_group() { + SystemWindowTabController::select_previous_tab(cx, tab_group, window_id); + } + }) + .register_action(|_, _: &MergeAllWindows, window, _cx| { + window.merge_all_windows(); + }) + .register_action(|_, _: &MoveTabToNewWindow, window, _cx| { + window.move_tab_to_new_window(); + }); + }) + .detach(); + } + fn render_tab( &self, ix: usize, diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index ed27696f9f..9094ad6601 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -12,6 +12,7 @@ mod stories; use crate::{ application_menu::{ApplicationMenu, show_menus}, platform_title_bar::PlatformTitleBar, + system_window_tabs::SystemWindowTabs, }; #[cfg(not(target_os = "macos"))] @@ -66,6 +67,7 @@ actions!( pub fn init(cx: &mut App) { TitleBarSettings::register(cx); + SystemWindowTabs::init(cx); cx.observe_new(|workspace: &mut Workspace, window, cx| { let Some(window) = window else { From 1a36c51cf17735d99a7db228433b74c956ba350e Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Sat, 26 Jul 2025 17:26:53 +0200 Subject: [PATCH 28/42] Fix borrow issues --- crates/gpui/src/platform/mac/window.rs | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index e5a6a1b94e..470de57011 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -5,7 +5,8 @@ use crate::{ MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions, ScaledPixels, Size, Timer, WindowAppearance, WindowBackgroundAppearance, WindowBounds, - WindowControlArea, WindowKind, WindowParams, platform::PlatformInputHandler, point, px, size, + WindowControlArea, WindowKind, WindowParams, dispatch_get_main_queue, + dispatch_sys::dispatch_async_f, platform::PlatformInputHandler, point, px, size, }; use block::ConcreteBlock; use cocoa::{ @@ -963,15 +964,34 @@ impl PlatformWindow for MacWindow { fn merge_all_windows(&self) { let native_window = self.0.lock().native_window; - unsafe { + unsafe extern "C" fn merge_windows_async(context: *mut std::ffi::c_void) { + let native_window = context as id; let _: () = msg_send![native_window, mergeAllWindows:nil]; } + + unsafe { + dispatch_async_f( + dispatch_get_main_queue(), + native_window as *mut std::ffi::c_void, + Some(merge_windows_async), + ); + } } fn move_tab_to_new_window(&self) { let native_window = self.0.lock().native_window; - unsafe { + unsafe extern "C" fn move_tab_async(context: *mut std::ffi::c_void) { + let native_window = context as id; let _: () = msg_send![native_window, moveTabToNewWindow:nil]; + let _: () = msg_send![native_window, makeKeyAndOrderFront: nil]; + } + + unsafe { + dispatch_async_f( + dispatch_get_main_queue(), + native_window as *mut std::ffi::c_void, + Some(move_tab_async), + ); } } From 4e8159386f8184e9f53cad06c783c5ffd9746dce Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Sat, 26 Jul 2025 17:30:32 +0200 Subject: [PATCH 29/42] Request immediate frame when window becomes active --- crates/gpui/src/platform/mac/window.rs | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 470de57011..2363841672 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -1908,7 +1908,7 @@ extern "C" fn window_did_change_screen(this: &Object, _: Sel, _: id) { extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) { let window_state = unsafe { get_window_state(this) }; - let lock = window_state.lock(); + let mut lock = window_state.lock(); let is_active = unsafe { lock.native_window.isKeyWindow() == YES }; // When opening a pop-up while the application isn't active, Cocoa sends a spurious @@ -1929,6 +1929,29 @@ extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) let executor = lock.executor.clone(); drop(lock); + + // If window is becoming active, trigger immediate synchronous frame request. + if selector == sel!(windowDidBecomeKey:) && is_active { + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.lock(); + + if let Some(mut callback) = lock.request_frame_callback.take() { + #[cfg(not(feature = "macos-blade"))] + lock.renderer.set_presents_with_transaction(true); + lock.stop_display_link(); + drop(lock); + callback(RequestFrameOptions { + require_presentation: true, + }); + + let mut lock = window_state.lock(); + lock.request_frame_callback = Some(callback); + #[cfg(not(feature = "macos-blade"))] + lock.renderer.set_presents_with_transaction(false); + lock.start_display_link(); + } + } + executor .spawn(async move { let mut lock = window_state.as_ref().lock(); From 81403afc0ab43155590fa8f67647676498ae9897 Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Sat, 26 Jul 2025 18:23:55 +0200 Subject: [PATCH 30/42] Refactor tab rendering based on .tabbedWindows --- crates/gpui/src/platform.rs | 12 +- crates/gpui/src/platform/mac/window.rs | 118 ++------- crates/gpui/src/window.rs | 56 ++-- crates/rules_library/src/rules_library.rs | 2 +- crates/title_bar/src/platform_title_bar.rs | 4 +- crates/title_bar/src/system_window_tabs.rs | 293 ++++++--------------- crates/title_bar/src/title_bar.rs | 2 +- 7 files changed, 138 insertions(+), 349 deletions(-) diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index eb65f7e648..0983dc0e4a 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -40,8 +40,8 @@ use crate::{ DEFAULT_WINDOW_SIZE, DevicePixels, DispatchEventResult, Font, FontId, FontMetrics, FontRun, ForegroundExecutor, GlyphId, GpuSpecs, ImageSource, Keymap, LineLayout, Pixels, PlatformInput, Point, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, ScaledPixels, Scene, - ShapedGlyph, ShapedRun, SharedString, Size, SvgRenderer, SvgSize, Task, TaskLabel, Window, - WindowControlArea, hash, point, px, size, + ShapedGlyph, ShapedRun, SharedString, Size, SvgRenderer, SvgSize, SystemWindowTab, Task, + TaskLabel, Window, WindowControlArea, hash, point, px, size, }; use anyhow::Result; use async_task::Runnable; @@ -503,21 +503,17 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle { fn get_title(&self) -> String { String::new() } - fn get_tab_bar_visible(&self) -> bool { - false + fn tabbed_windows(&self) -> Option> { + None } fn set_edited(&mut self, _edited: bool) {} fn show_character_palette(&self) {} fn titlebar_double_click(&self) {} fn on_select_previous_tab(&self, _callback: Box) {} fn on_select_next_tab(&self, _callback: Box) {} - fn on_tab_group_changed(&self, _callback: Box) {} fn merge_all_windows(&self) {} fn move_tab_to_new_window(&self) {} fn toggle_window_tab_overview(&self) {} - fn tab_group(&self) -> Option { - None - } #[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 2363841672..e301e9c5ee 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -4,9 +4,10 @@ use crate::{ ForegroundExecutor, KeyDownEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions, - ScaledPixels, Size, Timer, WindowAppearance, WindowBackgroundAppearance, WindowBounds, - WindowControlArea, WindowKind, WindowParams, dispatch_get_main_queue, - dispatch_sys::dispatch_async_f, platform::PlatformInputHandler, point, px, size, + ScaledPixels, SharedString, Size, SystemWindowTab, Timer, WindowAppearance, + WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowKind, WindowParams, + dispatch_get_main_queue, dispatch_sys::dispatch_async_f, platform::PlatformInputHandler, point, + px, size, }; use block::ConcreteBlock; use cocoa::{ @@ -366,16 +367,6 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C select_previous_tab as extern "C" fn(&Object, Sel, id), ); - decl.add_method( - sel!(toggleTabBar:), - toggle_tab_bar as extern "C" fn(&Object, Sel, id), - ); - - decl.add_method( - sel!(observeValueForKeyPath:ofObject:change:context:), - observe_value_for_key_path as extern "C" fn(&Object, Sel, id, id, id, *mut c_void), - ); - decl.register() } } @@ -410,7 +401,6 @@ struct MacWindowState { fullscreen_restore_bounds: Bounds, select_next_tab_callback: Option>, select_previous_tab_callback: Option>, - tab_group_changed_callback: Option>, } impl MacWindowState { @@ -704,7 +694,6 @@ impl MacWindow { fullscreen_restore_bounds: Bounds::default(), select_next_tab_callback: None, select_previous_tab_callback: None, - tab_group_changed_callback: None, }))); (*native_window).set_ivar( @@ -792,7 +781,6 @@ impl MacWindow { } } - init_tab_group_observer(native_window); let app = NSApplication::sharedApplication(nil); let main_window: id = msg_send![app, mainWindow]; if allows_automatic_window_tabbing @@ -915,7 +903,6 @@ impl Drop for MacWindow { let window = this.native_window; this.display_link.take(); unsafe { - remove_tab_group_kvo_observer(window); this.native_window.setDelegate_(nil); } this.input_handler.take(); @@ -1374,23 +1361,27 @@ impl PlatformWindow for MacWindow { self.0.lock().appearance_changed_callback = Some(callback); } - fn tab_group(&self) -> Option { + fn tabbed_windows(&self) -> Option> { unsafe { - let tabgroup: id = msg_send![self.0.lock().native_window, tabGroup]; - let tabgroup_id = tabgroup as *const Object as usize; - Some(tabgroup_id) - } - } - - fn get_tab_bar_visible(&self) -> bool { - unsafe { - let tab_group: id = msg_send![self.0.lock().native_window, tabGroup]; - if tab_group.is_null() { - false - } else { - let tab_bar_visible: BOOL = msg_send![tab_group, isTabBarVisible]; - tab_bar_visible == YES + let windows: id = msg_send![self.0.lock().native_window, tabbedWindows]; + if windows.is_null() { + return None; } + + let count: NSUInteger = msg_send![windows, count]; + let mut result = Vec::new(); + for i in 0..count { + let window: id = msg_send![windows, objectAtIndex:i]; + if msg_send![window, isKindOfClass: WINDOW_CLASS] { + let handle = get_window_state(&*window).lock().handle; + let title: id = msg_send![window, title]; + let title = SharedString::from(title.to_str().to_string()); + + result.push(SystemWindowTab::new(title, handle)); + } + } + + Some(result) } } @@ -1402,10 +1393,6 @@ impl PlatformWindow for MacWindow { self.0.as_ref().lock().select_previous_tab_callback = Some(callback); } - fn on_tab_group_changed(&self, _callback: Box) { - self.0.as_ref().lock().tab_group_changed_callback = Some(_callback); - } - fn draw(&self, scene: &crate::Scene) { let mut this = self.0.lock(); this.renderer.draw(scene); @@ -1940,9 +1927,7 @@ extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) lock.renderer.set_presents_with_transaction(true); lock.stop_display_link(); drop(lock); - callback(RequestFrameOptions { - require_presentation: true, - }); + callback(Default::default()); let mut lock = window_state.lock(); lock.request_frame_callback = Some(callback); @@ -2527,58 +2512,3 @@ extern "C" fn select_previous_tab(this: &Object, _sel: Sel, _id: id) { window_state.lock().select_previous_tab_callback = Some(callback); } } - -extern "C" fn toggle_tab_bar(this: &Object, _sel: Sel, _id: id) { - unsafe { - let _: () = msg_send![super(this, class!(NSWindow)), toggleTabBar:nil]; - - let window_state = get_window_state(this); - window_state.as_ref().lock().move_traffic_light(); - } -} - -unsafe fn init_tab_group_observer(this: *mut Object) { - unsafe { - let _: () = msg_send![this, - addObserver:this - forKeyPath:ns_string("tabGroup") - options:1u64 // NSKeyValueObservingOptionNew - context:std::ptr::null_mut::()]; - } -} - -unsafe fn remove_tab_group_kvo_observer(this: *mut Object) { - unsafe { - let _: () = msg_send![this, - removeObserver:this - forKeyPath:ns_string("tabGroup") - context:std::ptr::null_mut::()]; - } -} - -extern "C" fn observe_value_for_key_path( - this: &Object, - _sel: Sel, - key_path: id, - _object: id, - change: id, - _context: *mut c_void, -) { - unsafe { - if key_path.isEqualToString("tabGroup") { - let tabgroup_id: id = msg_send![change, objectForKey: ns_string("new")]; - let window_state = get_window_state(this); - let queue: id = msg_send![class!(NSOperationQueue), mainQueue]; - let block = ConcreteBlock::new(move || { - let mut lock = window_state.as_ref().lock(); - if let Some(mut callback) = lock.tab_group_changed_callback.take() { - drop(lock); - callback(tabgroup_id as usize); - window_state.lock().tab_group_changed_callback = Some(callback); - } - }) - .copy(); - let _: () = msg_send![queue, addOperationWithBlock: &*block]; - } - } -} diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index a5a6da64d4..83df8dc9f6 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -12,11 +12,11 @@ use crate::{ PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, PromptButton, PromptLevel, Quad, Render, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Replay, ResizeEdge, SMOOTH_SVG_SCALE_FACTOR, SUBPIXEL_VARIANTS, ScaledPixels, Scene, Shadow, SharedString, Size, - StrikethroughStyle, Style, SubscriberSet, Subscription, SystemWindowTabController, TabHandles, - TaffyLayoutEngine, Task, TextStyle, TextStyleRefinement, TransformationMatrix, Underline, - UnderlineStyle, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControls, - WindowDecorations, WindowOptions, WindowParams, WindowTextSystem, point, prelude::*, px, rems, - size, transparent_black, + StrikethroughStyle, Style, SubscriberSet, Subscription, SystemWindowTab, + SystemWindowTabController, TabHandles, TaffyLayoutEngine, Task, TextStyle, TextStyleRefinement, + TransformationMatrix, Underline, UnderlineStyle, WindowAppearance, WindowBackgroundAppearance, + WindowBounds, WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem, + point, prelude::*, px, rems, size, transparent_black, }; use anyhow::{Context as _, Result, anyhow}; use collections::{FxHashMap, FxHashSet}; @@ -1135,11 +1135,11 @@ impl Window { let mut cx = cx.to_async(); Box::new(move || { handle - .update(&mut cx, |_, window, cx| { - let window_id = handle.window_id(); - if let Some(tab_group) = window.tab_group() { - SystemWindowTabController::select_next_tab(cx, tab_group, window_id); - } + .update(&mut cx, |_, _window, _cx| { + // let window_id = handle.window_id(); + // if let Some(tab_group) = window.tab_group() { + // SystemWindowTabController::select_next_tab(cx, tab_group, window_id); + // } }) .log_err(); }) @@ -1148,23 +1148,13 @@ impl Window { let mut cx = cx.to_async(); Box::new(move || { handle - .update(&mut cx, |_, window, cx| { - let window_id = handle.window_id(); - if let Some(tab_group) = window.tab_group() { - SystemWindowTabController::select_previous_tab( - cx, tab_group, window_id, - ); - } - }) - .log_err(); - }) - }); - platform_window.on_tab_group_changed({ - let mut cx = cx.to_async(); - Box::new(move |tab_group| { - handle - .update(&mut cx, |_, window, cx| { - SystemWindowTabController::insert_window(cx, window, tab_group); + .update(&mut cx, |_, _window, _cx| { + // let window_id = handle.window_id(); + // if let Some(tab_group) = window.tab_group() { + // SystemWindowTabController::select_previous_tab( + // cx, tab_group, window_id, + // ); + // } }) .log_err(); }) @@ -4332,16 +4322,10 @@ impl Window { self.platform_window.get_title() } - /// Gets the visibility of the tab bar at the platform level. + /// Returns a list of all tabbed windows and their titles. /// This is macOS specific. - pub fn tab_bar_visible(&self) -> bool { - self.platform_window.get_tab_bar_visible() - } - - /// Returns the tab group pointer of the window. - /// This is macOS specific. - pub fn tab_group(&self) -> Option { - self.platform_window.tab_group() + pub fn tabbed_windows(&self) -> Option> { + self.platform_window.tabbed_windows() } /// Merges all open windows into a single tabbed window. diff --git a/crates/rules_library/src/rules_library.rs b/crates/rules_library/src/rules_library.rs index e1c67784e4..84feed59bc 100644 --- a/crates/rules_library/src/rules_library.rs +++ b/crates/rules_library/src/rules_library.rs @@ -414,7 +414,7 @@ impl RulesLibrary { }); Self { title_bar: if !cfg!(target_os = "macos") { - Some(cx.new(|cx| PlatformTitleBar::new("rules-library-title-bar", window, cx))) + Some(cx.new(|cx| PlatformTitleBar::new("rules-library-title-bar", cx))) } else { None }, diff --git a/crates/title_bar/src/platform_title_bar.rs b/crates/title_bar/src/platform_title_bar.rs index 409cb41c27..bc1057a4d4 100644 --- a/crates/title_bar/src/platform_title_bar.rs +++ b/crates/title_bar/src/platform_title_bar.rs @@ -20,9 +20,9 @@ pub struct PlatformTitleBar { } impl PlatformTitleBar { - pub fn new(id: impl Into, window: &mut Window, cx: &mut Context) -> Self { + pub fn new(id: impl Into, cx: &mut Context) -> Self { let platform_style = PlatformStyle::platform(); - let system_window_tabs = cx.new(|cx| SystemWindowTabs::new(window, cx)); + let system_window_tabs = cx.new(|_cx| SystemWindowTabs::new()); Self { id: id.into(), diff --git a/crates/title_bar/src/system_window_tabs.rs b/crates/title_bar/src/system_window_tabs.rs index 4c92760af0..4661a052e8 100644 --- a/crates/title_bar/src/system_window_tabs.rs +++ b/crates/title_bar/src/system_window_tabs.rs @@ -1,11 +1,10 @@ -use command_palette_hooks::CommandPaletteFilter; use settings::Settings; -use std::any::TypeId; use gpui::{ - Context, Hsla, InteractiveElement, ParentElement, ScrollHandle, Styled, Subscription, - SystemWindowTab, SystemWindowTabController, Window, WindowId, actions, canvas, div, + Context, Hsla, InteractiveElement, ParentElement, ScrollHandle, Styled, SystemWindowTab, + SystemWindowTabController, Window, WindowId, actions, canvas, div, }; + use ui::{ Color, ContextMenu, DynamicSpacing, IconButton, IconButtonShape, IconName, IconSize, Label, LabelSize, Tab, h_flex, prelude::*, right_click_menu, @@ -36,94 +35,32 @@ pub struct DraggedWindowTab { } pub struct SystemWindowTabs { - tabs: Vec, tab_bar_scroll_handle: ScrollHandle, measured_tab_width: Pixels, - _subscriptions: Vec, } impl SystemWindowTabs { - pub fn new(window: &mut Window, cx: &mut Context) -> Self { - let window_id = window.window_handle().window_id(); - let mut subscriptions = Vec::new(); - - subscriptions.push( - cx.observe_global::(move |this, cx| { - let controller = cx.global::(); - let tab_group = controller.tabs().iter().find_map(|(group, windows)| { - windows - .iter() - .find(|tab| tab.id == window_id) - .map(|_| *group) - }); - - if let Some(tab_group) = tab_group { - let all_tab_groups = controller.tabs(); - let tabs = controller.windows(tab_group); - let show_merge_all_windows = all_tab_groups.len() > 1; - let show_other_tab_actions = if let Some(tabs) = tabs { - this.tabs = tabs.clone(); - tabs.len() > 1 - } else { - false - }; - - let merge_all_windows_action = TypeId::of::(); - let other_tab_actions = vec![ - TypeId::of::(), - TypeId::of::(), - TypeId::of::(), - ]; - - if show_merge_all_windows && show_other_tab_actions { - CommandPaletteFilter::update_global(cx, |filter, _cx| { - let mut all_actions = vec![merge_all_windows_action]; - all_actions.extend(other_tab_actions.iter().cloned()); - filter.show_action_types(all_actions.iter()); - }); - } else if show_merge_all_windows { - CommandPaletteFilter::update_global(cx, |filter, _cx| { - filter.show_action_types(std::iter::once(&merge_all_windows_action)); - filter.hide_action_types(&other_tab_actions); - }); - } else if show_other_tab_actions { - CommandPaletteFilter::update_global(cx, |filter, _cx| { - filter.show_action_types(other_tab_actions.iter()); - filter.hide_action_types(&[merge_all_windows_action]); - }); - } else { - CommandPaletteFilter::update_global(cx, |filter, _cx| { - let mut all_actions = vec![merge_all_windows_action]; - all_actions.extend(other_tab_actions.iter().cloned()); - filter.hide_action_types(&all_actions); - }); - } - } - }), - ); - + pub fn new() -> Self { Self { - tabs: Vec::new(), tab_bar_scroll_handle: ScrollHandle::new(), - measured_tab_width: window.bounds().size.width, - _subscriptions: subscriptions, + measured_tab_width: px(0.), } } pub fn init(cx: &mut App) { cx.observe_new(|workspace: &mut Workspace, _, _| { workspace - .register_action(|_, _: &ShowNextWindowTab, window, cx| { - let window_id = window.window_handle().window_id(); - if let Some(tab_group) = window.tab_group() { - SystemWindowTabController::select_next_tab(cx, tab_group, window_id); - } + .register_action(|_, _: &ShowNextWindowTab, _window, _cx| { + // let window_id = window.window_handle().window_id(); + // if let Some(tab_group) = window.tab_group() { + // SystemWindowTabController::select_next_tab(cx, tab_group, window_id); + // } }) - .register_action(|_, _: &ShowPreviousWindowTab, window, cx| { - let window_id = window.window_handle().window_id(); - if let Some(tab_group) = window.tab_group() { - SystemWindowTabController::select_previous_tab(cx, tab_group, window_id); - } + .register_action(|_, _: &ShowPreviousWindowTab, _window, _cx| { + // let window_id = window.window_handle().window_id(); + // if let Some(tab_group) = window.tab_group() { + // SystemWindowTabController::select_previous_tab(cx, tab_group, window_id); + // } }) .register_action(|_, _: &MergeAllWindows, window, _cx| { window.merge_all_windows(); @@ -140,7 +77,7 @@ impl SystemWindowTabs { ix: usize, item: SystemWindowTab, active_background_color: Hsla, - inactive_background_color: Hsla, + _inactive_background_color: Hsla, window: &mut Window, cx: &mut Context, ) -> impl IntoElement + use<> { @@ -149,7 +86,6 @@ impl SystemWindowTabs { let show_close_button = &settings.show_close_button; let rem_size = window.rem_size(); - let width = self.measured_tab_width.max(rem_size * 10); let is_active = window.window_handle().window_id() == item.id; let title = item.title.to_string(); @@ -163,153 +99,95 @@ impl SystemWindowTabs { }); let tab = h_flex() - .h_full() - .w(width) - .border_t_1() - .border_color(if is_active { - active_background_color - } else { - cx.theme().colors().border + .id(ix) + .group("tab") + .w_full() + .overflow_hidden() + .h(Tab::content_height(cx)) + .relative() + .px(DynamicSpacing::Base16.px(cx)) + .justify_center() + .border_l_1() + .border_color(cx.theme().colors().border) + .cursor_pointer() + .on_click(move |_, _, cx| { + let _ = item.handle.update(cx, |_, window, _| { + window.activate_window(); + }); }) - .child( - h_flex() - .id(ix) - .group("tab") - .w_full() - .h(Tab::content_height(cx)) - .relative() - .px(DynamicSpacing::Base16.px(cx)) - .justify_center() - .border_l_1() - .border_color(cx.theme().colors().border) - .when(is_active, |this| this.bg(active_background_color)) - .cursor_pointer() - .on_drag( - DraggedWindowTab { - id: item.id, - title: item.title.to_string(), - width, - is_active, - active_background_color, - inactive_background_color, - }, - |tab, _, _, cx| cx.new(|_| tab.clone()), - ) - .drag_over::(|element, _, _, cx| { - element.bg(cx.theme().colors().drop_target_background) - }) - .on_drop(cx.listener( - move |_this, dragged_tab: &DraggedWindowTab, _window, cx| { - Self::handle_tab_drop(dragged_tab, ix, cx); - }, - )) - .on_click(move |_, _, cx| { - let _ = item.handle.update(cx, |_, window, _| { - window.activate_window(); - }); - }) - .child(label) - .map(|this| match show_close_button { - ShowCloseButton::Hidden => this, - _ => this.child( - div() - .absolute() - .top_2() - .w_4() - .h_4() - .map(|this| match close_side { - ClosePosition::Left => this.left_1(), - ClosePosition::Right => this.right_1(), + .child(label) + .map(|this| match show_close_button { + ShowCloseButton::Hidden => this, + _ => this.child( + div() + .absolute() + .top_2() + .w_4() + .h_4() + .map(|this| match close_side { + ClosePosition::Left => this.left_1(), + ClosePosition::Right => this.right_1(), + }) + .child( + IconButton::new("close", IconName::Close) + .shape(IconButtonShape::Square) + .icon_color(Color::Muted) + .icon_size(IconSize::XSmall) + .on_click(|_, window, cx| { + window.dispatch_action(Box::new(CloseWindow), cx); }) - .child( - IconButton::new("close", IconName::Close) - .shape(IconButtonShape::Square) - .icon_color(Color::Muted) - .icon_size(IconSize::XSmall) - .on_click(|_, window, cx| { - window.dispatch_action(Box::new(CloseWindow), cx); - }) - .map(|this| match show_close_button { - ShowCloseButton::Hover => this.visible_on_hover("tab"), - _ => this, - }), - ), + .map(|this| match show_close_button { + ShowCloseButton::Hover => this.visible_on_hover("tab"), + _ => this, + }), ), - }), - ) + ), + }) .into_any(); - let tabs = self.tabs.clone(); let menu = right_click_menu(ix) .trigger(|_, _, _| tab) .menu(move |window, cx| { let focus_handle = cx.focus_handle(); - let tabs = tabs.clone(); - let other_tabs = tabs.clone(); - let move_tabs = tabs.clone(); - let merge_tabs = tabs.clone(); - ContextMenu::build(window, cx, move |mut menu, _window_, _cx| { - menu = menu.entry("Close Tab", None, move |window, cx| { - Self::handle_right_click_action( - cx, - window, - &tabs, - |tab| tab.id == item.id, - |window, cx| { - window.dispatch_action(Box::new(CloseWindow), cx); - }, - ); + menu = menu.entry("Close Tab", None, move |_window, _cx| { + dbg!("Close Tab"); }); - menu = menu.entry("Close Other Tabs", None, move |window, cx| { - Self::handle_right_click_action( - cx, - window, - &other_tabs, - |tab| tab.id != item.id, - |window, cx| { - window.dispatch_action(Box::new(CloseWindow), cx); - }, - ); + menu = menu.entry("Close Other Tabs", None, move |_window, _cx| { + dbg!("Close Other Tabs"); }); - menu = menu.entry("Move Tab to New Window", None, move |window, cx| { - Self::handle_right_click_action( - cx, - window, - &move_tabs, - |tab| tab.id == item.id, - |window, _cx| { - window.move_tab_to_new_window(); - }, - ); + menu = menu.entry("Move Tab to New Window", None, move |_window, _cx| { + dbg!("Move Tab to New Window"); }); - menu = menu.entry("Show All Tabs", None, move |window, cx| { - Self::handle_right_click_action( - cx, - window, - &merge_tabs, - |tab| tab.id == item.id, - |window, _cx| { - window.toggle_window_tab_overview(); - }, - ); + menu = menu.entry("Show All Tabs", None, move |_window, _cx| { + dbg!("Show All Tabs"); }); menu.context(focus_handle.clone()) }) }); - div().child(menu).size_full() + div() + .flex_1() + .min_w(rem_size * 10) + .when(is_active, |this| this.bg(active_background_color)) + .border_t_1() + .border_color(if is_active { + active_background_color + } else { + cx.theme().colors().border + }) + .child(menu) } - fn handle_tab_drop(dragged_tab: &DraggedWindowTab, ix: usize, cx: &mut Context) { + fn _handle_tab_drop(dragged_tab: &DraggedWindowTab, ix: usize, cx: &mut Context) { SystemWindowTabController::update_window_position(cx, dragged_tab.id, ix); } - fn handle_right_click_action( + fn _handle_right_click_action( cx: &mut App, window: &mut Window, tabs: &Vec, @@ -337,11 +215,13 @@ impl Render for SystemWindowTabs { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let active_background_color = cx.theme().colors().title_bar_background; let inactive_background_color = cx.theme().colors().tab_bar_background; - let entity = cx.entity(); - let number_of_tabs = self.tabs.len(); - let tab_items = self - .tabs + + let windows = window.tabbed_windows().unwrap_or(vec![SystemWindowTab::new( + SharedString::from(window.window_title()), + window.window_handle(), + )]); + let tab_items = windows .iter() .enumerate() .map(|(ix, item)| { @@ -356,7 +236,8 @@ impl Render for SystemWindowTabs { }) .collect::>(); - if !window.tab_bar_visible() { + let number_of_tabs = tab_items.len().max(1); + if number_of_tabs <= 1 { return h_flex().into_any_element(); } @@ -368,11 +249,9 @@ impl Render for SystemWindowTabs { h_flex() .id("window tabs") .w_full() - .h_full() .h(Tab::container_height(cx)) .bg(inactive_background_color) .overflow_x_scroll() - .w_full() .track_scroll(&self.tab_bar_scroll_handle) .children(tab_items) .child( diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 9094ad6601..3f22212593 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -287,7 +287,7 @@ impl TitleBar { ) }); - let platform_titlebar = cx.new(|cx| PlatformTitleBar::new(id, window, cx)); + let platform_titlebar = cx.new(|cx| PlatformTitleBar::new(id, cx)); Self { platform_titlebar, From c807da7f0ddc806997d1ba9e7929c7dd95610daa Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Sat, 26 Jul 2025 20:57:13 +0200 Subject: [PATCH 31/42] Bring back tab reordering --- crates/gpui/src/app.rs | 161 +++++++++++++++----- crates/gpui/src/platform.rs | 2 + crates/gpui/src/platform/mac/window.rs | 50 ++++++ crates/gpui/src/window.rs | 42 ++++-- crates/rules_library/src/rules_library.rs | 2 +- crates/title_bar/src/platform_title_bar.rs | 4 +- crates/title_bar/src/system_window_tabs.rs | 168 +++++++++++++++++---- crates/title_bar/src/title_bar.rs | 2 +- 8 files changed, 347 insertions(+), 84 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index e732de15eb..851b389e5d 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -17,6 +17,7 @@ use futures::{ channel::oneshot, future::{LocalBoxFuture, Shared}, }; +use itertools::Itertools; use parking_lot::RwLock; use slotmap::SlotMap; @@ -287,38 +288,6 @@ impl SystemWindowTabController { self.tabs.get(&tab_group) } - /// Insert a window into a tab group. - pub fn insert_window(cx: &mut App, window: &Window, tab_group: usize) { - let mut controller = cx.global_mut::(); - - let title = SharedString::from(window.window_title()); - let handle = window.window_handle(); - - for windows in controller.tabs.values_mut() { - if let Some(pos) = windows.iter().position(|tab| tab.id == handle.id) { - windows.remove(pos); - } - } - - controller.tabs.retain(|_, windows| !windows.is_empty()); - - let windows = controller.tabs.entry(tab_group).or_insert_with(Vec::new); - if !windows.iter().any(|tab| tab.id == handle.id) { - windows.push(SystemWindowTab::new(title, handle)); - } - } - - /// Remove a window from a tab group. - pub fn remove_window(cx: &mut App, id: WindowId) { - let mut controller = cx.global_mut::(); - controller.tabs.retain(|_, windows| { - if let Some(pos) = windows.iter().position(|tab| tab.id == id) { - windows.remove(pos); - } - !windows.is_empty() - }); - } - /// Move window to a new position within the same tab group. pub fn update_window_position(cx: &mut App, id: WindowId, ix: usize) { let mut controller = cx.global_mut::(); @@ -356,9 +325,119 @@ impl SystemWindowTabController { } } - /// Selects the next tab in the tab group in the trailing direction. - pub fn select_next_tab(cx: &mut App, tab_group: usize, id: WindowId) { + /// Insert a window into a tab group. + pub fn open_window(cx: &mut App, id: WindowId, windows: Vec) { let mut controller = cx.global_mut::(); + let mut expected_window_ids: Vec<_> = windows + .iter() + .filter(|tab| tab.id != id) + .map(|tab| tab.id) + .sorted() + .collect(); + + let tab_group = { + controller.tabs.iter().find_map(|(key, group)| { + let mut group_ids: Vec<_> = group.iter().map(|tab| tab.id).sorted().collect(); + if group_ids == expected_window_ids { + Some(*key) + } else { + None + } + }) + }; + + if let Some(tab_group) = tab_group { + if let Some(new_window) = windows.iter().find(|tab| tab.id == id) { + if let Some(group) = controller.tabs.get_mut(&tab_group) { + group.push(new_window.clone()); + } + } + } else { + let new_group_id = controller.tabs.len(); + controller.tabs.insert(new_group_id, windows); + } + } + + /// Remove a window from a tab group. + pub fn remove_window(cx: &mut App, id: WindowId) { + let mut controller = cx.global_mut::(); + controller.tabs.retain(|_, windows| { + if let Some(pos) = windows.iter().position(|tab| tab.id == id) { + windows.remove(pos); + } + !windows.is_empty() + }); + } + + /// Move a window to a different tab group. + pub fn move_tab_to_new_window(cx: &mut App, id: WindowId) { + let mut controller = cx.global_mut::(); + // Find and remove the window from its current group + let mut removed_tab: Option = None; + let mut empty_group_key: Option = None; + + for (group_id, windows) in controller.tabs.iter_mut() { + if let Some(pos) = windows.iter().position(|tab| tab.id == id) { + removed_tab = Some(windows.remove(pos)); + if windows.is_empty() { + empty_group_key = Some(*group_id); + } + break; + } + } + + // Remove the group if it became empty + if let Some(group_id) = empty_group_key { + controller.tabs.remove(&group_id); + } + + // Insert the removed tab into a new group if it was found + if let Some(tab) = removed_tab { + let new_group_id = controller.tabs.keys().max().map_or(0, |k| k + 1); + controller.tabs.insert(new_group_id, vec![tab]); + } + } + + /// Merge all tab groups into a single group. + pub fn merge_all_windows(cx: &mut App, id: WindowId) { + let mut controller = cx.global_mut::(); + let mut merged_windows = Vec::new(); + let mut first_group_windows = None; + + // Drain all tab groups, but keep track of the group containing the given id + for (_, mut windows) in controller.tabs.drain() { + if windows.iter().any(|tab| tab.id == id) { + first_group_windows = Some(windows); + } else { + merged_windows.extend(windows); + } + } + + // Place the group with the given id first, then all others + let mut final_windows = Vec::new(); + if let Some(mut first) = first_group_windows { + final_windows.append(&mut first); + } + final_windows.append(&mut merged_windows); + + controller.tabs.insert(0, final_windows); + } + + /// Selects the next tab in the tab group in the trailing direction. + pub fn select_next_tab(cx: &mut App, id: WindowId) { + let mut controller = cx.global_mut::(); + let tab_group = controller + .tabs + .iter() + .find_map(|(group_id, windows)| { + if windows.iter().any(|tab| tab.id == id) { + Some(*group_id) + } else { + None + } + }) + .expect("WindowId not found in any tab group"); + let windows = controller.tabs.get_mut(&tab_group).unwrap(); let current_index = windows.iter().position(|tab| tab.id == id).unwrap(); let next_index = (current_index + 1) % windows.len(); @@ -369,8 +448,20 @@ impl SystemWindowTabController { } /// Selects the previous tab in the tab group in the leading direction. - pub fn select_previous_tab(cx: &mut App, tab_group: usize, id: WindowId) { + pub fn select_previous_tab(cx: &mut App, id: WindowId) { let mut controller = cx.global_mut::(); + let tab_group = controller + .tabs + .iter() + .find_map(|(group_id, windows)| { + if windows.iter().any(|tab| tab.id == id) { + Some(*group_id) + } else { + None + } + }) + .expect("WindowId not found in any tab group"); + let windows = controller.tabs.get_mut(&tab_group).unwrap(); let current_index = windows.iter().position(|tab| tab.id == id).unwrap(); let previous_index = if current_index == 0 { diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 0983dc0e4a..764250c3d9 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -509,6 +509,8 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle { fn set_edited(&mut self, _edited: bool) {} fn show_character_palette(&self) {} fn titlebar_double_click(&self) {} + fn on_move_tab_to_new_window(&self, _callback: Box) {} + fn on_merge_all_windows(&self, _callback: Box) {} fn on_select_previous_tab(&self, _callback: Box) {} fn on_select_next_tab(&self, _callback: Box) {} fn merge_all_windows(&self) {} diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index e301e9c5ee..8ac88038f6 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -357,6 +357,16 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C add_titlebar_accessory_view_controller as extern "C" fn(&Object, Sel, id), ); + decl.add_method( + sel!(moveTabToNewWindow:), + move_tab_to_new_window as extern "C" fn(&Object, Sel, id), + ); + + decl.add_method( + sel!(mergeAllWindows:), + merge_all_windows as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( sel!(selectNextTab:), select_next_tab as extern "C" fn(&Object, Sel, id), @@ -399,6 +409,8 @@ struct MacWindowState { // Whether the next left-mouse click is also the focusing click. first_mouse: bool, fullscreen_restore_bounds: Bounds, + move_tab_to_new_window_callback: Option>, + merge_all_windows_callback: Option>, select_next_tab_callback: Option>, select_previous_tab_callback: Option>, } @@ -692,6 +704,8 @@ impl MacWindow { external_files_dragged: false, first_mouse: false, fullscreen_restore_bounds: Bounds::default(), + move_tab_to_new_window_callback: None, + merge_all_windows_callback: None, select_next_tab_callback: None, select_previous_tab_callback: None, }))); @@ -1385,6 +1399,14 @@ impl PlatformWindow for MacWindow { } } + fn on_move_tab_to_new_window(&self, callback: Box) { + self.0.as_ref().lock().move_tab_to_new_window_callback = Some(callback); + } + + fn on_merge_all_windows(&self, callback: Box) { + self.0.as_ref().lock().merge_all_windows_callback = Some(callback); + } + fn on_select_next_tab(&self, callback: Box) { self.0.as_ref().lock().select_next_tab_callback = Some(callback); } @@ -2493,6 +2515,34 @@ extern "C" fn add_titlebar_accessory_view_controller(this: &Object, _: Sel, view } } +extern "C" fn move_tab_to_new_window(this: &Object, _: Sel, _: id) { + unsafe { + let _: () = msg_send![super(this, class!(NSWindow)), moveTabToNewWindow:nil]; + + let window_state = get_window_state(this); + let mut lock = window_state.as_ref().lock(); + if let Some(mut callback) = lock.move_tab_to_new_window_callback.take() { + drop(lock); + callback(); + window_state.lock().move_tab_to_new_window_callback = Some(callback); + } + } +} + +extern "C" fn merge_all_windows(this: &Object, _: Sel, _: id) { + unsafe { + let _: () = msg_send![super(this, class!(NSWindow)), mergeAllWindows:nil]; + + let window_state = get_window_state(this); + let mut lock = window_state.as_ref().lock(); + if let Some(mut callback) = lock.merge_all_windows_callback.take() { + drop(lock); + callback(); + window_state.lock().merge_all_windows_callback = Some(callback); + } + } +} + extern "C" fn select_next_tab(this: &Object, _sel: Sel, _id: id) { let window_state = unsafe { get_window_state(this) }; let mut lock = window_state.as_ref().lock(); diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 83df8dc9f6..5de33ad836 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -964,6 +964,12 @@ impl Window { tabbing_identifier, }, )?; + + let windows = platform_window.tabbed_windows(); + if let Some(windows) = windows { + SystemWindowTabController::open_window(cx, handle.window_id(), windows); + } + let display_id = platform_window.display().map(|display| display.id()); let sprite_atlas = platform_window.sprite_atlas(); let mouse_position = platform_window.mouse_position(); @@ -1131,15 +1137,32 @@ impl Window { .unwrap_or(None) }) }); + platform_window.on_move_tab_to_new_window({ + let mut cx = cx.to_async(); + Box::new(move || { + handle + .update(&mut cx, |_, _window, cx| { + SystemWindowTabController::move_tab_to_new_window(cx, handle.window_id()); + }) + .log_err(); + }) + }); + platform_window.on_merge_all_windows({ + let mut cx = cx.to_async(); + Box::new(move || { + handle + .update(&mut cx, |_, _window, cx| { + SystemWindowTabController::merge_all_windows(cx, handle.window_id()); + }) + .log_err(); + }) + }); platform_window.on_select_next_tab({ let mut cx = cx.to_async(); Box::new(move || { handle - .update(&mut cx, |_, _window, _cx| { - // let window_id = handle.window_id(); - // if let Some(tab_group) = window.tab_group() { - // SystemWindowTabController::select_next_tab(cx, tab_group, window_id); - // } + .update(&mut cx, |_, _window, cx| { + SystemWindowTabController::select_next_tab(cx, handle.window_id()); }) .log_err(); }) @@ -1148,13 +1171,8 @@ impl Window { let mut cx = cx.to_async(); Box::new(move || { handle - .update(&mut cx, |_, _window, _cx| { - // let window_id = handle.window_id(); - // if let Some(tab_group) = window.tab_group() { - // SystemWindowTabController::select_previous_tab( - // cx, tab_group, window_id, - // ); - // } + .update(&mut cx, |_, _window, cx| { + SystemWindowTabController::select_previous_tab(cx, handle.window_id()) }) .log_err(); }) diff --git a/crates/rules_library/src/rules_library.rs b/crates/rules_library/src/rules_library.rs index 84feed59bc..e1c67784e4 100644 --- a/crates/rules_library/src/rules_library.rs +++ b/crates/rules_library/src/rules_library.rs @@ -414,7 +414,7 @@ impl RulesLibrary { }); Self { title_bar: if !cfg!(target_os = "macos") { - Some(cx.new(|cx| PlatformTitleBar::new("rules-library-title-bar", cx))) + Some(cx.new(|cx| PlatformTitleBar::new("rules-library-title-bar", window, cx))) } else { None }, diff --git a/crates/title_bar/src/platform_title_bar.rs b/crates/title_bar/src/platform_title_bar.rs index bc1057a4d4..409cb41c27 100644 --- a/crates/title_bar/src/platform_title_bar.rs +++ b/crates/title_bar/src/platform_title_bar.rs @@ -20,9 +20,9 @@ pub struct PlatformTitleBar { } impl PlatformTitleBar { - pub fn new(id: impl Into, cx: &mut Context) -> Self { + pub fn new(id: impl Into, window: &mut Window, cx: &mut Context) -> Self { let platform_style = PlatformStyle::platform(); - let system_window_tabs = cx.new(|_cx| SystemWindowTabs::new()); + let system_window_tabs = cx.new(|cx| SystemWindowTabs::new(window, cx)); Self { id: id.into(), diff --git a/crates/title_bar/src/system_window_tabs.rs b/crates/title_bar/src/system_window_tabs.rs index 4661a052e8..119a621254 100644 --- a/crates/title_bar/src/system_window_tabs.rs +++ b/crates/title_bar/src/system_window_tabs.rs @@ -1,8 +1,8 @@ use settings::Settings; use gpui::{ - Context, Hsla, InteractiveElement, ParentElement, ScrollHandle, Styled, SystemWindowTab, - SystemWindowTabController, Window, WindowId, actions, canvas, div, + Context, Hsla, InteractiveElement, ParentElement, ScrollHandle, Styled, Subscription, + SystemWindowTab, SystemWindowTabController, Window, WindowId, actions, canvas, div, }; use ui::{ @@ -35,37 +35,70 @@ pub struct DraggedWindowTab { } pub struct SystemWindowTabs { + tabs: Vec, tab_bar_scroll_handle: ScrollHandle, measured_tab_width: Pixels, + _subscriptions: Vec, } impl SystemWindowTabs { - pub fn new() -> Self { + pub fn new(window: &mut Window, cx: &mut Context) -> Self { + let window_id = window.window_handle().window_id(); + let mut subscriptions = Vec::new(); + + subscriptions.push( + cx.observe_global::(move |this, cx| { + let controller = cx.global::(); + let tab_group = controller.tabs().iter().find_map(|(group, windows)| { + windows + .iter() + .find(|tab| tab.id == window_id) + .map(|_| *group) + }); + + if let Some(tab_group) = tab_group { + if let Some(windows) = controller.windows(tab_group) { + this.tabs = windows.clone(); + } + } + }), + ); + Self { + tabs: Vec::new(), tab_bar_scroll_handle: ScrollHandle::new(), measured_tab_width: px(0.), + _subscriptions: subscriptions, } } pub fn init(cx: &mut App) { cx.observe_new(|workspace: &mut Workspace, _, _| { workspace - .register_action(|_, _: &ShowNextWindowTab, _window, _cx| { - // let window_id = window.window_handle().window_id(); - // if let Some(tab_group) = window.tab_group() { - // SystemWindowTabController::select_next_tab(cx, tab_group, window_id); - // } + .register_action(|_, _: &ShowNextWindowTab, window, cx| { + SystemWindowTabController::select_next_tab( + cx, + window.window_handle().window_id(), + ); }) - .register_action(|_, _: &ShowPreviousWindowTab, _window, _cx| { - // let window_id = window.window_handle().window_id(); - // if let Some(tab_group) = window.tab_group() { - // SystemWindowTabController::select_previous_tab(cx, tab_group, window_id); - // } + .register_action(|_, _: &ShowPreviousWindowTab, window, cx| { + SystemWindowTabController::select_previous_tab( + cx, + window.window_handle().window_id(), + ); }) - .register_action(|_, _: &MergeAllWindows, window, _cx| { + .register_action(|_, _: &MergeAllWindows, window, cx| { + SystemWindowTabController::merge_all_windows( + cx, + window.window_handle().window_id(), + ); window.merge_all_windows(); }) - .register_action(|_, _: &MoveTabToNewWindow, window, _cx| { + .register_action(|_, _: &MoveTabToNewWindow, window, cx| { + SystemWindowTabController::move_tab_to_new_window( + cx, + window.window_handle().window_id(), + ); window.move_tab_to_new_window(); }); }) @@ -77,7 +110,7 @@ impl SystemWindowTabs { ix: usize, item: SystemWindowTab, active_background_color: Hsla, - _inactive_background_color: Hsla, + inactive_background_color: Hsla, window: &mut Window, cx: &mut Context, ) -> impl IntoElement + use<> { @@ -86,6 +119,7 @@ impl SystemWindowTabs { let show_close_button = &settings.show_close_button; let rem_size = window.rem_size(); + let width = self.measured_tab_width.max(rem_size * 10); let is_active = window.window_handle().window_id() == item.id; let title = item.title.to_string(); @@ -110,6 +144,25 @@ impl SystemWindowTabs { .border_l_1() .border_color(cx.theme().colors().border) .cursor_pointer() + .on_drag( + DraggedWindowTab { + id: item.id, + title: item.title.to_string(), + width, + is_active, + active_background_color, + inactive_background_color, + }, + |tab, _, _, cx| cx.new(|_| tab.clone()), + ) + .drag_over::(|element, _, _, cx| { + element.bg(cx.theme().colors().drop_target_background) + }) + .on_drop( + cx.listener(move |_this, dragged_tab: &DraggedWindowTab, _window, cx| { + Self::handle_tab_drop(dragged_tab, ix, cx); + }), + ) .on_click(move |_, _, cx| { let _ = item.handle.update(cx, |_, window, _| { window.activate_window(); @@ -133,8 +186,18 @@ impl SystemWindowTabs { .shape(IconButtonShape::Square) .icon_color(Color::Muted) .icon_size(IconSize::XSmall) - .on_click(|_, window, cx| { - window.dispatch_action(Box::new(CloseWindow), cx); + .on_click({ + let handle = item.handle.clone(); + move |_, window, cx| { + if handle.window_id() == window.window_handle().window_id() + { + window.dispatch_action(Box::new(CloseWindow), cx); + } else { + let _ = handle.update(cx, |_, window, cx| { + window.dispatch_action(Box::new(CloseWindow), cx); + }); + } + } }) .map(|this| match show_close_button { ShowCloseButton::Hover => this.visible_on_hover("tab"), @@ -145,25 +208,67 @@ impl SystemWindowTabs { }) .into_any(); + let tabs = self.tabs.clone(); let menu = right_click_menu(ix) .trigger(|_, _, _| tab) .menu(move |window, cx| { let focus_handle = cx.focus_handle(); + let tabs = tabs.clone(); + let other_tabs = tabs.clone(); + let move_tabs = tabs.clone(); + let merge_tabs = tabs.clone(); + ContextMenu::build(window, cx, move |mut menu, _window_, _cx| { - menu = menu.entry("Close Tab", None, move |_window, _cx| { - dbg!("Close Tab"); + menu = menu.entry("Close Tab", None, move |window, cx| { + Self::handle_right_click_action( + cx, + window, + &tabs, + |tab| tab.id == item.id, + |window, cx| { + window.dispatch_action(Box::new(CloseWindow), cx); + }, + ); }); - menu = menu.entry("Close Other Tabs", None, move |_window, _cx| { - dbg!("Close Other Tabs"); + menu = menu.entry("Close Other Tabs", None, move |window, cx| { + Self::handle_right_click_action( + cx, + window, + &other_tabs, + |tab| tab.id != item.id, + |window, cx| { + window.dispatch_action(Box::new(CloseWindow), cx); + }, + ); }); - menu = menu.entry("Move Tab to New Window", None, move |_window, _cx| { - dbg!("Move Tab to New Window"); + menu = menu.entry("Move Tab to New Window", None, move |window, cx| { + Self::handle_right_click_action( + cx, + window, + &move_tabs, + |tab| tab.id == item.id, + |window, cx| { + SystemWindowTabController::move_tab_to_new_window( + cx, + window.window_handle().window_id(), + ); + window.move_tab_to_new_window(); + }, + ); }); - menu = menu.entry("Show All Tabs", None, move |_window, _cx| { - dbg!("Show All Tabs"); + menu = menu.entry("Show All Tabs", None, move |window, cx| { + Self::handle_right_click_action( + cx, + window, + &merge_tabs, + |tab| tab.id == item.id, + |window, _cx| { + window.toggle_window_tab_overview(); + }, + ); }); menu.context(focus_handle.clone()) @@ -183,11 +288,11 @@ impl SystemWindowTabs { .child(menu) } - fn _handle_tab_drop(dragged_tab: &DraggedWindowTab, ix: usize, cx: &mut Context) { + fn handle_tab_drop(dragged_tab: &DraggedWindowTab, ix: usize, cx: &mut Context) { SystemWindowTabController::update_window_position(cx, dragged_tab.id, ix); } - fn _handle_right_click_action( + fn handle_right_click_action( cx: &mut App, window: &mut Window, tabs: &Vec, @@ -217,11 +322,8 @@ impl Render for SystemWindowTabs { let inactive_background_color = cx.theme().colors().tab_bar_background; let entity = cx.entity(); - let windows = window.tabbed_windows().unwrap_or(vec![SystemWindowTab::new( - SharedString::from(window.window_title()), - window.window_handle(), - )]); - let tab_items = windows + let tab_items = self + .tabs .iter() .enumerate() .map(|(ix, item)| { diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 3f22212593..9094ad6601 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -287,7 +287,7 @@ impl TitleBar { ) }); - let platform_titlebar = cx.new(|cx| PlatformTitleBar::new(id, cx)); + let platform_titlebar = cx.new(|cx| PlatformTitleBar::new(id, window, cx)); Self { platform_titlebar, From 08902bde0f25c5f9ad7f6c0bd99faef16608346b Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Sat, 26 Jul 2025 21:32:16 +0200 Subject: [PATCH 32/42] Properly register conditional actions --- crates/title_bar/src/system_window_tabs.rs | 82 +++++++++++++++------- 1 file changed, 56 insertions(+), 26 deletions(-) diff --git a/crates/title_bar/src/system_window_tabs.rs b/crates/title_bar/src/system_window_tabs.rs index 119a621254..e9264e9029 100644 --- a/crates/title_bar/src/system_window_tabs.rs +++ b/crates/title_bar/src/system_window_tabs.rs @@ -74,33 +74,63 @@ impl SystemWindowTabs { pub fn init(cx: &mut App) { cx.observe_new(|workspace: &mut Workspace, _, _| { - workspace - .register_action(|_, _: &ShowNextWindowTab, window, cx| { - SystemWindowTabController::select_next_tab( - cx, - window.window_handle().window_id(), - ); - }) - .register_action(|_, _: &ShowPreviousWindowTab, window, cx| { - SystemWindowTabController::select_previous_tab( - cx, - window.window_handle().window_id(), - ); - }) - .register_action(|_, _: &MergeAllWindows, window, cx| { - SystemWindowTabController::merge_all_windows( - cx, - window.window_handle().window_id(), - ); - window.merge_all_windows(); - }) - .register_action(|_, _: &MoveTabToNewWindow, window, cx| { - SystemWindowTabController::move_tab_to_new_window( - cx, - window.window_handle().window_id(), - ); - window.move_tab_to_new_window(); + workspace.register_action_renderer(|div, _, window, cx| { + let window_id = window.window_handle().window_id(); + let controller = cx.global::(); + let tab_group = controller.tabs().iter().find_map(|(group, windows)| { + windows + .iter() + .find(|tab| tab.id == window_id) + .map(|_| *group) }); + + if let Some(tab_group) = tab_group { + let all_tab_groups = controller.tabs(); + let tabs = controller.windows(tab_group); + let show_merge_all_windows = all_tab_groups.len() > 1; + let show_other_tab_actions = if let Some(tabs) = tabs { + tabs.len() > 1 + } else { + false + }; + + return div + .when(show_other_tab_actions, |div| { + div.on_action(move |_: &ShowNextWindowTab, window, cx| { + SystemWindowTabController::select_next_tab( + cx, + window.window_handle().window_id(), + ); + }) + .on_action(move |_: &ShowPreviousWindowTab, window, cx| { + SystemWindowTabController::select_previous_tab( + cx, + window.window_handle().window_id(), + ); + }) + .on_action( + move |_: &MoveTabToNewWindow, window, cx| { + SystemWindowTabController::move_tab_to_new_window( + cx, + window.window_handle().window_id(), + ); + window.move_tab_to_new_window(); + }, + ) + }) + .when(show_merge_all_windows, |div| { + div.on_action(move |_: &MergeAllWindows, window, cx| { + SystemWindowTabController::merge_all_windows( + cx, + window.window_handle().window_id(), + ); + window.merge_all_windows(); + }) + }); + } + + div + }); }) .detach(); } From 0dd7aad25a96410545585b045fc62bc1b4edcfb3 Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Sun, 27 Jul 2025 00:27:26 +0200 Subject: [PATCH 33/42] Cleanup tab controller --- crates/gpui/src/app.rs | 210 +++++++++------------ crates/gpui/src/window.rs | 8 +- crates/title_bar/src/system_window_tabs.rs | 101 ++++------ crates/workspace/src/workspace.rs | 2 +- 4 files changed, 134 insertions(+), 187 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 851b389e5d..a2ed5a46f6 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -239,7 +239,7 @@ type ReleaseListener = Box; type NewEntityListener = Box, &mut App) + 'static>; #[doc(hidden)] -#[derive(Clone)] +#[derive(Clone, PartialEq, Eq)] pub struct SystemWindowTab { pub id: WindowId, pub title: SharedString, @@ -260,7 +260,7 @@ impl SystemWindowTab { /// A controller for managing window tabs. #[derive(Default)] pub struct SystemWindowTabController { - tabs: FxHashMap>, + tab_groups: FxHashMap>, } impl Global for SystemWindowTabController {} @@ -269,7 +269,7 @@ impl SystemWindowTabController { /// Create a new instance of the window tab controller. pub fn new() -> Self { Self { - tabs: FxHashMap::default(), + tab_groups: FxHashMap::default(), } } @@ -278,20 +278,29 @@ impl SystemWindowTabController { cx.set_global(SystemWindowTabController::new()); } - /// Get all tabs. - pub fn tabs(&self) -> &FxHashMap> { - &self.tabs + /// Get all tab groups. + pub fn tab_groups(&self) -> &FxHashMap> { + &self.tab_groups } - /// Get all windows in a tab. - pub fn windows(&self, tab_group: usize) -> Option<&Vec> { - self.tabs.get(&tab_group) + /// Get all tabs in the same window. + pub fn tabs(&self, id: WindowId) -> Option<&Vec> { + let tab_group = self + .tab_groups + .iter() + .find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| *group)); + + if let Some(tab_group) = tab_group { + self.tab_groups.get(&tab_group) + } else { + None + } } - /// Move window to a new position within the same tab group. - pub fn update_window_position(cx: &mut App, id: WindowId, ix: usize) { + /// Update the position of a tab within its group. + pub fn update_tab_position(cx: &mut App, id: WindowId, ix: usize) { let mut controller = cx.global_mut::(); - for (_, windows) in controller.tabs.iter_mut() { + for (_, windows) in controller.tab_groups.iter_mut() { if let Some(current_pos) = windows.iter().position(|tab| tab.id == id) { if ix < windows.len() && current_pos != ix { let window_tab = windows.remove(current_pos); @@ -302,11 +311,11 @@ impl SystemWindowTabController { } } - /// Update the title of a window. - pub fn update_window_title(cx: &mut App, id: WindowId, title: SharedString) { + /// Update the title of a tab. + pub fn update_tab_title(cx: &mut App, id: WindowId, title: SharedString) { let controller = cx.global::(); let tab = controller - .tabs + .tab_groups .values() .flat_map(|windows| windows.iter()) .find(|tab| tab.id == id); @@ -316,7 +325,7 @@ impl SystemWindowTabController { } let mut controller = cx.global_mut::(); - for windows in controller.tabs.values_mut() { + for windows in controller.tab_groups.values_mut() { for tab in windows.iter_mut() { if tab.id == id { tab.title = title.clone(); @@ -325,124 +334,96 @@ impl SystemWindowTabController { } } - /// Insert a window into a tab group. - pub fn open_window(cx: &mut App, id: WindowId, windows: Vec) { + /// Insert a tab into a tab group. + pub fn add_tab(cx: &mut App, id: WindowId, tabs: Vec) { let mut controller = cx.global_mut::(); - let mut expected_window_ids: Vec<_> = windows + let Some(tab) = tabs.clone().into_iter().find(|tab| tab.id == id) else { + return; + }; + + let mut expected_tab_ids: Vec<_> = tabs .iter() .filter(|tab| tab.id != id) .map(|tab| tab.id) .sorted() .collect(); - let tab_group = { - controller.tabs.iter().find_map(|(key, group)| { - let mut group_ids: Vec<_> = group.iter().map(|tab| tab.id).sorted().collect(); - if group_ids == expected_window_ids { - Some(*key) - } else { - None - } - }) - }; - - if let Some(tab_group) = tab_group { - if let Some(new_window) = windows.iter().find(|tab| tab.id == id) { - if let Some(group) = controller.tabs.get_mut(&tab_group) { - group.push(new_window.clone()); - } - } - } else { - let new_group_id = controller.tabs.len(); - controller.tabs.insert(new_group_id, windows); - } - } - - /// Remove a window from a tab group. - pub fn remove_window(cx: &mut App, id: WindowId) { - let mut controller = cx.global_mut::(); - controller.tabs.retain(|_, windows| { - if let Some(pos) = windows.iter().position(|tab| tab.id == id) { - windows.remove(pos); - } - !windows.is_empty() - }); - } - - /// Move a window to a different tab group. - pub fn move_tab_to_new_window(cx: &mut App, id: WindowId) { - let mut controller = cx.global_mut::(); - // Find and remove the window from its current group - let mut removed_tab: Option = None; - let mut empty_group_key: Option = None; - - for (group_id, windows) in controller.tabs.iter_mut() { - if let Some(pos) = windows.iter().position(|tab| tab.id == id) { - removed_tab = Some(windows.remove(pos)); - if windows.is_empty() { - empty_group_key = Some(*group_id); - } + let mut tab_group_id = None; + for (group_id, group_tabs) in &controller.tab_groups { + let tab_ids: Vec<_> = group_tabs.iter().map(|tab| tab.id).sorted().collect(); + if tab_ids == expected_tab_ids { + tab_group_id = Some(*group_id); break; } } - // Remove the group if it became empty - if let Some(group_id) = empty_group_key { - controller.tabs.remove(&group_id); + if let Some(tab_group_id) = tab_group_id { + if let Some(tabs) = controller.tab_groups.get_mut(&tab_group_id) { + tabs.push(tab); + } + } else { + let new_group_id = controller.tab_groups.len(); + controller.tab_groups.insert(new_group_id, tabs); } + } + + /// Remove a tab from a tab group. + pub fn remove_tab(cx: &mut App, id: WindowId) -> Option { + let mut controller = cx.global_mut::(); + let mut removed_tab = None; + + controller.tab_groups.retain(|_, tabs| { + if let Some(pos) = tabs.iter().position(|tab| tab.id == id) { + removed_tab = Some(tabs.remove(pos)); + } + !tabs.is_empty() + }); + + removed_tab + } + + /// Move a tab to a new tab group. + pub fn move_tab_to_new_window(cx: &mut App, id: WindowId) { + let mut removed_tab = Self::remove_tab(cx, id); + let mut controller = cx.global_mut::(); - // Insert the removed tab into a new group if it was found if let Some(tab) = removed_tab { - let new_group_id = controller.tabs.keys().max().map_or(0, |k| k + 1); - controller.tabs.insert(new_group_id, vec![tab]); + let new_group_id = controller.tab_groups.keys().max().map_or(0, |k| k + 1); + controller.tab_groups.insert(new_group_id, vec![tab]); } } /// Merge all tab groups into a single group. pub fn merge_all_windows(cx: &mut App, id: WindowId) { let mut controller = cx.global_mut::(); - let mut merged_windows = Vec::new(); - let mut first_group_windows = None; + let Some(initial_tabs) = controller.tabs(id) else { + return; + }; - // Drain all tab groups, but keep track of the group containing the given id - for (_, mut windows) in controller.tabs.drain() { - if windows.iter().any(|tab| tab.id == id) { - first_group_windows = Some(windows); - } else { - merged_windows.extend(windows); - } + let mut all_tabs = initial_tabs.clone(); + for tabs in controller.tab_groups.values() { + all_tabs.extend( + tabs.iter() + .filter(|tab| !initial_tabs.contains(tab)) + .cloned(), + ); } - // Place the group with the given id first, then all others - let mut final_windows = Vec::new(); - if let Some(mut first) = first_group_windows { - final_windows.append(&mut first); - } - final_windows.append(&mut merged_windows); - - controller.tabs.insert(0, final_windows); + controller.tab_groups.clear(); + controller.tab_groups.insert(0, all_tabs); } /// Selects the next tab in the tab group in the trailing direction. pub fn select_next_tab(cx: &mut App, id: WindowId) { let mut controller = cx.global_mut::(); - let tab_group = controller - .tabs - .iter() - .find_map(|(group_id, windows)| { - if windows.iter().any(|tab| tab.id == id) { - Some(*group_id) - } else { - None - } - }) - .expect("WindowId not found in any tab group"); + let Some(tabs) = controller.tabs(id) else { + return; + }; - let windows = controller.tabs.get_mut(&tab_group).unwrap(); - let current_index = windows.iter().position(|tab| tab.id == id).unwrap(); - let next_index = (current_index + 1) % windows.len(); + let current_index = tabs.iter().position(|tab| tab.id == id).unwrap(); + let next_index = (current_index + 1) % tabs.len(); - let _ = &windows[next_index].handle.update(cx, |_, window, _| { + let _ = &tabs[next_index].handle.update(cx, |_, window, _| { window.activate_window(); }); } @@ -450,27 +431,18 @@ impl SystemWindowTabController { /// Selects the previous tab in the tab group in the leading direction. pub fn select_previous_tab(cx: &mut App, id: WindowId) { let mut controller = cx.global_mut::(); - let tab_group = controller - .tabs - .iter() - .find_map(|(group_id, windows)| { - if windows.iter().any(|tab| tab.id == id) { - Some(*group_id) - } else { - None - } - }) - .expect("WindowId not found in any tab group"); + let Some(tabs) = controller.tabs(id) else { + return; + }; - let windows = controller.tabs.get_mut(&tab_group).unwrap(); - let current_index = windows.iter().position(|tab| tab.id == id).unwrap(); + let current_index = tabs.iter().position(|tab| tab.id == id).unwrap(); let previous_index = if current_index == 0 { - windows.len() - 1 + tabs.len() - 1 } else { current_index - 1 }; - let _ = &windows[previous_index].handle.update(cx, |_, window, _| { + let _ = &tabs[previous_index].handle.update(cx, |_, window, _| { window.activate_window(); }); } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 5de33ad836..4dc0b7a2c5 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -965,9 +965,9 @@ impl Window { }, )?; - let windows = platform_window.tabbed_windows(); - if let Some(windows) = windows { - SystemWindowTabController::open_window(cx, handle.window_id(), windows); + let tabs = platform_window.tabbed_windows(); + if let Some(tabs) = tabs { + SystemWindowTabController::add_tab(cx, handle.window_id(), tabs); } let display_id = platform_window.display().map(|display| display.id()); @@ -1004,7 +1004,7 @@ impl Window { move || { let _ = handle.update(&mut cx, |_, window, _| window.remove_window()); let _ = cx.update(|cx| { - SystemWindowTabController::remove_window(cx, window_id); + SystemWindowTabController::remove_tab(cx, window_id); }); } })); diff --git a/crates/title_bar/src/system_window_tabs.rs b/crates/title_bar/src/system_window_tabs.rs index e9264e9029..a28470d9e9 100644 --- a/crates/title_bar/src/system_window_tabs.rs +++ b/crates/title_bar/src/system_window_tabs.rs @@ -49,17 +49,8 @@ impl SystemWindowTabs { subscriptions.push( cx.observe_global::(move |this, cx| { let controller = cx.global::(); - let tab_group = controller.tabs().iter().find_map(|(group, windows)| { - windows - .iter() - .find(|tab| tab.id == window_id) - .map(|_| *group) - }); - - if let Some(tab_group) = tab_group { - if let Some(windows) = controller.windows(tab_group) { - this.tabs = windows.clone(); - } + if let Some(tabs) = controller.tabs(window_id) { + this.tabs = tabs.clone(); } }), ); @@ -77,59 +68,43 @@ impl SystemWindowTabs { workspace.register_action_renderer(|div, _, window, cx| { let window_id = window.window_handle().window_id(); let controller = cx.global::(); - let tab_group = controller.tabs().iter().find_map(|(group, windows)| { - windows - .iter() - .find(|tab| tab.id == window_id) - .map(|_| *group) - }); - if let Some(tab_group) = tab_group { - let all_tab_groups = controller.tabs(); - let tabs = controller.windows(tab_group); - let show_merge_all_windows = all_tab_groups.len() > 1; - let show_other_tab_actions = if let Some(tabs) = tabs { - tabs.len() > 1 - } else { - false - }; + let tab_groups = controller.tab_groups(); + let tabs = controller.tabs(window_id); + let Some(tabs) = tabs else { + return div; + }; - return div - .when(show_other_tab_actions, |div| { - div.on_action(move |_: &ShowNextWindowTab, window, cx| { - SystemWindowTabController::select_next_tab( - cx, - window.window_handle().window_id(), - ); - }) - .on_action(move |_: &ShowPreviousWindowTab, window, cx| { - SystemWindowTabController::select_previous_tab( - cx, - window.window_handle().window_id(), - ); - }) - .on_action( - move |_: &MoveTabToNewWindow, window, cx| { - SystemWindowTabController::move_tab_to_new_window( - cx, - window.window_handle().window_id(), - ); - window.move_tab_to_new_window(); - }, - ) - }) - .when(show_merge_all_windows, |div| { - div.on_action(move |_: &MergeAllWindows, window, cx| { - SystemWindowTabController::merge_all_windows( - cx, - window.window_handle().window_id(), - ); - window.merge_all_windows(); - }) - }); - } - - div + div.when(tabs.len() > 1, |div| { + div.on_action(move |_: &ShowNextWindowTab, window, cx| { + SystemWindowTabController::select_next_tab( + cx, + window.window_handle().window_id(), + ); + }) + .on_action(move |_: &ShowPreviousWindowTab, window, cx| { + SystemWindowTabController::select_previous_tab( + cx, + window.window_handle().window_id(), + ); + }) + .on_action(move |_: &MoveTabToNewWindow, window, cx| { + SystemWindowTabController::move_tab_to_new_window( + cx, + window.window_handle().window_id(), + ); + window.move_tab_to_new_window(); + }) + }) + .when(tab_groups.len() > 1, |div| { + div.on_action(move |_: &MergeAllWindows, window, cx| { + SystemWindowTabController::merge_all_windows( + cx, + window.window_handle().window_id(), + ); + window.merge_all_windows(); + }) + }) }); }) .detach(); @@ -319,7 +294,7 @@ impl SystemWindowTabs { } fn handle_tab_drop(dragged_tab: &DraggedWindowTab, ix: usize, cx: &mut Context) { - SystemWindowTabController::update_window_position(cx, dragged_tab.id, ix); + SystemWindowTabController::update_tab_position(cx, dragged_tab.id, ix); } fn handle_right_click_action( diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index a391af6b2c..dd1b89d5a5 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -4393,7 +4393,7 @@ impl Workspace { return; } window.set_window_title(&title); - SystemWindowTabController::update_window_title( + SystemWindowTabController::update_tab_title( cx, window.window_handle().window_id(), SharedString::from(&title), From c2fa4db758d6afe79e2102316636b781b6a15d64 Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Sun, 27 Jul 2025 17:55:16 +0200 Subject: [PATCH 34/42] Bring back tab visibility toggle --- crates/gpui/src/app.rs | 21 ++++++++++++ crates/gpui/src/platform.rs | 4 +++ crates/gpui/src/platform/mac/window.rs | 40 ++++++++++++++++++++++ crates/gpui/src/window.rs | 22 ++++++++++-- crates/rules_library/src/rules_library.rs | 2 +- crates/title_bar/src/platform_title_bar.rs | 4 +-- crates/title_bar/src/system_window_tabs.rs | 36 +++++++++---------- crates/title_bar/src/title_bar.rs | 2 +- 8 files changed, 105 insertions(+), 26 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index a2ed5a46f6..46cb31386d 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -260,6 +260,7 @@ impl SystemWindowTab { /// A controller for managing window tabs. #[derive(Default)] pub struct SystemWindowTabController { + visible: Option, tab_groups: FxHashMap>, } @@ -269,6 +270,7 @@ impl SystemWindowTabController { /// Create a new instance of the window tab controller. pub fn new() -> Self { Self { + visible: None, tab_groups: FxHashMap::default(), } } @@ -297,6 +299,25 @@ impl SystemWindowTabController { } } + /// Initialize the visibility of the system window tab controller. + pub fn init_visible(cx: &mut App, visible: bool) { + let mut controller = cx.global_mut::(); + if !controller.visible.is_some() { + controller.visible = Some(visible); + } + } + + /// Get the visibility of the system window tab controller. + pub fn is_visible(&self) -> bool { + self.visible.unwrap_or(false) + } + + /// Set the visibility of the system window tab controller. + pub fn set_visible(cx: &mut App, visible: bool) { + let mut controller = cx.global_mut::(); + controller.visible = Some(visible); + } + /// Update the position of a tab within its group. pub fn update_tab_position(cx: &mut App, id: WindowId, ix: usize) { let mut controller = cx.global_mut::(); diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 764250c3d9..f95ac4979e 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -506,6 +506,9 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle { fn tabbed_windows(&self) -> Option> { None } + fn tab_bar_visible(&self) -> bool { + false + } fn set_edited(&mut self, _edited: bool) {} fn show_character_palette(&self) {} fn titlebar_double_click(&self) {} @@ -513,6 +516,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle { fn on_merge_all_windows(&self, _callback: Box) {} fn on_select_previous_tab(&self, _callback: Box) {} fn on_select_next_tab(&self, _callback: Box) {} + fn on_toggle_tab_bar(&self, _callback: Box) {} fn merge_all_windows(&self) {} fn move_tab_to_new_window(&self) {} fn toggle_window_tab_overview(&self) {} diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 8ac88038f6..4f7c745968 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -377,6 +377,11 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C select_previous_tab as extern "C" fn(&Object, Sel, id), ); + decl.add_method( + sel!(toggleTabBar:), + toggle_tab_bar as extern "C" fn(&Object, Sel, id), + ); + decl.register() } } @@ -413,6 +418,7 @@ struct MacWindowState { merge_all_windows_callback: Option>, select_next_tab_callback: Option>, select_previous_tab_callback: Option>, + toggle_tab_bar_callback: Option>, } impl MacWindowState { @@ -708,6 +714,7 @@ impl MacWindow { merge_all_windows_callback: None, select_next_tab_callback: None, select_previous_tab_callback: None, + toggle_tab_bar_callback: None, }))); (*native_window).set_ivar( @@ -1399,6 +1406,18 @@ impl PlatformWindow for MacWindow { } } + fn tab_bar_visible(&self) -> bool { + unsafe { + let tab_group: id = msg_send![self.0.lock().native_window, tabGroup]; + if tab_group.is_null() { + false + } else { + let tab_bar_visible: BOOL = msg_send![tab_group, isTabBarVisible]; + tab_bar_visible == YES + } + } + } + fn on_move_tab_to_new_window(&self, callback: Box) { self.0.as_ref().lock().move_tab_to_new_window_callback = Some(callback); } @@ -1415,6 +1434,10 @@ impl PlatformWindow for MacWindow { self.0.as_ref().lock().select_previous_tab_callback = Some(callback); } + fn on_toggle_tab_bar(&self, callback: Box) { + self.0.as_ref().lock().toggle_tab_bar_callback = Some(callback); + } + fn draw(&self, scene: &crate::Scene) { let mut this = self.0.lock(); this.renderer.draw(scene); @@ -1856,6 +1879,7 @@ extern "C" fn window_did_change_occlusion_state(this: &Object, _: Sel, _: id) { .occlusionState() .contains(NSWindowOcclusionState::NSWindowOcclusionStateVisible) { + lock.move_traffic_light(); lock.start_display_link(); } else { lock.stop_display_link(); @@ -2562,3 +2586,19 @@ extern "C" fn select_previous_tab(this: &Object, _sel: Sel, _id: id) { window_state.lock().select_previous_tab_callback = Some(callback); } } + +extern "C" fn toggle_tab_bar(this: &Object, _sel: Sel, _id: id) { + unsafe { + let _: () = msg_send![super(this, class!(NSWindow)), toggleTabBar:nil]; + + let window_state = get_window_state(this); + let mut lock = window_state.as_ref().lock(); + lock.move_traffic_light(); + + if let Some(mut callback) = lock.toggle_tab_bar_callback.take() { + drop(lock); + callback(); + window_state.lock().toggle_tab_bar_callback = Some(callback); + } + } +} diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 4dc0b7a2c5..701f7d5a1b 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -965,8 +965,9 @@ impl Window { }, )?; - let tabs = platform_window.tabbed_windows(); - if let Some(tabs) = tabs { + let tab_bar_visible = platform_window.tab_bar_visible(); + SystemWindowTabController::init_visible(cx, tab_bar_visible); + if let Some(tabs) = platform_window.tabbed_windows() { SystemWindowTabController::add_tab(cx, handle.window_id(), tabs); } @@ -1177,6 +1178,17 @@ impl Window { .log_err(); }) }); + platform_window.on_toggle_tab_bar({ + let mut cx = cx.to_async(); + Box::new(move || { + handle + .update(&mut cx, |_, window, cx| { + let tab_bar_visible = window.platform_window.tab_bar_visible(); + SystemWindowTabController::set_visible(cx, tab_bar_visible); + }) + .log_err(); + }) + }); if let Some(app_id) = app_id { platform_window.set_app_id(&app_id); @@ -4346,6 +4358,12 @@ impl Window { self.platform_window.tabbed_windows() } + /// Returns the tab bar visibility. + /// This is macOS specific. + pub fn tab_bar_visible(&self) -> bool { + self.platform_window.tab_bar_visible() + } + /// Merges all open windows into a single tabbed window. /// This is macOS specific. pub fn merge_all_windows(&self) { diff --git a/crates/rules_library/src/rules_library.rs b/crates/rules_library/src/rules_library.rs index e1c67784e4..84feed59bc 100644 --- a/crates/rules_library/src/rules_library.rs +++ b/crates/rules_library/src/rules_library.rs @@ -414,7 +414,7 @@ impl RulesLibrary { }); Self { title_bar: if !cfg!(target_os = "macos") { - Some(cx.new(|cx| PlatformTitleBar::new("rules-library-title-bar", window, cx))) + Some(cx.new(|cx| PlatformTitleBar::new("rules-library-title-bar", cx))) } else { None }, diff --git a/crates/title_bar/src/platform_title_bar.rs b/crates/title_bar/src/platform_title_bar.rs index 409cb41c27..bc1057a4d4 100644 --- a/crates/title_bar/src/platform_title_bar.rs +++ b/crates/title_bar/src/platform_title_bar.rs @@ -20,9 +20,9 @@ pub struct PlatformTitleBar { } impl PlatformTitleBar { - pub fn new(id: impl Into, window: &mut Window, cx: &mut Context) -> Self { + pub fn new(id: impl Into, cx: &mut Context) -> Self { let platform_style = PlatformStyle::platform(); - let system_window_tabs = cx.new(|cx| SystemWindowTabs::new(window, cx)); + let system_window_tabs = cx.new(|_cx| SystemWindowTabs::new()); Self { id: id.into(), diff --git a/crates/title_bar/src/system_window_tabs.rs b/crates/title_bar/src/system_window_tabs.rs index a28470d9e9..9349fa054a 100644 --- a/crates/title_bar/src/system_window_tabs.rs +++ b/crates/title_bar/src/system_window_tabs.rs @@ -1,8 +1,8 @@ use settings::Settings; use gpui::{ - Context, Hsla, InteractiveElement, ParentElement, ScrollHandle, Styled, Subscription, - SystemWindowTab, SystemWindowTabController, Window, WindowId, actions, canvas, div, + Context, Hsla, InteractiveElement, ParentElement, ScrollHandle, Styled, SystemWindowTab, + SystemWindowTabController, Window, WindowId, actions, canvas, div, }; use ui::{ @@ -38,28 +38,14 @@ pub struct SystemWindowTabs { tabs: Vec, tab_bar_scroll_handle: ScrollHandle, measured_tab_width: Pixels, - _subscriptions: Vec, } impl SystemWindowTabs { - pub fn new(window: &mut Window, cx: &mut Context) -> Self { - let window_id = window.window_handle().window_id(); - let mut subscriptions = Vec::new(); - - subscriptions.push( - cx.observe_global::(move |this, cx| { - let controller = cx.global::(); - if let Some(tabs) = controller.tabs(window_id) { - this.tabs = tabs.clone(); - } - }), - ); - + pub fn new() -> Self { Self { tabs: Vec::new(), tab_bar_scroll_handle: ScrollHandle::new(), measured_tab_width: px(0.), - _subscriptions: subscriptions, } } @@ -327,8 +313,18 @@ impl Render for SystemWindowTabs { let inactive_background_color = cx.theme().colors().tab_bar_background; let entity = cx.entity(); - let tab_items = self - .tabs + let controller = cx.global::(); + let visible = controller.is_visible(); + let current_window_tab = vec![SystemWindowTab::new( + SharedString::from(window.window_title()), + window.window_handle(), + )]; + let tabs = controller + .tabs(window.window_handle().window_id()) + .unwrap_or(¤t_window_tab) + .clone(); + + let tab_items = tabs .iter() .enumerate() .map(|(ix, item)| { @@ -344,7 +340,7 @@ impl Render for SystemWindowTabs { .collect::>(); let number_of_tabs = tab_items.len().max(1); - if number_of_tabs <= 1 { + if !window.tab_bar_visible() && !visible { return h_flex().into_any_element(); } diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 9094ad6601..3f22212593 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -287,7 +287,7 @@ impl TitleBar { ) }); - let platform_titlebar = cx.new(|cx| PlatformTitleBar::new(id, window, cx)); + let platform_titlebar = cx.new(|cx| PlatformTitleBar::new(id, cx)); Self { platform_titlebar, From 1900f2a5d3814b9eabd882e2cf6f913ed4f1c031 Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Sun, 27 Jul 2025 18:55:09 +0200 Subject: [PATCH 35/42] Fix right click menu items --- crates/title_bar/src/system_window_tabs.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/title_bar/src/system_window_tabs.rs b/crates/title_bar/src/system_window_tabs.rs index 9349fa054a..d905ad84e8 100644 --- a/crates/title_bar/src/system_window_tabs.rs +++ b/crates/title_bar/src/system_window_tabs.rs @@ -35,7 +35,6 @@ pub struct DraggedWindowTab { } pub struct SystemWindowTabs { - tabs: Vec, tab_bar_scroll_handle: ScrollHandle, measured_tab_width: Pixels, } @@ -43,7 +42,6 @@ pub struct SystemWindowTabs { impl SystemWindowTabs { pub fn new() -> Self { Self { - tabs: Vec::new(), tab_bar_scroll_handle: ScrollHandle::new(), measured_tab_width: px(0.), } @@ -100,6 +98,7 @@ impl SystemWindowTabs { &self, ix: usize, item: SystemWindowTab, + tabs: Vec, active_background_color: Hsla, inactive_background_color: Hsla, window: &mut Window, @@ -199,7 +198,6 @@ impl SystemWindowTabs { }) .into_any(); - let tabs = self.tabs.clone(); let menu = right_click_menu(ix) .trigger(|_, _, _| tab) .menu(move |window, cx| { @@ -331,6 +329,7 @@ impl Render for SystemWindowTabs { self.render_tab( ix, item.clone(), + tabs.clone(), active_background_color, inactive_background_color, window, From b4d7a29b3a9cb91c6542fe05e3eccda874ddc34d Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Sun, 17 Aug 2025 16:29:42 +0200 Subject: [PATCH 36/42] Switch between actual windows instead of tabs --- crates/gpui/src/app.rs | 74 ++++++++++++++++++++++++++++++- crates/gpui/src/window.rs | 2 + crates/workspace/src/workspace.rs | 56 +++++++++++++---------- 3 files changed, 108 insertions(+), 24 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 46cb31386d..cf635e7faa 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -7,7 +7,7 @@ use std::{ path::{Path, PathBuf}, rc::{Rc, Weak}, sync::{Arc, atomic::Ordering::SeqCst}, - time::Duration, + time::{Duration, Instant}, }; use anyhow::{Context as _, Result, anyhow}; @@ -244,6 +244,7 @@ pub struct SystemWindowTab { pub id: WindowId, pub title: SharedString, pub handle: AnyWindowHandle, + pub last_active_at: Instant, } impl SystemWindowTab { @@ -253,6 +254,7 @@ impl SystemWindowTab { id: handle.id, title, handle, + last_active_at: Instant::now(), } } } @@ -285,6 +287,64 @@ impl SystemWindowTabController { &self.tab_groups } + /// Get the next tab group window handle. + pub fn get_next_tab_group_window(cx: &mut App, id: WindowId) -> Option<&AnyWindowHandle> { + let controller = cx.global::(); + let current_group = controller + .tab_groups + .iter() + .find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| group)); + + let Some(current_group) = current_group else { + return None; + }; + + let mut group_ids: Vec<_> = controller.tab_groups.keys().collect(); + let idx = group_ids.iter().position(|g| *g == current_group)?; + let next_idx = (idx + 1) % group_ids.len(); + + controller + .tab_groups + .get(&group_ids[next_idx]) + .and_then(|tabs| { + tabs.iter() + .max_by_key(|tab| tab.last_active_at) + .or_else(|| tabs.first()) + .map(|tab| &tab.handle) + }) + } + + /// Get the previous tab group window handle. + pub fn get_prev_tab_group_window(cx: &mut App, id: WindowId) -> Option<&AnyWindowHandle> { + let controller = cx.global::(); + let current_group = controller + .tab_groups + .iter() + .find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| group)); + + let Some(current_group) = current_group else { + return None; + }; + + let mut group_ids: Vec<_> = controller.tab_groups.keys().collect(); + let idx = group_ids.iter().position(|g| *g == current_group)?; + let prev_idx = if idx == 0 { + group_ids.len() - 1 + } else { + idx - 1 + }; + + controller + .tab_groups + .get(&group_ids[prev_idx]) + .and_then(|tabs| { + tabs.iter() + .max_by_key(|tab| tab.last_active_at) + .or_else(|| tabs.first()) + .map(|tab| &tab.handle) + }) + } + /// Get all tabs in the same window. pub fn tabs(&self, id: WindowId) -> Option<&Vec> { let tab_group = self @@ -318,6 +378,18 @@ impl SystemWindowTabController { controller.visible = Some(visible); } + /// Update the last active of a window. + pub fn update_last_active(cx: &mut App, id: WindowId) { + let mut controller = cx.global_mut::(); + for windows in controller.tab_groups.values_mut() { + for tab in windows.iter_mut() { + if tab.id == id { + tab.last_active_at = Instant::now(); + } + } + } + } + /// Update the position of a tab within its group. pub fn update_tab_position(cx: &mut App, id: WindowId, ix: usize) { let mut controller = cx.global_mut::(); diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 701f7d5a1b..9b1b2b994c 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -1098,6 +1098,8 @@ impl Window { window.bounds_changed(cx); window.refresh(); + + SystemWindowTabController::update_last_active(cx, window.handle.id); }) .log_err(); } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index dd1b89d5a5..72c6f99c2c 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -5864,17 +5864,22 @@ impl Workspace { return; }; let windows = cx.windows(); - let Some(next_window) = windows - .iter() - .cycle() - .skip_while(|window| window.window_id() != current_window_id) - .nth(1) - else { - return; - }; - next_window - .update(cx, |_, window, _| window.activate_window()) - .ok(); + let next_window = + SystemWindowTabController::get_next_tab_group_window(cx, current_window_id).or_else( + || { + windows + .iter() + .cycle() + .skip_while(|window| window.window_id() != current_window_id) + .nth(1) + }, + ); + + if let Some(window) = next_window { + window + .update(cx, |_, window, _| window.activate_window()) + .ok(); + } } pub fn activate_previous_window(&mut self, cx: &mut Context) { @@ -5882,18 +5887,23 @@ impl Workspace { return; }; let windows = cx.windows(); - let Some(prev_window) = windows - .iter() - .rev() - .cycle() - .skip_while(|window| window.window_id() != current_window_id) - .nth(1) - else { - return; - }; - prev_window - .update(cx, |_, window, _| window.activate_window()) - .ok(); + let prev_window = + SystemWindowTabController::get_prev_tab_group_window(cx, current_window_id).or_else( + || { + windows + .iter() + .rev() + .cycle() + .skip_while(|window| window.window_id() != current_window_id) + .nth(1) + }, + ); + + if let Some(window) = prev_window { + window + .update(cx, |_, window, _| window.activate_window()) + .ok(); + } } pub fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context) { From f4228f8999219726585c4390883382b3b6013793 Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Sun, 17 Aug 2025 18:12:13 +0200 Subject: [PATCH 37/42] Fix dragged tab font difference --- crates/title_bar/src/system_window_tabs.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/title_bar/src/system_window_tabs.rs b/crates/title_bar/src/system_window_tabs.rs index d905ad84e8..679986ce90 100644 --- a/crates/title_bar/src/system_window_tabs.rs +++ b/crates/title_bar/src/system_window_tabs.rs @@ -5,6 +5,7 @@ use gpui::{ SystemWindowTabController, Window, WindowId, actions, canvas, div, }; +use theme::ThemeSettings; use ui::{ Color, ContextMenu, DynamicSpacing, IconButton, IconButtonShape, IconName, IconSize, Label, LabelSize, Tab, h_flex, prelude::*, right_click_menu, @@ -405,6 +406,7 @@ impl Render for DraggedWindowTab { _window: &mut gpui::Window, cx: &mut gpui::Context, ) -> impl gpui::IntoElement { + let ui_font = ThemeSettings::get_global(cx).ui_font.clone(); let label = Label::new(self.title.clone()) .size(LabelSize::Small) .truncate() @@ -426,6 +428,7 @@ impl Render for DraggedWindowTab { }) .border_1() .border_color(cx.theme().colors().border) + .font(ui_font) .child(label) } } From 3f9cb9842c39e64ac30dd1b2ca9ae3ffebb7345b Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Sun, 17 Aug 2025 18:57:16 +0200 Subject: [PATCH 38/42] Move tab to new window when dragged outside tab bar --- crates/title_bar/src/system_window_tabs.rs | 34 +++++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/crates/title_bar/src/system_window_tabs.rs b/crates/title_bar/src/system_window_tabs.rs index 679986ce90..b2e7db32a6 100644 --- a/crates/title_bar/src/system_window_tabs.rs +++ b/crates/title_bar/src/system_window_tabs.rs @@ -1,8 +1,8 @@ use settings::Settings; use gpui::{ - Context, Hsla, InteractiveElement, ParentElement, ScrollHandle, Styled, SystemWindowTab, - SystemWindowTabController, Window, WindowId, actions, canvas, div, + AnyWindowHandle, Context, Hsla, InteractiveElement, MouseButton, ParentElement, ScrollHandle, + Styled, SystemWindowTab, SystemWindowTabController, Window, WindowId, actions, canvas, div, }; use theme::ThemeSettings; @@ -28,6 +28,7 @@ actions!( #[derive(Clone)] pub struct DraggedWindowTab { pub id: WindowId, + pub handle: AnyWindowHandle, pub title: String, pub width: Pixels, pub is_active: bool, @@ -38,6 +39,7 @@ pub struct DraggedWindowTab { pub struct SystemWindowTabs { tab_bar_scroll_handle: ScrollHandle, measured_tab_width: Pixels, + last_dragged_tab: Option, } impl SystemWindowTabs { @@ -45,6 +47,7 @@ impl SystemWindowTabs { Self { tab_bar_scroll_handle: ScrollHandle::new(), measured_tab_width: px(0.), + last_dragged_tab: None, } } @@ -105,6 +108,7 @@ impl SystemWindowTabs { window: &mut Window, cx: &mut Context, ) -> impl IntoElement + use<> { + let entity = cx.entity(); let settings = ItemSettings::get_global(cx); let close_side = &settings.close_position; let show_close_button = &settings.show_close_button; @@ -138,19 +142,26 @@ impl SystemWindowTabs { .on_drag( DraggedWindowTab { id: item.id, + handle: item.handle.clone(), title: item.title.to_string(), width, is_active, active_background_color, inactive_background_color, }, - |tab, _, _, cx| cx.new(|_| tab.clone()), + move |tab, _, _, cx| { + entity.update(cx, |this, _cx| { + this.last_dragged_tab = Some(tab.clone()); + }); + cx.new(|_| tab.clone()) + }, ) .drag_over::(|element, _, _, cx| { element.bg(cx.theme().colors().drop_target_background) }) .on_drop( - cx.listener(move |_this, dragged_tab: &DraggedWindowTab, _window, cx| { + cx.listener(move |this, dragged_tab: &DraggedWindowTab, _window, cx| { + this.last_dragged_tab = None; Self::handle_tab_drop(dragged_tab, ix, cx); }), ) @@ -348,6 +359,21 @@ impl Render for SystemWindowTabs { .w_full() .h(Tab::container_height(cx)) .bg(inactive_background_color) + .on_mouse_up_out( + MouseButton::Left, + cx.listener(|this, _event, window, cx| { + if let Some(tab) = this.last_dragged_tab.take() { + SystemWindowTabController::move_tab_to_new_window(cx, tab.id); + if tab.id == window.window_handle().window_id() { + window.move_tab_to_new_window(); + } else { + let _ = tab.handle.update(cx, |_, window, _cx| { + window.move_tab_to_new_window(); + }); + } + } + }), + ) .child( h_flex() .id("window tabs") From 2c364ac55f7f3e94940691e7bdc21579d27a5b86 Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Sun, 17 Aug 2025 19:05:19 +0200 Subject: [PATCH 39/42] Fix clippy issues --- Cargo.lock | 1 - crates/gpui/examples/window_positioning.rs | 1 + crates/gpui/src/app.rs | 12 +++--------- crates/gpui/src/platform.rs | 1 + crates/title_bar/Cargo.toml | 1 - crates/title_bar/src/system_window_tabs.rs | 10 +++++----- 6 files changed, 10 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f4941a1abb..dc9d074f01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16711,7 +16711,6 @@ dependencies = [ "client", "cloud_llm_client", "collections", - "command_palette_hooks", "db", "gpui", "http_client", diff --git a/crates/gpui/examples/window_positioning.rs b/crates/gpui/examples/window_positioning.rs index 0f0bb8ac28..25a3ff1e09 100644 --- a/crates/gpui/examples/window_positioning.rs +++ b/crates/gpui/examples/window_positioning.rs @@ -62,6 +62,7 @@ fn build_window_options(display_id: DisplayId, bounds: Bounds) -> Window app_id: None, window_min_size: None, window_decorations: None, + tabbing_identifier: None, } } diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index cf635e7faa..01fc848c3c 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -295,10 +295,7 @@ impl SystemWindowTabController { .iter() .find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| group)); - let Some(current_group) = current_group else { - return None; - }; - + let current_group = current_group?; let mut group_ids: Vec<_> = controller.tab_groups.keys().collect(); let idx = group_ids.iter().position(|g| *g == current_group)?; let next_idx = (idx + 1) % group_ids.len(); @@ -322,10 +319,7 @@ impl SystemWindowTabController { .iter() .find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| group)); - let Some(current_group) = current_group else { - return None; - }; - + let current_group = current_group?; let mut group_ids: Vec<_> = controller.tab_groups.keys().collect(); let idx = group_ids.iter().position(|g| *g == current_group)?; let prev_idx = if idx == 0 { @@ -362,7 +356,7 @@ impl SystemWindowTabController { /// Initialize the visibility of the system window tab controller. pub fn init_visible(cx: &mut App, visible: bool) { let mut controller = cx.global_mut::(); - if !controller.visible.is_some() { + if controller.visible.is_none() { controller.visible = Some(visible); } } diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index f95ac4979e..ecbe812b46 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -1164,6 +1164,7 @@ pub(crate) struct WindowParams { pub display_id: Option, pub window_min_size: Option>, + #[cfg(target_os = "macos")] pub tabbing_identifier: Option, } diff --git a/crates/title_bar/Cargo.toml b/crates/title_bar/Cargo.toml index 82a8b06f82..cf178e2850 100644 --- a/crates/title_bar/Cargo.toml +++ b/crates/title_bar/Cargo.toml @@ -32,7 +32,6 @@ auto_update.workspace = true call.workspace = true chrono.workspace = true client.workspace = true -command_palette_hooks.workspace = true cloud_llm_client.workspace = true db.workspace = true gpui = { workspace = true, features = ["screen-capture"] } diff --git a/crates/title_bar/src/system_window_tabs.rs b/crates/title_bar/src/system_window_tabs.rs index b2e7db32a6..4233e53f27 100644 --- a/crates/title_bar/src/system_window_tabs.rs +++ b/crates/title_bar/src/system_window_tabs.rs @@ -142,7 +142,7 @@ impl SystemWindowTabs { .on_drag( DraggedWindowTab { id: item.id, - handle: item.handle.clone(), + handle: item.handle, title: item.title.to_string(), width, is_active, @@ -189,13 +189,13 @@ impl SystemWindowTabs { .icon_color(Color::Muted) .icon_size(IconSize::XSmall) .on_click({ - let handle = item.handle.clone(); move |_, window, cx| { - if handle.window_id() == window.window_handle().window_id() + if item.handle.window_id() + == window.window_handle().window_id() { window.dispatch_action(Box::new(CloseWindow), cx); } else { - let _ = handle.update(cx, |_, window, cx| { + let _ = item.handle.update(cx, |_, window, cx| { window.dispatch_action(Box::new(CloseWindow), cx); }); } @@ -272,7 +272,7 @@ impl SystemWindowTabs { ); }); - menu.context(focus_handle.clone()) + menu.context(focus_handle) }) }); From 9b881a9da13fc4fe097a53e491de21f732dc15c8 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 25 Aug 2025 21:24:44 -0600 Subject: [PATCH 40/42] Hmm --- crates/gpui/src/window.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index f8a228d247..c365d9101b 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -961,6 +961,7 @@ impl Window { show, display_id, window_min_size, + #[cfg(target_os = "macos")] tabbing_identifier, }, )?; From b1d244c6c60d67ae2e3dba76abeb36cf367d3af9 Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Tue, 26 Aug 2025 20:54:33 +0200 Subject: [PATCH 41/42] Fix more clippy issues --- crates/gpui/src/app.rs | 4 ++-- crates/gpui/src/platform/mac/window.rs | 2 +- crates/gpui/src/window.rs | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 0728f2e14a..d6f471b3f4 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -302,7 +302,7 @@ impl SystemWindowTabController { controller .tab_groups - .get(&group_ids[next_idx]) + .get(group_ids[next_idx]) .and_then(|tabs| { tabs.iter() .max_by_key(|tab| tab.last_active_at) @@ -330,7 +330,7 @@ impl SystemWindowTabController { controller .tab_groups - .get(&group_ids[prev_idx]) + .get(group_ids[prev_idx]) .and_then(|tabs| { tabs.iter() .max_by_key(|tab| tab.last_active_at) diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 63e233368d..9a0af28d18 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -770,7 +770,7 @@ impl MacWindow { native_window.setLevel_(NSNormalWindowLevel); native_window.setAcceptsMouseMovedEvents_(YES); - if let Some(tabbing_identifier) = tabbing_identifier.clone() { + 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]; } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index c365d9101b..6ddb9c83b4 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -944,6 +944,7 @@ impl Window { app_id, window_min_size, window_decorations, + #[cfg_attr(not(target_os = "macos"), allow(unused_variables))] tabbing_identifier, } = options; From 222809b0277939ac8c88a0bdcbcc29dd91014b27 Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Tue, 26 Aug 2025 21:05:44 +0200 Subject: [PATCH 42/42] Sync drop target border with new design --- crates/title_bar/src/system_window_tabs.rs | 29 +++++++++++++++++----- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/crates/title_bar/src/system_window_tabs.rs b/crates/title_bar/src/system_window_tabs.rs index 4233e53f27..cc50fbc2b9 100644 --- a/crates/title_bar/src/system_window_tabs.rs +++ b/crates/title_bar/src/system_window_tabs.rs @@ -28,6 +28,7 @@ actions!( #[derive(Clone)] pub struct DraggedWindowTab { pub id: WindowId, + pub ix: usize, pub handle: AnyWindowHandle, pub title: String, pub width: Pixels, @@ -142,6 +143,7 @@ impl SystemWindowTabs { .on_drag( DraggedWindowTab { id: item.id, + ix, handle: item.handle, title: item.title.to_string(), width, @@ -156,15 +158,30 @@ impl SystemWindowTabs { cx.new(|_| tab.clone()) }, ) - .drag_over::(|element, _, _, cx| { - element.bg(cx.theme().colors().drop_target_background) + .drag_over::({ + let tab_ix = ix; + move |element, dragged_tab: &DraggedWindowTab, _, cx| { + let mut styled_tab = element + .bg(cx.theme().colors().drop_target_background) + .border_color(cx.theme().colors().drop_target_border) + .border_0(); + + if tab_ix < dragged_tab.ix { + styled_tab = styled_tab.border_l_2(); + } else if tab_ix > dragged_tab.ix { + styled_tab = styled_tab.border_r_2(); + } + + styled_tab + } }) - .on_drop( + .on_drop({ + let tab_ix = ix; cx.listener(move |this, dragged_tab: &DraggedWindowTab, _window, cx| { this.last_dragged_tab = None; - Self::handle_tab_drop(dragged_tab, ix, cx); - }), - ) + Self::handle_tab_drop(dragged_tab, tab_ix, cx); + }) + }) .on_click(move |_, _, cx| { let _ = item.handle.update(cx, |_, window, _| { window.activate_window();