update
This commit is contained in:
+163
-19
@@ -18,7 +18,8 @@ use gpui_component::button::{Button, ButtonVariants as _};
|
||||
use gpui_component::menu::{DropdownMenu, PopupMenu};
|
||||
use gpui_component::tab::{Tab, TabBar};
|
||||
use gpui_component::{
|
||||
ActiveTheme, AxisExt, ElementExt, IconName, Placement, Selectable, Sizable, h_flex, v_flex,
|
||||
ActiveTheme, AxisExt, Disableable, ElementExt, IconName, Placement, Selectable, Sizable,
|
||||
h_flex, v_flex,
|
||||
};
|
||||
|
||||
use super::{
|
||||
@@ -355,6 +356,22 @@ impl TabPanel {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Switch the active tab to the previous or next tab in the strip.
|
||||
///
|
||||
/// Same as clicking that tab; the tab order is unchanged.
|
||||
fn activate_relative_tab(&mut self, delta: isize, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.collapsed || self.panels.len() < 2 {
|
||||
return;
|
||||
}
|
||||
|
||||
let target = self.active_ix as isize + delta;
|
||||
if !(0..self.panels.len() as isize).contains(&target) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.set_active_ix(target as usize, window, cx);
|
||||
}
|
||||
|
||||
/// Queue one reconcile task per frame that notifies panels of their
|
||||
/// frame-end net active state. Using a spawned task (not `defer`) is what
|
||||
/// guarantees the task runs after every same-frame mutation, including
|
||||
@@ -702,6 +719,42 @@ impl TabPanel {
|
||||
)
|
||||
}
|
||||
|
||||
/// The previous/next tab buttons shown in the tab bar's leading prefix.
|
||||
///
|
||||
/// Unlike the dock toggle button they always render, but are disabled at
|
||||
/// the ends of the tab strip (or while the panel is collapsed).
|
||||
fn render_prev_next_tab_buttons(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let prev_enabled = !self.collapsed && self.active_ix > 0;
|
||||
let next_enabled = !self.collapsed && self.active_ix + 1 < self.panels.len();
|
||||
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Button::new("tab:prev")
|
||||
.icon(IconName::ChevronLeft)
|
||||
.small()
|
||||
.ghost()
|
||||
.tab_stop(false)
|
||||
.tooltip("Previous tab")
|
||||
.disabled(!prev_enabled)
|
||||
.on_click(cx.listener(|view, _, window, cx| {
|
||||
view.activate_relative_tab(-1, window, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("tab:next")
|
||||
.icon(IconName::ChevronRight)
|
||||
.small()
|
||||
.ghost()
|
||||
.tab_stop(false)
|
||||
.tooltip("Next tab")
|
||||
.disabled(!next_enabled)
|
||||
.on_click(cx.listener(|view, _, window, cx| {
|
||||
view.activate_relative_tab(1, window, cx);
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_dock_toggle_button(
|
||||
&self,
|
||||
placement: DockPlacement,
|
||||
@@ -770,7 +823,6 @@ impl TabPanel {
|
||||
let view = cx.entity().clone();
|
||||
|
||||
let left_dock_button = self.render_dock_toggle_button(DockPlacement::Left, window, cx);
|
||||
let has_extend_dock_button = left_dock_button.is_some();
|
||||
|
||||
// macOS: the traffic lights overlay the window's top-left corner. The
|
||||
// left dock (sidebar) normally clears them; when it is closed or
|
||||
@@ -851,23 +903,17 @@ impl TabPanel {
|
||||
.pill()
|
||||
.small()
|
||||
.track_scroll(&self.tab_bar_scroll_handle)
|
||||
.when(
|
||||
has_extend_dock_button || needs_traffic_light_padding,
|
||||
|this| {
|
||||
this.prefix(
|
||||
h_flex()
|
||||
.items_center()
|
||||
.top_0()
|
||||
// Right -1 for avoid border overlap with the first tab
|
||||
.right(-px(1.))
|
||||
.h_full()
|
||||
.px_2()
|
||||
.when(needs_traffic_light_padding, |this| {
|
||||
this.pl(px(80.))
|
||||
})
|
||||
.children(left_dock_button),
|
||||
)
|
||||
},
|
||||
.prefix(
|
||||
h_flex()
|
||||
.items_center()
|
||||
.top_0()
|
||||
// Right -1 for avoid border overlap with the first tab
|
||||
.right(-px(1.))
|
||||
.h_full()
|
||||
.px_2()
|
||||
.when(needs_traffic_light_padding, |this| this.pl(px(80.)))
|
||||
.children(left_dock_button)
|
||||
.child(self.render_prev_next_tab_buttons(cx)),
|
||||
)
|
||||
.children(self.panels.iter().enumerate().filter_map(|(ix, panel)| {
|
||||
let mut active = state.active_panel.as_ref() == Some(panel);
|
||||
@@ -1956,6 +2002,104 @@ mod tests {
|
||||
assert_eq!(drain(&fixture.log), [("A", false), ("C", true)]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn prev_next_buttons_switch_the_active_tab(cx: &mut TestAppContext) {
|
||||
let fixture = setup(cx);
|
||||
let mut cx = VisualTestContext::from_window(fixture.window.into(), cx);
|
||||
|
||||
let (_keep, tab_panel, panels) = build_tabs(&fixture, &["A", "B", "C"], Some(0), &mut cx);
|
||||
cx.run_until_parked();
|
||||
drain(&fixture.log);
|
||||
|
||||
let order = |cx: &App| {
|
||||
tab_panel
|
||||
.read(cx)
|
||||
.panels
|
||||
.iter()
|
||||
.map(|p| p.view().entity_id())
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
// Next: activate B (index 1).
|
||||
cx.update(|window, cx| {
|
||||
tab_panel.update(cx, |tab_panel, cx| {
|
||||
tab_panel.activate_relative_tab(1, window, cx)
|
||||
})
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(cx.read(|cx| tab_panel.read(cx).active_ix), 1);
|
||||
assert_eq!(
|
||||
cx.read(|cx| order(cx)),
|
||||
vec![
|
||||
panels[0].entity_id(),
|
||||
panels[1].entity_id(),
|
||||
panels[2].entity_id()
|
||||
],
|
||||
"switching must not reorder the tabs"
|
||||
);
|
||||
assert_eq!(drain(&fixture.log), [("A", false), ("B", true)]);
|
||||
|
||||
// Next: activate C (index 2).
|
||||
cx.update(|window, cx| {
|
||||
tab_panel.update(cx, |tab_panel, cx| {
|
||||
tab_panel.activate_relative_tab(1, window, cx)
|
||||
})
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(cx.read(|cx| tab_panel.read(cx).active_ix), 2);
|
||||
assert_eq!(drain(&fixture.log), [("B", false), ("C", true)]);
|
||||
|
||||
// Past the end is a no-op.
|
||||
cx.update(|window, cx| {
|
||||
tab_panel.update(cx, |tab_panel, cx| {
|
||||
tab_panel.activate_relative_tab(1, window, cx)
|
||||
})
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(cx.read(|cx| tab_panel.read(cx).active_ix), 2);
|
||||
assert_eq!(drain(&fixture.log), []);
|
||||
|
||||
// Prev: back to B, then to A.
|
||||
cx.update(|window, cx| {
|
||||
tab_panel.update(cx, |tab_panel, cx| {
|
||||
tab_panel.activate_relative_tab(-1, window, cx)
|
||||
})
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(cx.read(|cx| tab_panel.read(cx).active_ix), 1);
|
||||
assert_eq!(drain(&fixture.log), [("C", false), ("B", true)]);
|
||||
|
||||
cx.update(|window, cx| {
|
||||
tab_panel.update(cx, |tab_panel, cx| {
|
||||
tab_panel.activate_relative_tab(-1, window, cx)
|
||||
})
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(cx.read(|cx| tab_panel.read(cx).active_ix), 0);
|
||||
assert_eq!(drain(&fixture.log), [("B", false), ("A", true)]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn prev_next_buttons_are_a_noop_for_a_single_tab(cx: &mut TestAppContext) {
|
||||
let fixture = setup(cx);
|
||||
let mut cx = VisualTestContext::from_window(fixture.window.into(), cx);
|
||||
|
||||
let (_keep, tab_panel, _) = build_tabs(&fixture, &["A"], None, &mut cx);
|
||||
cx.run_until_parked();
|
||||
drain(&fixture.log);
|
||||
|
||||
cx.update(|window, cx| {
|
||||
tab_panel.update(cx, |tab_panel, cx| {
|
||||
tab_panel.activate_relative_tab(1, window, cx);
|
||||
tab_panel.activate_relative_tab(-1, window, cx);
|
||||
})
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
assert_eq!(cx.read(|cx| tab_panel.read(cx).active_ix), 0);
|
||||
assert_eq!(drain(&fixture.log), []);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn removing_before_active_keeps_displayed_panel(cx: &mut TestAppContext) {
|
||||
let fixture = setup(cx);
|
||||
|
||||
Reference in New Issue
Block a user