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