This commit is contained in:
Gaauwe Rombouts 2025-08-26 19:06:04 +00:00 committed by GitHub
commit a788db60f9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 1356 additions and 52 deletions

View file

@ -363,6 +363,8 @@
// Whether to show code action buttons in the editor toolbar.
"code_actions": false
},
// Whether to allow windows to tab together based on the users tabbing preference (macOS only).
"use_system_window_tabs": false,
// Titlebar related settings
"title_bar": {
// Whether to show the branch icon beside branch switcher in the titlebar.

View file

@ -62,6 +62,7 @@ impl AgentNotification {
app_id: Some(app_id.to_owned()),
window_min_size: None,
window_decorations: Some(WindowDecorations::Client),
tabbing_identifier: None,
}
}
}

View file

@ -66,5 +66,6 @@ fn notification_window_options(
app_id: Some(app_id.to_owned()),
window_min_size: None,
window_decorations: Some(WindowDecorations::Client),
tabbing_identifier: None,
}
}

View file

@ -62,6 +62,7 @@ fn build_window_options(display_id: DisplayId, bounds: Bounds<Pixels>) -> Window
app_id: None,
window_min_size: None,
window_decorations: None,
tabbing_identifier: None,
}
}

View file

@ -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};
@ -17,6 +17,7 @@ use futures::{
channel::oneshot,
future::{LocalBoxFuture, Shared},
};
use itertools::Itertools;
use parking_lot::RwLock;
use slotmap::SlotMap;
@ -39,8 +40,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,
};
@ -237,6 +238,303 @@ type WindowClosedHandler = Box<dyn FnMut(&mut App)>;
type ReleaseListener = Box<dyn FnOnce(&mut dyn Any, &mut App) + 'static>;
type NewEntityListener = Box<dyn FnMut(AnyEntity, &mut Option<&mut Window>, &mut App) + 'static>;
#[doc(hidden)]
#[derive(Clone, PartialEq, Eq)]
pub struct SystemWindowTab {
pub id: WindowId,
pub title: SharedString,
pub handle: AnyWindowHandle,
pub last_active_at: Instant,
}
impl SystemWindowTab {
/// Create a new instance of the window tab.
pub fn new(title: SharedString, handle: AnyWindowHandle) -> Self {
Self {
id: handle.id,
title,
handle,
last_active_at: Instant::now(),
}
}
}
/// A controller for managing window tabs.
#[derive(Default)]
pub struct SystemWindowTabController {
visible: Option<bool>,
tab_groups: FxHashMap<usize, Vec<SystemWindowTab>>,
}
impl Global for SystemWindowTabController {}
impl SystemWindowTabController {
/// Create a new instance of the window tab controller.
pub fn new() -> Self {
Self {
visible: None,
tab_groups: FxHashMap::default(),
}
}
/// Initialize the global window tab controller.
pub fn init(cx: &mut App) {
cx.set_global(SystemWindowTabController::new());
}
/// Get all tab groups.
pub fn tab_groups(&self) -> &FxHashMap<usize, Vec<SystemWindowTab>> {
&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::<SystemWindowTabController>();
let current_group = controller
.tab_groups
.iter()
.find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| group));
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();
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::<SystemWindowTabController>();
let current_group = controller
.tab_groups
.iter()
.find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| group));
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 {
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<SystemWindowTab>> {
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
}
}
/// Initialize the visibility of the system window tab controller.
pub fn init_visible(cx: &mut App, visible: bool) {
let mut controller = cx.global_mut::<SystemWindowTabController>();
if controller.visible.is_none() {
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::<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::<SystemWindowTabController>();
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::<SystemWindowTabController>();
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);
windows.insert(ix, window_tab);
}
break;
}
}
}
/// Update the title of a tab.
pub fn update_tab_title(cx: &mut App, id: WindowId, title: SharedString) {
let controller = cx.global::<SystemWindowTabController>();
let tab = controller
.tab_groups
.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::<SystemWindowTabController>();
for windows in controller.tab_groups.values_mut() {
for tab in windows.iter_mut() {
if tab.id == id {
tab.title = title.clone();
}
}
}
}
/// Insert a tab into a tab group.
pub fn add_tab(cx: &mut App, id: WindowId, tabs: Vec<SystemWindowTab>) {
let mut controller = cx.global_mut::<SystemWindowTabController>();
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 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;
}
}
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<SystemWindowTab> {
let mut controller = cx.global_mut::<SystemWindowTabController>();
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::<SystemWindowTabController>();
if let Some(tab) = removed_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::<SystemWindowTabController>();
let Some(initial_tabs) = controller.tabs(id) else {
return;
};
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(),
);
}
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::<SystemWindowTabController>();
let Some(tabs) = controller.tabs(id) else {
return;
};
let current_index = tabs.iter().position(|tab| tab.id == id).unwrap();
let next_index = (current_index + 1) % tabs.len();
let _ = &tabs[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, id: WindowId) {
let mut controller = cx.global_mut::<SystemWindowTabController>();
let Some(tabs) = controller.tabs(id) else {
return;
};
let current_index = tabs.iter().position(|tab| tab.id == id).unwrap();
let previous_index = if current_index == 0 {
tabs.len() - 1
} else {
current_index - 1
};
let _ = &tabs[previous_index].handle.update(cx, |_, window, _| {
window.activate_window();
});
}
}
/// 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].
@ -369,6 +667,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);

