tab.rs

  1use std::cmp::Ordering;
  2use std::rc::Rc;
  3
  4use gpui::{AnyElement, AnyView, ClickEvent, IntoElement, MouseButton};
  5use smallvec::SmallVec;
  6
  7use crate::prelude::*;
  8
  9/// The position of a [`Tab`] within a list of tabs.
 10#[derive(Debug, PartialEq, Eq, Clone, Copy)]
 11pub enum TabPosition {
 12    /// The tab is first in the list.
 13    First,
 14
 15    /// The tab is in the middle of the list (i.e., it is not the first or last tab).
 16    ///
 17    /// The [`Ordering`] is where this tab is positioned with respect to the selected tab.
 18    Middle(Ordering),
 19
 20    /// The tab is last in the list.
 21    Last,
 22}
 23
 24#[derive(Debug, PartialEq, Eq, Clone, Copy)]
 25pub enum TabCloseSide {
 26    Start,
 27    End,
 28}
 29
 30#[derive(IntoElement)]
 31pub struct Tab {
 32    id: ElementId,
 33    selected: bool,
 34    position: TabPosition,
 35    close_side: TabCloseSide,
 36    on_click: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
 37    tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView + 'static>>,
 38    start_slot: Option<AnyElement>,
 39    end_slot: Option<AnyElement>,
 40    children: SmallVec<[AnyElement; 2]>,
 41}
 42
 43impl Tab {
 44    pub fn new(id: impl Into<ElementId>) -> Self {
 45        Self {
 46            id: id.into(),
 47            selected: false,
 48            position: TabPosition::First,
 49            close_side: TabCloseSide::End,
 50            on_click: None,
 51            tooltip: None,
 52            start_slot: None,
 53            end_slot: None,
 54            children: SmallVec::new(),
 55        }
 56    }
 57
 58    pub fn position(mut self, position: TabPosition) -> Self {
 59        self.position = position;
 60        self
 61    }
 62
 63    pub fn close_side(mut self, close_side: TabCloseSide) -> Self {
 64        self.close_side = close_side;
 65        self
 66    }
 67
 68    pub fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
 69        self.on_click = Some(Rc::new(handler));
 70        self
 71    }
 72
 73    pub fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
 74        self.tooltip = Some(Box::new(tooltip));
 75        self
 76    }
 77
 78    pub fn start_slot<E: IntoElement>(mut self, element: impl Into<Option<E>>) -> Self {
 79        self.start_slot = element.into().map(IntoElement::into_any_element);
 80        self
 81    }
 82
 83    pub fn end_slot<E: IntoElement>(mut self, element: impl Into<Option<E>>) -> Self {
 84        self.end_slot = element.into().map(IntoElement::into_any_element);
 85        self
 86    }
 87}
 88
 89impl Selectable for Tab {
 90    fn selected(mut self, selected: bool) -> Self {
 91        self.selected = selected;
 92        self
 93    }
 94}
 95
 96impl ParentElement for Tab {
 97    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
 98        &mut self.children
 99    }
100}
101
102impl RenderOnce for Tab {
103    type Rendered = Div;
104
105    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
106        const HEIGHT_IN_REMS: f32 = 30. / 16.;
107
108        let (text_color, tab_bg, _tab_hover_bg, _tab_active_bg) = match self.selected {
109            false => (
110                cx.theme().colors().text_muted,
111                cx.theme().colors().tab_inactive_background,
112                cx.theme().colors().ghost_element_hover,
113                cx.theme().colors().ghost_element_active,
114            ),
115            true => (
116                cx.theme().colors().text,
117                cx.theme().colors().tab_active_background,
118                cx.theme().colors().element_hover,
119                cx.theme().colors().element_active,
120            ),
121        };
122
123        div()
124            .h(rems(HEIGHT_IN_REMS))
125            .bg(tab_bg)
126            .border_color(cx.theme().colors().border)
127            .map(|this| match self.position {
128                TabPosition::First => {
129                    if self.selected {
130                        this.pl_px().border_r().pb_px()
131                    } else {
132                        this.pl_px().pr_px().border_b()
133                    }
134                }
135                TabPosition::Last => {
136                    if self.selected {
137                        this.border_l().border_r().pb_px()
138                    } else {
139                        this.pr_px().pl_px().border_b()
140                    }
141                }
142                TabPosition::Middle(Ordering::Equal) => this.border_l().border_r().pb_px(),
143                TabPosition::Middle(Ordering::Less) => this.border_l().pr_px().border_b(),
144                TabPosition::Middle(Ordering::Greater) => this.border_r().pl_px().border_b(),
145            })
146            .child(
147                h_stack()
148                    .group("")
149                    .id(self.id)
150                    .relative()
151                    .h_full()
152                    .px_5()
153                    .gap_1()
154                    .text_color(text_color)
155                    // .hover(|style| style.bg(tab_hover_bg))
156                    // .active(|style| style.bg(tab_active_bg))
157                    .when_some(self.on_click, |tab, on_click| {
158                        tab.cursor_pointer().on_click(move |event, cx| {
159                            // HACK: GPUI currently fires `on_click` with any mouse button,
160                            // but we only care about the left button.
161                            if event.down.button == MouseButton::Left {
162                                (on_click)(event, cx)
163                            }
164                        })
165                    })
166                    .when_some(self.tooltip, |tab, tooltip| {
167                        tab.tooltip(move |cx| tooltip(cx))
168                    })
169                    .child(
170                        h_stack()
171                            .w_3()
172                            .h_3()
173                            .justify_center()
174                            .absolute()
175                            .map(|this| match self.close_side {
176                                TabCloseSide::Start => this.right_1(),
177                                TabCloseSide::End => this.left_1(),
178                            })
179                            .children(self.start_slot),
180                    )
181                    .child(
182                        h_stack()
183                            .invisible()
184                            .w_3()
185                            .h_3()
186                            .justify_center()
187                            .absolute()
188                            .map(|this| match self.close_side {
189                                TabCloseSide::Start => this.left_1(),
190                                TabCloseSide::End => this.right_1(),
191                            })
192                            .group_hover("", |style| style.visible())
193                            .children(self.end_slot),
194                    )
195                    .children(self.children),
196            )
197    }
198}