Detailed changes
@@ -28,6 +28,7 @@ pub enum ComponentStory {
ListHeader,
ListItem,
Scroll,
+ Tab,
Text,
ZIndex,
Picker,
@@ -53,6 +54,7 @@ impl ComponentStory {
Self::ListItem => cx.build_view(|_| ui::ListItemStory).into(),
Self::Scroll => ScrollStory::view(cx).into(),
Self::Text => TextStory::view(cx).into(),
+ Self::Tab => cx.build_view(|_| ui::TabStory).into(),
Self::ZIndex => cx.build_view(|_| ZIndexStory).into(),
Self::Picker => PickerStory::new(cx).into(),
}
@@ -13,6 +13,7 @@ mod popover;
mod popover_menu;
mod right_click_menu;
mod stack;
+mod tab;
mod tooltip;
#[cfg(feature = "stories")]
@@ -33,6 +34,7 @@ pub use popover::*;
pub use popover_menu::*;
pub use right_click_menu::*;
pub use stack::*;
+pub use tab::*;
pub use tooltip::*;
#[cfg(feature = "stories")]
@@ -10,6 +10,7 @@ mod label;
mod list;
mod list_header;
mod list_item;
+mod tab;
pub use avatar::*;
pub use button::*;
@@ -23,3 +24,4 @@ pub use label::*;
pub use list::*;
pub use list_header::*;
pub use list_item::*;
+pub use tab::*;
@@ -0,0 +1,114 @@
+use std::cmp::Ordering;
+
+use gpui::{Div, Render};
+use story::Story;
+
+use crate::{prelude::*, TabPosition};
+use crate::{Indicator, Tab};
+
+pub struct TabStory;
+
+impl Render for TabStory {
+ type Element = Div;
+
+ fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+ Story::container()
+ .child(Story::title_for::<Tab>())
+ .child(Story::label("Default"))
+ .child(h_stack().child(Tab::new("tab_1").child("Tab 1")))
+ .child(Story::label("With indicator"))
+ .child(
+ h_stack().child(
+ Tab::new("tab_1")
+ .start_slot(Indicator::dot().color(Color::Warning))
+ .child("Tab 1"),
+ ),
+ )
+ .child(Story::label("With close button"))
+ .child(
+ h_stack().child(
+ Tab::new("tab_1")
+ .end_slot(
+ IconButton::new("close_button", Icon::Close)
+ .icon_color(Color::Muted)
+ .size(ButtonSize::None)
+ .icon_size(IconSize::XSmall),
+ )
+ .child("Tab 1"),
+ ),
+ )
+ .child(Story::label("List of tabs"))
+ .child(
+ h_stack()
+ .child(Tab::new("tab_1").child("Tab 1"))
+ .child(Tab::new("tab_2").child("Tab 2")),
+ )
+ .child(Story::label("List of tabs with first tab selected"))
+ .child(
+ h_stack()
+ .child(
+ Tab::new("tab_1")
+ .selected(true)
+ .position(TabPosition::First)
+ .child("Tab 1"),
+ )
+ .child(
+ Tab::new("tab_2")
+ .position(TabPosition::Middle(Ordering::Greater))
+ .child("Tab 2"),
+ )
+ .child(
+ Tab::new("tab_3")
+ .position(TabPosition::Middle(Ordering::Greater))
+ .child("Tab 3"),
+ )
+ .child(Tab::new("tab_4").position(TabPosition::Last).child("Tab 4")),
+ )
+ .child(Story::label("List of tabs with last tab selected"))
+ .child(
+ h_stack()
+ .child(
+ Tab::new("tab_1")
+ .position(TabPosition::First)
+ .child("Tab 1"),
+ )
+ .child(
+ Tab::new("tab_2")
+ .position(TabPosition::Middle(Ordering::Less))
+ .child("Tab 2"),
+ )
+ .child(
+ Tab::new("tab_3")
+ .position(TabPosition::Middle(Ordering::Less))
+ .child("Tab 3"),
+ )
+ .child(
+ Tab::new("tab_4")
+ .position(TabPosition::Last)
+ .selected(true)
+ .child("Tab 4"),
+ ),
+ )
+ .child(Story::label("List of tabs with second tab selected"))
+ .child(
+ h_stack()
+ .child(
+ Tab::new("tab_1")
+ .position(TabPosition::First)
+ .child("Tab 1"),
+ )
+ .child(
+ Tab::new("tab_2")
+ .position(TabPosition::Middle(Ordering::Equal))
+ .selected(true)
+ .child("Tab 2"),
+ )
+ .child(
+ Tab::new("tab_3")
+ .position(TabPosition::Middle(Ordering::Greater))
+ .child("Tab 3"),
+ )
+ .child(Tab::new("tab_4").position(TabPosition::Last).child("Tab 4")),
+ )
+ }
+}
@@ -0,0 +1,198 @@
+use std::cmp::Ordering;
+use std::rc::Rc;
+
+use gpui::{AnyElement, AnyView, ClickEvent, IntoElement, MouseButton};
+use smallvec::SmallVec;
+
+use crate::prelude::*;
+
+/// The position of a [`Tab`] within a list of tabs.
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub enum TabPosition {
+ /// The tab is first in the list.
+ First,
+
+ /// The tab is in the middle of the list (i.e., it is not the first or last tab).
+ ///
+ /// The [`Ordering`] is where this tab is positioned with respect to the selected tab.
+ Middle(Ordering),
+
+ /// The tab is last in the list.
+ Last,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub enum TabCloseSide {
+ Start,
+ End,
+}
+
+#[derive(IntoElement)]
+pub struct Tab {
+ id: ElementId,
+ selected: bool,
+ position: TabPosition,
+ close_side: TabCloseSide,
+ on_click: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
+ tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView + 'static>>,
+ start_slot: Option<AnyElement>,
+ end_slot: Option<AnyElement>,
+ children: SmallVec<[AnyElement; 2]>,
+}
+
+impl Tab {
+ pub fn new(id: impl Into<ElementId>) -> Self {
+ Self {
+ id: id.into(),
+ selected: false,
+ position: TabPosition::First,
+ close_side: TabCloseSide::End,
+ on_click: None,
+ tooltip: None,
+ start_slot: None,
+ end_slot: None,
+ children: SmallVec::new(),
+ }
+ }
+
+ pub fn position(mut self, position: TabPosition) -> Self {
+ self.position = position;
+ self
+ }
+
+ pub fn close_side(mut self, close_side: TabCloseSide) -> Self {
+ self.close_side = close_side;
+ self
+ }
+
+ pub fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
+ self.on_click = Some(Rc::new(handler));
+ self
+ }
+
+ pub fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
+ self.tooltip = Some(Box::new(tooltip));
+ self
+ }
+
+ pub fn start_slot<E: IntoElement>(mut self, element: impl Into<Option<E>>) -> Self {
+ self.start_slot = element.into().map(IntoElement::into_any_element);
+ self
+ }
+
+ pub fn end_slot<E: IntoElement>(mut self, element: impl Into<Option<E>>) -> Self {
+ self.end_slot = element.into().map(IntoElement::into_any_element);
+ self
+ }
+}
+
+impl Selectable for Tab {
+ fn selected(mut self, selected: bool) -> Self {
+ self.selected = selected;
+ self
+ }
+}
+
+impl ParentElement for Tab {
+ fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
+ &mut self.children
+ }
+}
+
+impl RenderOnce for Tab {
+ type Rendered = Div;
+
+ fn render(self, cx: &mut WindowContext) -> Self::Rendered {
+ const HEIGHT_IN_REMS: f32 = 30. / 16.;
+
+ let (text_color, tab_bg, _tab_hover_bg, _tab_active_bg) = match self.selected {
+ false => (
+ cx.theme().colors().text_muted,
+ cx.theme().colors().tab_inactive_background,
+ cx.theme().colors().ghost_element_hover,
+ cx.theme().colors().ghost_element_active,
+ ),
+ true => (
+ cx.theme().colors().text,
+ cx.theme().colors().tab_active_background,
+ cx.theme().colors().element_hover,
+ cx.theme().colors().element_active,
+ ),
+ };
+
+ div()
+ .h(rems(HEIGHT_IN_REMS))
+ .bg(tab_bg)
+ .border_color(cx.theme().colors().border)
+ .map(|this| match self.position {
+ TabPosition::First => {
+ if self.selected {
+ this.pl_px().border_r().pb_px()
+ } else {
+ this.pl_px().pr_px().border_b()
+ }
+ }
+ TabPosition::Last => {
+ if self.selected {
+ this.border_l().border_r().pb_px()
+ } else {
+ this.pr_px().pl_px().border_b()
+ }
+ }
+ TabPosition::Middle(Ordering::Equal) => this.border_l().border_r().pb_px(),
+ TabPosition::Middle(Ordering::Less) => this.border_l().pr_px().border_b(),
+ TabPosition::Middle(Ordering::Greater) => this.border_r().pl_px().border_b(),
+ })
+ .child(
+ h_stack()
+ .group("")
+ .id(self.id)
+ .relative()
+ .h_full()
+ .px_5()
+ .gap_1()
+ .text_color(text_color)
+ // .hover(|style| style.bg(tab_hover_bg))
+ // .active(|style| style.bg(tab_active_bg))
+ .when_some(self.on_click, |tab, on_click| {
+ tab.cursor_pointer().on_click(move |event, cx| {
+ // HACK: GPUI currently fires `on_click` with any mouse button,
+ // but we only care about the left button.
+ if event.down.button == MouseButton::Left {
+ (on_click)(event, cx)
+ }
+ })
+ })
+ .when_some(self.tooltip, |tab, tooltip| {
+ tab.tooltip(move |cx| tooltip(cx))
+ })
+ .child(
+ h_stack()
+ .w_3()
+ .h_3()
+ .justify_center()
+ .absolute()
+ .map(|this| match self.close_side {
+ TabCloseSide::Start => this.right_1(),
+ TabCloseSide::End => this.left_1(),
+ })
+ .children(self.start_slot),
+ )
+ .child(
+ h_stack()
+ .invisible()
+ .w_3()
+ .h_3()
+ .justify_center()
+ .absolute()
+ .map(|this| match self.close_side {
+ TabCloseSide::Start => this.left_1(),
+ TabCloseSide::End => this.right_1(),
+ })
+ .group_hover("", |style| style.visible())
+ .children(self.end_slot),
+ )
+ .children(self.children),
+ )
+ }
+}
@@ -28,10 +28,10 @@ use std::{
use ui::{
h_stack, prelude::*, right_click_menu, ButtonSize, Color, Icon, IconButton, IconSize,
- Indicator, Label, Tooltip,
+ Indicator, Label, Tab, TabPosition, Tooltip,
};
use ui::{v_stack, ContextMenu};
-use util::truncate_and_remove_front;
+use util::{maybe, truncate_and_remove_front};
#[derive(PartialEq, Clone, Copy, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
@@ -1438,42 +1438,49 @@ impl Pane {
let is_active = ix == self.active_item_index;
- let indicator = {
+ let indicator = maybe!({
let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
- (true, _) => Some(Color::Warning),
- (_, true) => Some(Color::Accent),
- (false, false) => None,
+ (true, _) => Color::Warning,
+ (_, true) => Color::Accent,
+ (false, false) => return None,
};
- h_stack()
- .w_3()
- .h_3()
- .justify_center()
- .absolute()
- .map(|this| match close_side {
- ClosePosition::Left => this.right_1(),
- ClosePosition::Right => this.left_1(),
+ Some(Indicator::dot().color(indicator_color))
+ });
+
+ let id = item.item_id();
+
+ let is_first_item = ix == 0;
+ let is_last_item = ix == self.items.len() - 1;
+ let position_relative_to_active_item = ix.cmp(&self.active_item_index);
+
+ let tab =
+ Tab::new(ix)
+ .position(if is_first_item {
+ TabPosition::First
+ } else if is_last_item {
+ TabPosition::Last
+ } else {
+ TabPosition::Middle(position_relative_to_active_item)
})
- .when_some(indicator_color, |this, indicator_color| {
- this.child(Indicator::dot().color(indicator_color))
+ .close_side(match close_side {
+ ClosePosition::Left => ui::TabCloseSide::Start,
+ ClosePosition::Right => ui::TabCloseSide::End,
})
- };
-
- let close_button = {
- let id = item.item_id();
-
- h_stack()
- .invisible()
- .w_3()
- .h_3()
- .justify_center()
- .absolute()
- .map(|this| match close_side {
- ClosePosition::Left => this.left_1(),
- ClosePosition::Right => this.right_1(),
+ .selected(ix == self.active_item_index())
+ .on_click(cx.listener(move |pane: &mut Self, event, cx| {
+ pane.activate_item(ix, true, true, cx)
+ }))
+ // .on_drag(move |pane, cx| pane.render_tab(ix, item.boxed_clone(), detail, cx))
+ // .drag_over::<DraggedTab>(|d| d.bg(cx.theme().colors().element_drop_target))
+ // .on_drop(|_view, state: View<DraggedTab>, cx| {
+ // eprintln!("{:?}", state.read(cx));
+ // })
+ .when_some(item.tab_tooltip_text(cx), |tab, text| {
+ tab.tooltip(move |cx| Tooltip::text(text.clone(), cx))
})
- .group_hover("", |style| style.visible())
- .child(
+ .start_slot::<Indicator>(indicator)
+ .end_slot(
// TODO: Fix button size
IconButton::new("close tab", Icon::Close)
.icon_color(Color::Muted)
@@ -1484,67 +1491,7 @@ impl Pane {
.detach_and_log_err(cx);
})),
)
- };
-
- let tab = div()
- .border_color(cx.theme().colors().border)
- .bg(tab_bg)
- // 30px @ 16px/rem
- .h(rems(1.875))
- .map(|this| {
- let is_first_item = ix == 0;
- let is_last_item = ix == self.items.len() - 1;
- match ix.cmp(&self.active_item_index) {
- cmp::Ordering::Less => {
- if is_first_item {
- this.pl_px().pr_px().border_b()
- } else {
- this.border_l().pr_px().border_b()
- }
- }
- cmp::Ordering::Greater => {
- if is_last_item {
- this.pr_px().pl_px().border_b()
- } else {
- this.border_r().pl_px().border_b()
- }
- }
- cmp::Ordering::Equal => {
- if is_first_item {
- this.pl_px().border_r().pb_px()
- } else {
- this.border_l().border_r().pb_px()
- }
- }
- }
- })
- .child(
- h_stack()
- .group("")
- .id(ix)
- .relative()
- .h_full()
- .cursor_pointer()
- .when_some(item.tab_tooltip_text(cx), |div, text| {
- div.tooltip(move |cx| cx.build_view(|cx| Tooltip::new(text.clone())).into())
- })
- .on_click(
- cx.listener(move |v: &mut Self, e, cx| v.activate_item(ix, true, true, cx)),
- )
- // .on_drag(move |pane, cx| pane.render_tab(ix, item.boxed_clone(), detail, cx))
- // .drag_over::<DraggedTab>(|d| d.bg(cx.theme().colors().element_drop_target))
- // .on_drop(|_view, state: View<DraggedTab>, cx| {
- // eprintln!("{:?}", state.read(cx));
- // })
- .px_5()
- // .hover(|h| h.bg(tab_hover_bg))
- // .active(|a| a.bg(tab_active_bg))
- .gap_1()
- .text_color(text_color)
- .child(indicator)
- .child(close_button)
- .child(label),
- );
+ .child(label);
right_click_menu(ix).trigger(tab).menu(|cx| {
ContextMenu::build(cx, |menu, cx| {