View file

@ -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;
@ -500,9 +500,26 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
// macOS specific methods
fn get_title(&self) -> String {
String::new()
}
fn tabbed_windows(&self) -> Option<Vec<SystemWindowTab>> {
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) {}
fn on_move_tab_to_new_window(&self, _callback: Box<dyn FnMut()>) {}
fn on_merge_all_windows(&self, _callback: Box<dyn FnMut()>) {}
fn on_select_previous_tab(&self, _callback: Box<dyn FnMut()>) {}
fn on_select_next_tab(&self, _callback: Box<dyn FnMut()>) {}
fn on_toggle_tab_bar(&self, _callback: Box<dyn FnMut()>) {}
fn merge_all_windows(&self) {}
fn move_tab_to_new_window(&self) {}
fn toggle_window_tab_overview(&self) {}
#[cfg(target_os = "windows")]
fn get_raw_handle(&self) -> windows::HWND;
@ -1105,6 +1122,9 @@ pub struct WindowOptions {
/// Whether to use client or server side decorations. Wayland only
/// Note that this may be ignored.
pub window_decorations: Option<WindowDecorations>,
/// 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<String>,
}
/// The variables that can be configured when creating a new window
@ -1144,6 +1164,8 @@ pub(crate) struct WindowParams {
pub display_id: Option<DisplayId>,
pub window_min_size: Option<Size<Pixels>>,
#[cfg(target_os = "macos")]
pub tabbing_identifier: Option<String>,
}
/// Represents the status of how a window should be opened.
@ -1194,6 +1216,7 @@ impl Default for WindowOptions {
app_id: None,
window_min_size: None,
window_decorations: None,
tabbing_identifier: None,
}
}
}

View file

