Add option to show pinned tabs in a separate row (#46573)
## Problem: When working with many `pinned tabs`, they consume significant horizontal space in the tab bar, leaving less room for unpinned (active working) tabs, especially on small screens. This creates a poor user experience as users must constantly scroll to find their working tabs, or the tabs become too narrow to read file names.  ## Solution: Added a new opt-in setting `tab_bar.show_pinned_tabs_in_separate_row` that displays pinned and unpinned tabs in two separate rows: - Top row: Pinned tabs with navigation buttons and tab bar controls - Bottom row: Unpinned (working) tabs with full horizontal space - when pinned and not pinned tabs we show 2 rows: <img width="1512" height="159" alt="2_rows" src="https://github.com/user-attachments/assets/62a85fcc-8073-40d2-a8e4-270efd0d0f21" /> This is a well-established UX pattern used in many popular IDEs including JetBrains products (IntelliJ IDEA, WebStorm, PyCharm, RubyMine), Visual Studio, and Eclipse. The two-row layout only appears when both pinned AND unpinned tabs exist. If only one type is present, a single row is displayed: - when only not pinned tabs we show one row: <img width="1510" height="111" alt="when only not pinned" src="https://github.com/user-attachments/assets/048df4fc-4b17-4ea2-9c5b-b4db91cdfc78" /> - when only pinned tabs we show one row: <img width="1510" height="111" alt="when only pinned tabs" src="https://github.com/user-attachments/assets/7ccf2517-8711-4114-815d-6cbb1e89d7e2" /> **Important** - Default Behavior Preserved: - The setting defaults to false - existing users will see no change whatsoever - Original tab bar logic is preserved in a dedicated `render_single_row_tab_bar` method, while the new two-row layout lives in `render_two_row_tab_bar`. Both methods share common components (`configure_tab_bar_start`, `configure_tab_bar_end`, `render_unpinned_tabs_container`, `render_tab_bar_drop_target`) ensuring DRY code and consistent behavior. - Purely opt-in - users must explicitly enable this feature via Settings UI or settings.json - No impact on default UX - Zed's default tab bar appearance and behavior remains identical This implementation ensures zero risk to the existing user experience while providing an optional enhancement for users who prefer separated tab rows. Configuration Via Settings UI: - Open Settings (Cmd+,) → Editor section → "Pinned Tabs Layout" toggle  Via settings.json: ```json { "tab_bar": { "show_pinned_tabs_in_separate_row": true } } ``` <img width="468" height="67" alt="manual_settings" src="https://github.com/user-attachments/assets/d21f6dde-7683-47b3-8dca-d190049d32fb" /> ## Video of implemented feature: https://github.com/user-attachments/assets/b1e52074-a5a1-4c4e-ad59-e55e2213509d ## Tested: - Verify default behavior unchanged (single row with pinned + unpinned tabs inline) - Enable setting → verify two rows appear when both pinned and unpinned tabs exist - Enable setting → verify single row when only pinned tabs exist - Enable setting → verify single row when only unpinned tabs exist - Verify drag & drop works in both layouts - Verify navigation buttons and tab bar buttons render correctly - Verify setting toggle works in Settings UI - Verify setting works via settings.json Tested on: MacBook Pro M4, macOS 26.2 ## Release Notes: - Added `tab_bar.show_pinned_tabs_in_separate_row` setting to display pinned and unpinned tabs in separate rows, giving unpinned tabs full horizontal space --------- Co-authored-by: Matt Miller <mattrx@gmail.com>
This commit is contained in:
1 parent
9c464f4501
commit
166e6b582a
6 files changed
+425
-64
No files matched your search
@@ -1179,6 +1179,10 @@
|
||||
"show_nav_history_buttons": true,
|
||||
// Whether or not to show the tab bar buttons.
|
||||
"show_tab_bar_buttons": true,
|
||||
// Whether or not to show pinned tabs in a separate row.
|
||||
// When enabled, pinned tabs appear in a top row and unpinned tabs in a bottom row.
|
||||
// When disabled, all tabs appear in a single row (default behavior).
|
||||
"show_pinned_tabs_in_separate_row": false,
|
||||
},
|
||||
// Settings related to the editor's tabs
|
||||
"tabs": {
|
||||
|
||||
@@ -413,6 +413,11 @@ pub struct TabBarSettingsContent {
|
||||
///
|
||||
/// Default: true
|
||||
pub show_tab_bar_buttons: Option<bool>,
|
||||
/// Whether or not to show pinned tabs in a separate row.
|
||||
/// When enabled, pinned tabs appear in a top row and unpinned tabs in a bottom row.
|
||||
///
|
||||
/// Default: false
|
||||
pub show_pinned_tabs_in_separate_row: Option<bool>,
|
||||
}
|
||||
|
||||
#[with_fallible_options]
|
||||
|
||||
@@ -646,6 +646,7 @@ impl VsCodeSettings {
|
||||
show_tab_bar_buttons: self
|
||||
.read_str("workbench.editor.editorActionsLocation")
|
||||
.and_then(|str| if str == "hidden" { Some(false) } else { None }),
|
||||
show_pinned_tabs_in_separate_row: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -3405,7 +3405,7 @@ fn window_and_layout_page() -> SettingsPage {
|
||||
]
|
||||
}
|
||||
|
||||
fn tab_bar_section() -> [SettingsPageItem; 8] {
|
||||
fn tab_bar_section() -> [SettingsPageItem; 9] {
|
||||
[
|
||||
SettingsPageItem::SectionHeader("Tab Bar"),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
@@ -3524,6 +3524,28 @@ fn window_and_layout_page() -> SettingsPage {
|
||||
metadata: None,
|
||||
files: USER,
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Pinned Tabs Layout",
|
||||
description: "Show pinned tabs in a separate row above unpinned tabs.",
|
||||
field: Box::new(SettingField {
|
||||
json_path: Some("tab_bar.show_pinned_tabs_in_separate_row"),
|
||||
pick: |settings_content| {
|
||||
settings_content
|
||||
.tab_bar
|
||||
.as_ref()?
|
||||
.show_pinned_tabs_in_separate_row
|
||||
.as_ref()
|
||||
},
|
||||
write: |settings_content, value| {
|
||||
settings_content
|
||||
.tab_bar
|
||||
.get_or_insert_default()
|
||||
.show_pinned_tabs_in_separate_row = value;
|
||||
},
|
||||
}),
|
||||
metadata: None,
|
||||
files: USER,
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
+390
-63
@@ -2765,7 +2765,7 @@ impl Pane {
|
||||
.on_drop(
|
||||
cx.listener(move |this, dragged_tab: &DraggedTab, window, cx| {
|
||||
this.drag_split_direction = None;
|
||||
this.handle_tab_drop(dragged_tab, ix, window, cx)
|
||||
this.handle_tab_drop(dragged_tab, this.items.len(), window, cx)
|
||||
}),
|
||||
)
|
||||
.on_drop(
|
||||
@@ -3276,6 +3276,7 @@ impl Pane {
|
||||
.zip(tab_details(&self.items, window, cx))
|
||||
.map(|((ix, item), detail)| {
|
||||
self.render_tab(ix, &**item, detail, &focus_handle, window, cx)
|
||||
.into_any_element()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let tab_count = tab_items.len();
|
||||
@@ -3320,7 +3321,51 @@ impl Pane {
|
||||
.flatten()
|
||||
.unwrap_or(false);
|
||||
|
||||
TabBar::new("tab_bar")
|
||||
let tab_bar_settings = TabBarSettings::get_global(cx);
|
||||
let use_separate_rows = tab_bar_settings.show_pinned_tabs_in_separate_row;
|
||||
|
||||
if use_separate_rows && !pinned_tabs.is_empty() && !unpinned_tabs.is_empty() {
|
||||
self.render_two_row_tab_bar(
|
||||
pinned_tabs,
|
||||
unpinned_tabs,
|
||||
tab_count,
|
||||
navigate_backward,
|
||||
navigate_forward,
|
||||
open_aside_left,
|
||||
open_aside_right,
|
||||
render_aside_toggle_left,
|
||||
render_aside_toggle_right,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
self.render_single_row_tab_bar(
|
||||
pinned_tabs,
|
||||
unpinned_tabs,
|
||||
tab_count,
|
||||
navigate_backward,
|
||||
navigate_forward,
|
||||
open_aside_left,
|
||||
open_aside_right,
|
||||
render_aside_toggle_left,
|
||||
render_aside_toggle_right,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn configure_tab_bar_start(
|
||||
&mut self,
|
||||
tab_bar: TabBar,
|
||||
navigate_backward: IconButton,
|
||||
navigate_forward: IconButton,
|
||||
open_aside_left: Option<AnyElement>,
|
||||
render_aside_toggle_left: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Pane>,
|
||||
) -> TabBar {
|
||||
tab_bar
|
||||
.map(|tab_bar| {
|
||||
if let Some(open_aside_left) = open_aside_left
|
||||
&& render_aside_toggle_left
|
||||
@@ -3349,6 +3394,48 @@ impl Pane {
|
||||
tab_bar
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn configure_tab_bar_end(
|
||||
tab_bar: TabBar,
|
||||
open_aside_right: Option<AnyElement>,
|
||||
render_aside_toggle_right: bool,
|
||||
) -> TabBar {
|
||||
tab_bar.map(|tab_bar| {
|
||||
if let Some(open_aside_right) = open_aside_right
|
||||
&& render_aside_toggle_right
|
||||
{
|
||||
tab_bar.end_child(open_aside_right)
|
||||
} else {
|
||||
tab_bar
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn render_single_row_tab_bar(
|
||||
&mut self,
|
||||
pinned_tabs: Vec<AnyElement>,
|
||||
unpinned_tabs: Vec<AnyElement>,
|
||||
tab_count: usize,
|
||||
navigate_backward: IconButton,
|
||||
navigate_forward: IconButton,
|
||||
open_aside_left: Option<AnyElement>,
|
||||
open_aside_right: Option<AnyElement>,
|
||||
render_aside_toggle_left: bool,
|
||||
render_aside_toggle_right: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Pane>,
|
||||
) -> AnyElement {
|
||||
let tab_bar = self
|
||||
.configure_tab_bar_start(
|
||||
TabBar::new("tab_bar"),
|
||||
navigate_backward,
|
||||
navigate_forward,
|
||||
open_aside_left,
|
||||
render_aside_toggle_left,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.children(pinned_tabs.len().ne(&0).then(|| {
|
||||
let max_scroll = self.tab_bar_scroll_handle.max_offset().width;
|
||||
// We need to check both because offset returns delta values even when the scroll handle is not scrollable
|
||||
@@ -3365,75 +3452,129 @@ impl Pane {
|
||||
.border_color(cx.theme().colors().border)
|
||||
})
|
||||
}))
|
||||
.child(self.render_unpinned_tabs_container(unpinned_tabs, tab_count, cx));
|
||||
Self::configure_tab_bar_end(tab_bar, open_aside_right, render_aside_toggle_right)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_two_row_tab_bar(
|
||||
&mut self,
|
||||
pinned_tabs: Vec<AnyElement>,
|
||||
unpinned_tabs: Vec<AnyElement>,
|
||||
tab_count: usize,
|
||||
navigate_backward: IconButton,
|
||||
navigate_forward: IconButton,
|
||||
open_aside_left: Option<AnyElement>,
|
||||
open_aside_right: Option<AnyElement>,
|
||||
render_aside_toggle_left: bool,
|
||||
render_aside_toggle_right: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Pane>,
|
||||
) -> AnyElement {
|
||||
let pinned_tab_bar = self
|
||||
.configure_tab_bar_start(
|
||||
TabBar::new("pinned_tab_bar"),
|
||||
navigate_backward,
|
||||
navigate_forward,
|
||||
open_aside_left,
|
||||
render_aside_toggle_left,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.id("unpinned tabs")
|
||||
.id("pinned_tabs_row")
|
||||
.debug_selector(|| "pinned_tabs_row".into())
|
||||
.overflow_x_scroll()
|
||||
.w_full()
|
||||
.track_scroll(&self.tab_bar_scroll_handle)
|
||||
.on_scroll_wheel(cx.listener(|this, _, _, _| {
|
||||
this.suppress_scroll = true;
|
||||
}))
|
||||
.children(unpinned_tabs)
|
||||
.child(
|
||||
div()
|
||||
.id("tab_bar_drop_target")
|
||||
.min_w_6()
|
||||
// HACK: This empty child is currently necessary to force the drop target to appear
|
||||
// despite us setting a min width above.
|
||||
.child("")
|
||||
// HACK: h_full doesn't occupy the complete height, using fixed height instead
|
||||
.h(Tab::container_height(cx))
|
||||
.flex_grow()
|
||||
.drag_over::<DraggedTab>(|bar, _, _, cx| {
|
||||
bar.bg(cx.theme().colors().drop_target_background)
|
||||
})
|
||||
.drag_over::<DraggedSelection>(|bar, _, _, cx| {
|
||||
bar.bg(cx.theme().colors().drop_target_background)
|
||||
})
|
||||
.on_drop(cx.listener(
|
||||
move |this, dragged_tab: &DraggedTab, window, cx| {
|
||||
this.drag_split_direction = None;
|
||||
this.handle_tab_drop(dragged_tab, this.items.len(), window, cx)
|
||||
},
|
||||
))
|
||||
.on_drop(cx.listener(
|
||||
move |this, selection: &DraggedSelection, window, cx| {
|
||||
this.drag_split_direction = None;
|
||||
this.handle_project_entry_drop(
|
||||
&selection.active_selection.entry_id,
|
||||
Some(tab_count),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
))
|
||||
.on_drop(cx.listener(move |this, paths, window, cx| {
|
||||
this.drag_split_direction = None;
|
||||
this.handle_external_paths_drop(paths, window, cx)
|
||||
}))
|
||||
.on_click(cx.listener(move |this, event: &ClickEvent, window, cx| {
|
||||
if event.click_count() == 2 {
|
||||
window.dispatch_action(
|
||||
this.double_click_dispatch_action.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})),
|
||||
),
|
||||
.children(pinned_tabs),
|
||||
);
|
||||
let pinned_tab_bar = Self::configure_tab_bar_end(
|
||||
pinned_tab_bar,
|
||||
open_aside_right,
|
||||
render_aside_toggle_right,
|
||||
);
|
||||
|
||||
v_flex()
|
||||
.w_full()
|
||||
.flex_none()
|
||||
.child(pinned_tab_bar)
|
||||
.child(
|
||||
TabBar::new("unpinned_tab_bar").child(self.render_unpinned_tabs_container(
|
||||
unpinned_tabs,
|
||||
tab_count,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.map(|tab_bar| {
|
||||
if let Some(open_aside_right) = open_aside_right
|
||||
&& render_aside_toggle_right
|
||||
{
|
||||
tab_bar.end_child(open_aside_right)
|
||||
} else {
|
||||
tab_bar
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_unpinned_tabs_container(
|
||||
&mut self,
|
||||
unpinned_tabs: Vec<AnyElement>,
|
||||
tab_count: usize,
|
||||
cx: &mut Context<Pane>,
|
||||
) -> impl IntoElement {
|
||||
h_flex()
|
||||
.id("unpinned tabs")
|
||||
.overflow_x_scroll()
|
||||
.w_full()
|
||||
.track_scroll(&self.tab_bar_scroll_handle)
|
||||
.on_scroll_wheel(cx.listener(|this, _, _, _| {
|
||||
this.suppress_scroll = true;
|
||||
}))
|
||||
.children(unpinned_tabs)
|
||||
.child(self.render_tab_bar_drop_target(tab_count, cx))
|
||||
}
|
||||
|
||||
fn render_tab_bar_drop_target(
|
||||
&self,
|
||||
tab_count: usize,
|
||||
cx: &mut Context<Pane>,
|
||||
) -> impl IntoElement {
|
||||
div()
|
||||
.id("tab_bar_drop_target")
|
||||
.min_w_6()
|
||||
// HACK: This empty child is currently necessary to force the drop target to appear
|
||||
// despite us setting a min width above.
|
||||
.child("")
|
||||
// HACK: h_full doesn't occupy the complete height, using fixed height instead
|
||||
.h(Tab::container_height(cx))
|
||||
.flex_grow()
|
||||
.drag_over::<DraggedTab>(|bar, _, _, cx| {
|
||||
bar.bg(cx.theme().colors().drop_target_background)
|
||||
})
|
||||
.drag_over::<DraggedSelection>(|bar, _, _, cx| {
|
||||
bar.bg(cx.theme().colors().drop_target_background)
|
||||
})
|
||||
.on_drop(
|
||||
cx.listener(move |this, dragged_tab: &DraggedTab, window, cx| {
|
||||
this.drag_split_direction = None;
|
||||
this.handle_tab_drop(dragged_tab, this.items.len(), window, cx)
|
||||
}),
|
||||
)
|
||||
.on_drop(
|
||||
cx.listener(move |this, selection: &DraggedSelection, window, cx| {
|
||||
this.drag_split_direction = None;
|
||||
this.handle_project_entry_drop(
|
||||
&selection.active_selection.entry_id,
|
||||
Some(tab_count),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
)
|
||||
.on_drop(cx.listener(move |this, paths, window, cx| {
|
||||
this.drag_split_direction = None;
|
||||
this.handle_external_paths_drop(paths, window, cx)
|
||||
}))
|
||||
.on_click(cx.listener(move |this, event: &ClickEvent, window, cx| {
|
||||
if event.click_count() == 2 {
|
||||
window.dispatch_action(this.double_click_dispatch_action.boxed_clone(), cx);
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn render_menu_overlay(menu: &Entity<ContextMenu>) -> Div {
|
||||
div().absolute().bottom_0().right_0().size_0().child(
|
||||
deferred(anchored().anchor(Corner::TopRight).child(menu.clone())).with_priority(1),
|
||||
@@ -5007,6 +5148,181 @@ mod tests {
|
||||
assert_item_labels(&pane, ["A", "B*", "C"], cx);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_separate_pinned_row_disabled_by_default(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
|
||||
let project = Project::test(fs, None, cx).await;
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||
|
||||
let item_a = add_labeled_item(&pane, "A", false, cx);
|
||||
add_labeled_item(&pane, "B", false, cx);
|
||||
add_labeled_item(&pane, "C", false, cx);
|
||||
|
||||
// Pin one tab
|
||||
pane.update_in(cx, |pane, window, cx| {
|
||||
let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
|
||||
pane.pin_tab_at(ix, window, cx);
|
||||
});
|
||||
assert_item_labels(&pane, ["A!", "B", "C*"], cx);
|
||||
|
||||
// Verify setting is disabled by default
|
||||
let is_separate_row_enabled = pane.read_with(cx, |_, cx| {
|
||||
TabBarSettings::get_global(cx).show_pinned_tabs_in_separate_row
|
||||
});
|
||||
assert!(
|
||||
!is_separate_row_enabled,
|
||||
"Separate pinned row should be disabled by default"
|
||||
);
|
||||
|
||||
// Verify pinned_tabs_row element does NOT exist (single row layout)
|
||||
let pinned_row_bounds = cx.debug_bounds("pinned_tabs_row");
|
||||
assert!(
|
||||
pinned_row_bounds.is_none(),
|
||||
"pinned_tabs_row should not exist when setting is disabled"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_separate_pinned_row_two_rows_when_both_tab_types_exist(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
|
||||
let project = Project::test(fs, None, cx).await;
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||
|
||||
// Enable separate row setting
|
||||
set_pinned_tabs_separate_row(cx, true);
|
||||
|
||||
let item_a = add_labeled_item(&pane, "A", false, cx);
|
||||
add_labeled_item(&pane, "B", false, cx);
|
||||
add_labeled_item(&pane, "C", false, cx);
|
||||
|
||||
// Pin one tab - now we have both pinned and unpinned tabs
|
||||
pane.update_in(cx, |pane, window, cx| {
|
||||
let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
|
||||
pane.pin_tab_at(ix, window, cx);
|
||||
});
|
||||
assert_item_labels(&pane, ["A!", "B", "C*"], cx);
|
||||
|
||||
// Verify pinned_tabs_row element exists (two row layout)
|
||||
let pinned_row_bounds = cx.debug_bounds("pinned_tabs_row");
|
||||
assert!(
|
||||
pinned_row_bounds.is_some(),
|
||||
"pinned_tabs_row should exist when setting is enabled and both tab types exist"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_separate_pinned_row_single_row_when_only_pinned_tabs(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
|
||||
let project = Project::test(fs, None, cx).await;
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||
|
||||
// Enable separate row setting
|
||||
set_pinned_tabs_separate_row(cx, true);
|
||||
|
||||
let item_a = add_labeled_item(&pane, "A", false, cx);
|
||||
let item_b = add_labeled_item(&pane, "B", false, cx);
|
||||
|
||||
// Pin all tabs - only pinned tabs exist
|
||||
pane.update_in(cx, |pane, window, cx| {
|
||||
let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
|
||||
pane.pin_tab_at(ix, window, cx);
|
||||
let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
|
||||
pane.pin_tab_at(ix, window, cx);
|
||||
});
|
||||
assert_item_labels(&pane, ["A!", "B*!"], cx);
|
||||
|
||||
// Verify pinned_tabs_row does NOT exist (single row layout for pinned-only)
|
||||
let pinned_row_bounds = cx.debug_bounds("pinned_tabs_row");
|
||||
assert!(
|
||||
pinned_row_bounds.is_none(),
|
||||
"pinned_tabs_row should not exist when only pinned tabs exist (uses single row)"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_separate_pinned_row_single_row_when_only_unpinned_tabs(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
|
||||
let project = Project::test(fs, None, cx).await;
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||
|
||||
// Enable separate row setting
|
||||
set_pinned_tabs_separate_row(cx, true);
|
||||
|
||||
// Add only unpinned tabs
|
||||
add_labeled_item(&pane, "A", false, cx);
|
||||
add_labeled_item(&pane, "B", false, cx);
|
||||
add_labeled_item(&pane, "C", false, cx);
|
||||
assert_item_labels(&pane, ["A", "B", "C*"], cx);
|
||||
|
||||
// Verify pinned_tabs_row does NOT exist (single row layout for unpinned-only)
|
||||
let pinned_row_bounds = cx.debug_bounds("pinned_tabs_row");
|
||||
assert!(
|
||||
pinned_row_bounds.is_none(),
|
||||
"pinned_tabs_row should not exist when only unpinned tabs exist (uses single row)"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_separate_pinned_row_toggles_between_layouts(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
|
||||
let project = Project::test(fs, None, cx).await;
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||
|
||||
let item_a = add_labeled_item(&pane, "A", false, cx);
|
||||
add_labeled_item(&pane, "B", false, cx);
|
||||
|
||||
// Pin one tab
|
||||
pane.update_in(cx, |pane, window, cx| {
|
||||
let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
|
||||
pane.pin_tab_at(ix, window, cx);
|
||||
});
|
||||
|
||||
// Initially disabled - single row
|
||||
let pinned_row_bounds = cx.debug_bounds("pinned_tabs_row");
|
||||
assert!(
|
||||
pinned_row_bounds.is_none(),
|
||||
"Should be single row when disabled"
|
||||
);
|
||||
|
||||
// Enable - two rows
|
||||
set_pinned_tabs_separate_row(cx, true);
|
||||
cx.run_until_parked();
|
||||
let pinned_row_bounds = cx.debug_bounds("pinned_tabs_row");
|
||||
assert!(
|
||||
pinned_row_bounds.is_some(),
|
||||
"Should be two rows when enabled"
|
||||
);
|
||||
|
||||
// Disable again - back to single row
|
||||
set_pinned_tabs_separate_row(cx, false);
|
||||
cx.run_until_parked();
|
||||
let pinned_row_bounds = cx.debug_bounds("pinned_tabs_row");
|
||||
assert!(
|
||||
pinned_row_bounds.is_none(),
|
||||
"Should be single row when disabled again"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_pinning_active_tab_without_position_change_maintains_focus(
|
||||
cx: &mut TestAppContext,
|
||||
@@ -7295,6 +7611,17 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
fn set_pinned_tabs_separate_row(cx: &mut TestAppContext, enabled: bool) {
|
||||
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||
store.update_user_settings(cx, |settings| {
|
||||
settings
|
||||
.tab_bar
|
||||
.get_or_insert_default()
|
||||
.show_pinned_tabs_in_separate_row = Some(enabled);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn add_labeled_item(
|
||||
pane: &Entity<Pane>,
|
||||
label: &str,
|
||||
|
||||
@@ -60,6 +60,7 @@ pub struct TabBarSettings {
|
||||
pub show: bool,
|
||||
pub show_nav_history_buttons: bool,
|
||||
pub show_tab_bar_buttons: bool,
|
||||
pub show_pinned_tabs_in_separate_row: bool,
|
||||
}
|
||||
|
||||
impl Settings for WorkspaceSettings {
|
||||
@@ -121,6 +122,7 @@ impl Settings for TabBarSettings {
|
||||
show: tab_bar.show.unwrap(),
|
||||
show_nav_history_buttons: tab_bar.show_nav_history_buttons.unwrap(),
|
||||
show_tab_bar_buttons: tab_bar.show_tab_bar_buttons.unwrap(),
|
||||
show_pinned_tabs_in_separate_row: tab_bar.show_pinned_tabs_in_separate_row.unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user