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()
126                .size(px(12.)) // use px over rem from size_3
127                .justify_center()
128                .children(self.start_slot);
129
130            let end_slot = h_flex()
131                .size(px(12.)) // use px over rem from size_3
132                .justify_center()
133                .children(self.end_slot);
134
135            match self.close_side {
136                TabCloseSide::End => (start_slot, end_slot),
137                TabCloseSide::Start => (end_slot, start_slot),
138            }
139        };
140
141        self.div
142            .h(Tab::container_height(cx))
143            .bg(tab_bg)
144            .border_color(cx.theme().colors().border)
145            .map(|this| match self.position {
146                TabPosition::First => {
147                    if self.selected {
148                        this.pl_px().border_r_1().pb_px()
149                    } else {
150                        this.pl_px().pr_px().border_b_1()
151                    }
152                }
153                TabPosition::Last => {
154                    if self.selected {
155                        this.border_l_1().border_r_1().pb_px()
156                    } else {
157                        this.pl_px().border_b_1().border_r_1()
158                    }
159                }
160                TabPosition::Middle(Ordering::Equal) => this.border_l_1().border_r_1().pb_px(),
161                TabPosition::Middle(Ordering::Less) => this.border_l_1().pr_px().border_b_1(),
162                TabPosition::Middle(Ordering::Greater) => this.border_r_1().pl_px().border_b_1(),
163            })
164            .cursor_pointer()
165            .child(
166                h_flex()
167                    .group("")
168                    .relative()
169                    .h(Tab::content_height(cx))
170                    .px(DynamicSpacing::Base04.px(cx))
171                    .gap(DynamicSpacing::Base04.rems(cx))
172                    .text_color(text_color)
173                    .child(start_slot)
174                    .children(self.children)
175                    .child(end_slot),
176            )
177    }
178}
179
180impl Component for Tab {
181    fn scope() -> ComponentScope {
182        ComponentScope::Navigation
183    }
184
185    fn description() -> Option<&'static str> {
186        Some(
187            "A tab component that can be used in a tabbed interface, supporting different positions and states.",
188        )
189    }
190
191    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
192        Some(
193            v_flex()
194                .gap_6()
195                .children(vec![example_group_with_title(
196                    "Variations",
197                    vec![
198                        single_example(
199                            "Default",
200                            Tab::new("default").child("Default Tab").into_any_element(),
201                        ),
202                        single_example(
203                            "Selected",
204                            Tab::new("selected")
205                                .toggle_state(true)
206                                .child("Selected Tab")
207                                .into_any_element(),
208                        ),
209                        single_example(
210                            "First",
211                            Tab::new("first")
212                                .position(TabPosition::First)
213                                .child("First Tab")
214                                .into_any_element(),
215                        ),
216                        single_example(
217                            "Middle",
218                            Tab::new("middle")
219                                .position(TabPosition::Middle(Ordering::Equal))
220                                .child("Middle Tab")
221                                .into_any_element(),
222                        ),
223                        single_example(
224                            "Last",
225                            Tab::new("last")
226                                .position(TabPosition::Last)
227                                .child("Last Tab")
228                                .into_any_element(),
229                        ),
230                    ],
231                )])
232                .into_any_element(),
233        )
234    }
235}