tab.rs

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