@ -4,8 +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, 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::{
@ -24,6 +26,7 @@ use cocoa::{
NSUserDefaults,
},
};
use core_graphics::display::{CGDirectDisplayID, CGPoint, CGRect};
use ctor::ctor;
use futures::channel::oneshot;
@ -82,6 +85,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" {
@ -343,6 +352,36 @@ 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!(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),
);
decl.add_method(
sel!(selectPreviousTab:),
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()
}
}
@ -375,6 +414,11 @@ struct MacWindowState {
// Whether the next left-mouse click is also the focusing click.
first_mouse: bool,
fullscreen_restore_bounds: Bounds<Pixels>,
move_tab_to_new_window_callback: Option<Box<dyn FnMut()>>,
merge_all_windows_callback: Option<Box<dyn FnMut()>>,
select_next_tab_callback: Option<Box<dyn FnMut()>>,
select_previous_tab_callback: Option<Box<dyn FnMut()>>,
toggle_tab_bar_callback: Option<Box<dyn FnMut()>>,
}
impl MacWindowState {
@ -534,6 +578,7 @@ impl MacWindow {
show,
display_id,
window_min_size,
tabbing_identifier,
}: WindowParams,
executor: ForegroundExecutor,
renderer_context: renderer::Context,
@ -541,7 +586,12 @@ impl MacWindow {
unsafe {
let pool = NSAutoreleasePool::new(nil);
let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: NO];
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() {
@ -660,6 +710,11 @@ 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,
toggle_tab_bar_callback: None,
})));
(*native_window).set_ivar(
@ -714,6 +769,11 @@ impl MacWindow {
WindowKind::Normal => {
native_window.setLevel_(NSNormalWindowLevel);
native_window.setAcceptsMouseMovedEvents_(YES);
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];
}
}
WindowKind::PopUp => {
// Use a tracking area to allow receiving MouseMoved events even when
@ -742,6 +802,38 @@ impl MacWindow {
}
}
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];
// 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];
}
}
}
}
if focus && show {
native_window.makeKeyAndOrderFront_(nil);
} else if show {
@ -796,6 +888,33 @@ impl MacWindow {
window_handles
}
}
pub fn get_user_tabbing_preference() -> Option<UserTabbingPreference> {
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 {
@ -851,6 +970,46 @@ impl PlatformWindow for MacWindow {
.detach();
}
fn merge_all_windows(&self) {
let native_window = self.0.lock().native_window;
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 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),
);
}
}
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()
}
@ -1051,6 +1210,17 @@ 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) {
@ -1212,6 +1382,62 @@ impl PlatformWindow for MacWindow {
self.0.lock().appearance_changed_callback = Some(callback);
}
fn tabbed_windows(&self) -> Option<Vec<SystemWindowTab>> {
unsafe {
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)
}
}
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<dyn FnMut()>) {
self.0.as_ref().lock().move_tab_to_new_window_callback = Some(callback);
}
fn on_merge_all_windows(&self, callback: Box<dyn FnMut()>) {
self.0.as_ref().lock().merge_all_windows_callback = Some(callback);
}
fn on_select_next_tab(&self, callback: Box<dyn FnMut()>) {
self.0.as_ref().lock().select_next_tab_callback = Some(callback);
}
fn on_select_previous_tab(&self, callback: Box<dyn FnMut()>) {
self.0.as_ref().lock().select_previous_tab_callback = Some(callback);
}
fn on_toggle_tab_bar(&self, callback: Box<dyn FnMut()>) {
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);
@ -1653,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();
@ -1714,7 +1941,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
@ -1735,9 +1962,34 @@ 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(Default::default());
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();
if is_active {
lock.move_traffic_light();
}
if let Some(mut callback) = lock.activate_callback.take() {
drop(lock);
callback(is_active);
@ -2273,3 +2525,80 @@ 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 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();
if let Some(mut callback) = lock.select_next_tab_callback.take() {
drop(lock);
callback();
window_state.lock().select_next_tab_callback = Some(callback);
}
}
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 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);
}
}
}

View file

@ -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, 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};
@ -944,6 +944,8 @@ impl Window {
app_id,
window_min_size,
window_decorations,
#[cfg_attr(not(target_os = "macos"), allow(unused_variables))]
tabbing_identifier,
} = options;
let bounds = window_bounds
@ -960,8 +962,17 @@ impl Window {
show,
display_id,
window_min_size,
#[cfg(target_os = "macos")]
tabbing_identifier,
},
)?;
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);
}
let display_id = platform_window.display().map(|display| display.id());
let sprite_atlas = platform_window.sprite_atlas();
let mouse_position = platform_window.mouse_position();
@ -991,9 +1002,13 @@ impl Window {
}
platform_window.on_close(Box::new({
let window_id = handle.window_id();
let mut cx = cx.to_async();
move || {
let _ = handle.update(&mut cx, |_, window, _| window.remove_window());
let _ = cx.update(|cx| {
SystemWindowTabController::remove_tab(cx, window_id);
});
}
}));
platform_window.on_request_frame(Box::new({
@ -1082,7 +1097,11 @@ impl Window {
.activation_observers
.clone()
.retain(&(), |callback| callback(window, cx));
window.bounds_changed(cx);
window.refresh();
SystemWindowTabController::update_last_active(cx, window.handle.id);
})
.log_err();
}
@ -1123,6 +1142,57 @@ 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| {
SystemWindowTabController::select_next_tab(cx, handle.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| {
SystemWindowTabController::select_previous_tab(cx, handle.window_id())
})
.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);
@ -4275,11 +4345,47 @@ 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();
}
/// Gets the window's title at the platform level.
/// This is macOS specific.
pub fn window_title(&self) -> String {
self.platform_window.get_title()
}
/// Returns a list of all tabbed windows and their titles.
/// This is macOS specific.
pub fn tabbed_windows(&self) -> Option<Vec<SystemWindowTab>> {
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) {
self.platform_window.merge_all_windows()
}
/// Moves the tab to a new containing window.
/// This is macOS specific.
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.
#[cfg(any(feature = "inspector", debug_assertions))]
pub fn toggle_inspector(&mut self, cx: &mut App) {

View file

@ -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", cx)))
} else {
None
},

