tab.rs

  1use std::cmp::Ordering;
  2
  3use gpui::{AnyElement, IntoElement, Stateful};
  4use smallvec::SmallVec;
  5
  6use crate::prelude::*;
  7
  8/// The position of a [`Tab`] within a list of tabs.
  9#[derive(Debug, PartialEq, Eq, Clone, Copy)]
 10pub enum TabPosition {
 11    /// The tab is first in the list.
 12    First,
 13
 14    /// The tab is in the middle of the list (i.e., it is not the first or last tab).
 15    ///
 16    /// The [`Ordering`] is where this tab is positioned with respect to the selected tab.
 17    Middle(Ordering),
 18
 19    /// The tab is last in the list.
 20    Last,
 21}
 22
 23#[derive(Debug, PartialEq, Eq, Clone, Copy)]
 24pub enum TabCloseSide {
 25    Start,
 26    End,
 27}
 28
 29#[derive(IntoElement, RegisterComponent)]
 30pub struct Tab {
 31    div: Stateful<Div>,
 32    selected: bool,
 33    position: TabPosition,
 34    close_side: TabCloseSide,
 35    start_slot: Option<AnyElement>,
 36    end_slot: Option<AnyElement>,
 37    children: SmallVec<[AnyElement; 2]>,
 38}
 39
 40impl Tab {
 41    pub fn new(id: impl Into<ElementId>) -> Self {
 42        let id = id.into();
 43        Self {
 44            div: div()
 45                .id(id.clone())
 46                .debug_selector(|| format!("TAB-{}", id)),
 47            selected: false,
 48            position: TabPosition::First,
 49            close_side: TabCloseSide::End,
 50            start_slot: None,
 51            end_slot: None,
 52            children: SmallVec::new(),
 53        }
 54    }
 55
 56    pub fn position(mut self, position: TabPosition) -> Self {
 57        self.position = position;
 58        self
 59    }
 60
 61    pub fn close_side(mut self, close_side: TabCloseSide) -> Self {
 62        self.close_side = close_side;
 63        self
 64    }
 65
 66    pub fn start_slot<E: IntoElement>(mut self, element: impl Into<Option<E>>) -> Self {
 67        self.start_slot = element.into().map(IntoElement::into_any_element);
 68        self
 69    }
 70
 71    pub fn end_slot<E: IntoElement>(mut self, element: impl Into<Option<E>>) -> Self {
 72        self.end_slot = element.into().map(IntoElement::into_any_element);
 73        self
 74    }
 75
 76    pub fn content_height(cx: &App) -> Pixels {
 77        DynamicSpacing::Base32.px(cx) - px(1.)
 78    }
 79
 80    pub fn container_height(cx: &App) -> Pixels {
 81        DynamicSpacing::Base32.px(cx)
 82    }
 83}
 84
 85impl InteractiveElement for Tab {
 86    fn interactivity(&mut self) -> &mut gpui::Interactivity {
 87        self.div.interactivity()
 88    }
 89}
 90
 91impl StatefulInteractiveElement for Tab {}
 92
 93impl Toggleable for Tab {
 94    fn toggle_state(mut self, selected: bool) -> Self {
 95        self.selected = selected;
 96        self
 97    }
 98}
 99
100impl ParentElement for Tab {
101    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
102        self.children.extend(elements)
103    }
104}
105
106impl RenderOnce for Tab {
107    #[allow(refining_impl_trait)]
108    fn render(self, _: &mut Window, cx: &mut App) -> Stateful<Div> {
109        let (text_color, tab_bg, _tab_hover_bg, _tab_active_bg) = match self.selected {
110            false => (
111                cx.theme().colors().text_muted,
112                cx.theme().colors().tab_inactive_background,
113                cx.theme().colors().ghost_element_hover,
114                cx.theme().colors().ghost_element_active,
115            ),
116            true => (
117                cx.theme().colors().text,
118                cx.theme().colors().tab_active_background,
119                cx.theme().colors().element_hover,
120                cx.theme().colors().element_active,
121            ),
122        };
123
124        let (start_slot, end_slot) = {
125            let start_slot = h_flex().size_3().justify_center().children(self.start_slot);
126
127            let end_slot = h_flex().size_3().justify_center().children(self.end_slot);
128
129            match self.close_side {
130                TabCloseSide::End => (start_slot, end_slot),
131                TabCloseSide::Start => (end_slot, start_slot),
132            }
133        };
134
135        self.div
136            .h(Tab::container_height(cx))
137            .bg(tab_bg)
138            .border_color(cx.theme().colors().border)
139            .map(|this| match self.position {
140                TabPosition::First => {
141                    if self.selected {
142                        this.pl_px().border_r_1().pb_px()
143                    } else {
144                        this.pl_px().pr_px().border_b_1()
145                    }
146                }
147                TabPosition::Last => {
148                    if self.selected {
149                        this.border_l_1().border_r_1().pb_px()
150                    } else {
151                        this.pr_px().pl_px().border_b_1().border_r_1()
152                    }
153                }
154                TabPosition::Middle(Ordering::Equal) => this.border_l_1().border_r_1().pb_px(),
155                TabPosition::Middle(Ordering::Less) => this.border_l_1().pr_px().border_b_1(),
156                TabPosition::Middle(Ordering::Greater) => this.border_r_1().pl_px().border_b_1(),
157            })
158            .cursor_pointer()
159            .child(
160                h_flex()
161                    .group("")
162                    .relative()
163                    .h(Tab::content_height(cx))
164                    .px(DynamicSpacing::Base04.px(cx))
165                    .gap(DynamicSpacing::Base04.rems(cx))
166                    .text_color(text_color)
167                    .child(start_slot)
168                    .children(self.children)
169                    .child(end_slot),
170            )
171    }
172}
173
174impl Component for Tab {
175    fn scope() -> ComponentScope {
176        ComponentScope::None
177    }
178
179    fn description() -> Option<&'static str> {
180        Some(
181            "A tab component that can be used in a tabbed interface, supporting different positions and states.",
182        )
183    }
184
185    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
186        Some(
187            v_flex()
188                .gap_6()
189                .children(vec![example_group_with_title(
190                    "Variations",
191                    vec![
192                        single_example(
193                            "Default",
194                            Tab::new("default").child("Default Tab").into_any_element(),
195                        ),
196                        single_example(
197                            "Selected",
198                            Tab::new("selected")
199                                .toggle_state(true)
200                                .child("Selected Tab")
201                                .into_any_element(),
202                        ),
203                        single_example(
204                            "First",
205                            Tab::new("first")
206                                .position(TabPosition::First)
207                                .child("First Tab")
208                                .into_any_element(),
209                        ),
210                        single_example(
211                            "Middle",
212                            Tab::new("middle")
213                                .position(TabPosition::Middle(Ordering::Equal))
214                                .child("Middle Tab")
215                                .into_any_element(),
216                        ),
217                        single_example(
218                            "Last",
219                            Tab::new("last")
220                                .position(TabPosition::Last)
221                                .child("Last Tab")
222                                .into_any_element(),
223                        ),
224                    ],
225                )])
226                .into_any_element(),
227        )
228    }
229}