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::menu::{DropdownMenu, PopupMenu};
|
||||||
use gpui_component::tab::{Tab, TabBar};
|
use gpui_component::tab::{Tab, TabBar};
|
||||||
use gpui_component::{
|
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::{
|
use super::{
|
||||||
@@ -355,6 +356,22 @@ impl TabPanel {
|
|||||||
cx.notify();
|
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
|
/// Queue one reconcile task per frame that notifies panels of their
|
||||||
/// frame-end net active state. Using a spawned task (not `defer`) is what
|
/// frame-end net active state. Using a spawned task (not `defer`) is what
|
||||||
/// guarantees the task runs after every same-frame mutation, including
|
/// 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(
|
fn render_dock_toggle_button(
|
||||||
&self,
|
&self,
|
||||||
placement: DockPlacement,
|
placement: DockPlacement,
|
||||||
@@ -770,7 +823,6 @@ impl TabPanel {
|
|||||||
let view = cx.entity().clone();
|
let view = cx.entity().clone();
|
||||||
|
|
||||||
let left_dock_button = self.render_dock_toggle_button(DockPlacement::Left, window, cx);
|
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
|
// macOS: the traffic lights overlay the window's top-left corner. The
|
||||||
// left dock (sidebar) normally clears them; when it is closed or
|
// left dock (sidebar) normally clears them; when it is closed or
|
||||||
@@ -851,23 +903,17 @@ impl TabPanel {
|
|||||||
.pill()
|
.pill()
|
||||||
.small()
|
.small()
|
||||||
.track_scroll(&self.tab_bar_scroll_handle)
|
.track_scroll(&self.tab_bar_scroll_handle)
|
||||||
.when(
|
.prefix(
|
||||||
has_extend_dock_button || needs_traffic_light_padding,
|
h_flex()
|
||||||
|this| {
|
.items_center()
|
||||||
this.prefix(
|
.top_0()
|
||||||
h_flex()
|
// Right -1 for avoid border overlap with the first tab
|
||||||
.items_center()
|
.right(-px(1.))
|
||||||
.top_0()
|
.h_full()
|
||||||
// Right -1 for avoid border overlap with the first tab
|
.px_2()
|
||||||
.right(-px(1.))
|
.when(needs_traffic_light_padding, |this| this.pl(px(80.)))
|
||||||
.h_full()
|
.children(left_dock_button)
|
||||||
.px_2()
|
.child(self.render_prev_next_tab_buttons(cx)),
|
||||||
.when(needs_traffic_light_padding, |this| {
|
|
||||||
this.pl(px(80.))
|
|
||||||
})
|
|
||||||
.children(left_dock_button),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
.children(self.panels.iter().enumerate().filter_map(|(ix, panel)| {
|
.children(self.panels.iter().enumerate().filter_map(|(ix, panel)| {
|
||||||
let mut active = state.active_panel.as_ref() == Some(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)]);
|
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]
|
#[gpui::test]
|
||||||
fn removing_before_active_keeps_displayed_panel(cx: &mut TestAppContext) {
|
fn removing_before_active_keeps_displayed_panel(cx: &mut TestAppContext) {
|
||||||
let fixture = setup(cx);
|
let fixture = setup(cx);
|
||||||
|
|||||||
Reference in New Issue
Block a user