View file

@ -1,28 +1,35 @@
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<SystemWindowTabs>,
}
impl PlatformTitleBar {
pub fn new(id: impl Into<ElementId>) -> Self {
pub fn new(id: impl Into<ElementId>, cx: &mut Context<Self>) -> Self {
let platform_style = PlatformStyle::platform();
let system_window_tabs = cx.new(|_cx| SystemWindowTabs::new());
Self {
id: id.into(),
platform_style,
children: SmallVec::new(),
should_move: false,
system_window_tabs,
}
}
@ -66,7 +73,7 @@ impl Render for PlatformTitleBar {
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()
.h(height)
@ -162,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())
}
}

View file

@ -0,0 +1,477 @@
use settings::Settings;
use gpui::{
AnyWindowHandle, Context, Hsla, InteractiveElement, MouseButton, ParentElement, ScrollHandle,
Styled, SystemWindowTab, 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,
};
use workspace::{
CloseWindow, ItemSettings, Workspace,
item::{ClosePosition, ShowCloseButton},
};
actions!(
window,
[
ShowNextWindowTab,
ShowPreviousWindowTab,
MergeAllWindows,
MoveTabToNewWindow
]
);
#[derive(Clone)]
pub struct DraggedWindowTab {
pub id: WindowId,
pub ix: usize,
pub handle: AnyWindowHandle,
pub title: String,
pub width: Pixels,
pub is_active: bool,
pub active_background_color: Hsla,
pub inactive_background_color: Hsla,
}
pub struct SystemWindowTabs {
tab_bar_scroll_handle: ScrollHandle,
measured_tab_width: Pixels,
last_dragged_tab: Option<DraggedWindowTab>,
}
impl SystemWindowTabs {
pub fn new() -> Self {
Self {
tab_bar_scroll_handle: ScrollHandle::new(),
measured_tab_width: px(0.),
last_dragged_tab: None,
}
}
pub fn init(cx: &mut App) {
cx.observe_new(|workspace: &mut Workspace, _, _| {
workspace.register_action_renderer(|div, _, window, cx| {
let window_id = window.window_handle().window_id();
let controller = cx.global::<SystemWindowTabController>();
let tab_groups = controller.tab_groups();
let tabs = controller.tabs(window_id);
let Some(tabs) = tabs else {
return 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();
}
fn render_tab(
&self,
ix: usize,
item: SystemWindowTab,
tabs: Vec<SystemWindowTab>,
active_background_color: Hsla,
inactive_background_color: Hsla,
window: &mut Window,
cx: &mut Context<Self>,
) -> 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;
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()
.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_drag(
DraggedWindowTab {
id: item.id,
ix,
handle: item.handle,
title: item.title.to_string(),
width,
is_active,
active_background_color,
inactive_background_color,
},
move |tab, _, _, cx| {
entity.update(cx, |this, _cx| {
this.last_dragged_tab = Some(tab.clone());
});
cx.new(|_| tab.clone())
},
)
.drag_over::<DraggedWindowTab>({
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({
let tab_ix = ix;
cx.listener(move |this, dragged_tab: &DraggedWindowTab, _window, cx| {
this.last_dragged_tab = None;
Self::handle_tab_drop(dragged_tab, 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(
IconButton::new("close", IconName::Close)
.shape(IconButtonShape::Square)
.icon_color(Color::Muted)
.icon_size(IconSize::XSmall)
.on_click({
move |_, window, cx| {
if item.handle.window_id()
== window.window_handle().window_id()
{
window.dispatch_action(Box::new(CloseWindow), cx);
} else {
let _ = item.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"),
_ => this,
}),
),
),
})
.into_any();
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 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| {
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| {
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)
})
});
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<Self>) {
SystemWindowTabController::update_tab_position(cx, dragged_tab.id, ix);
}
fn handle_right_click_action<F, P>(
cx: &mut App,
window: &mut Window,
tabs: &Vec<SystemWindowTab>,
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 {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> 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 controller = cx.global::<SystemWindowTabController>();
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(&current_window_tab)
.clone();
let tab_items = tabs
.iter()
.enumerate()
.map(|(ix, item)| {
self.render_tab(
ix,
item.clone(),
tabs.clone(),
active_background_color,
inactive_background_color,
window,
cx,
)
})
.collect::<Vec<_>>();
let number_of_tabs = tab_items.len().max(1);
if !window.tab_bar_visible() && !visible {
return h_flex().into_any_element();
}
h_flex()
.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")
.w_full()
.h(Tab::container_height(cx))
.bg(inactive_background_color)
.overflow_x_scroll()
.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<Self>,
) -> impl gpui::IntoElement {
let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
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 {
self.active_background_color
} else {
self.inactive_background_color
})
.border_1()
.border_color(cx.theme().colors().border)
.font(ui_font)
.child(label)
}
}

View file

@ -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")]
@ -11,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"))]
@ -65,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 {
@ -284,7 +287,7 @@ impl TitleBar {
)
});
let platform_titlebar = cx.new(|_| PlatformTitleBar::new(id));
let platform_titlebar = cx.new(|cx| PlatformTitleBar::new(id, cx));
Self {
platform_titlebar,

View file

@ -42,9 +42,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::{
@ -4375,6 +4375,11 @@ impl Workspace {
return;
}
window.set_window_title(&title);
SystemWindowTabController::update_tab_title(
cx,
window.window_handle().window_id(),
SharedString::from(&title),
);
self.last_window_title = Some(title);
}
@ -5797,17 +5802,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<Self>) {
@ -5815,18 +5825,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<Self>) {

View file

@ -29,6 +29,7 @@ pub struct WorkspaceSettings {
pub on_last_window_closed: OnLastWindowClosed,
pub resize_all_panels_in_dock: Vec<DockPosition>,
pub close_on_file_delete: bool,
pub use_system_window_tabs: bool,
pub zoomed_padding: bool,
}
@ -203,6 +204,10 @@ pub struct WorkspaceSettingsContent {
///
/// Default: false
pub close_on_file_delete: Option<bool>,
/// Whether to allow windows to tab together based on the users tabbing preference (macOS only).
///
/// Default: false
pub use_system_window_tabs: Option<bool>,
/// Whether to show padding for zoomed panels.
/// When enabled, zoomed bottom panels will have some top padding,
/// while zoomed left/right panels will have padding to the right/left (respectively).
@ -357,6 +362,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"

View file

@ -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};
@ -947,9 +947,13 @@ async fn installation_id() -> Result<IdType> {
async fn restore_or_create_workspace(app_state: Arc<AppState>, 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<Result<(), Error>> = Vec::new();
let mut tasks = Vec::new();
for (location, paths) in locations {
for (index, (location, paths)) in locations.into_iter().enumerate() {
match location {
SerializedWorkspaceLocation::Local => {
let app_state = app_state.clone();
@ -964,7 +968,14 @@ async fn restore_or_create_workspace(app_state: Arc<AppState>, 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();
@ -998,7 +1009,7 @@ async fn restore_or_create_workspace(app_state: Arc<AppState>, 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;

View file

@ -282,6 +282,8 @@ pub fn build_window_options(display_uuid: Option<Uuid>, cx: &mut App) -> WindowO
_ => gpui::WindowDecorations::Client,
};
let use_system_window_tabs = WorkspaceSettings::get_global(cx).use_system_window_tabs;
WindowOptions {
titlebar: Some(TitlebarOptions {
title: None,
@ -301,6 +303,11 @@ pub fn build_window_options(display_uuid: Option<Uuid>, cx: &mut App) -> WindowO
width: px(360.0),
height: px(240.0),
}),
tabbing_identifier: if use_system_window_tabs {
Some(String::from("zed"))
} else {
None
},
}
}

View file

@ -1249,6 +1249,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 users tabbing preference (macOS only).
- Setting: `use_system_window_tabs`
- Default: `false`
**Options**
This setting enables integration with macOSs 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.