tab.rs

  1#![allow(missing_docs)]
  2use std::cmp::Ordering;
  3
  4use gpui::{AnyElement, IntoElement, Stateful};
  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, IntoComponent)]
 31pub struct Tab {
 32    div: Stateful<Div>,
 33    selected: bool,
 34    position: TabPosition,
 35    close_side: TabCloseSide,
 36    start_slot: Option<AnyElement>,
 37    end_slot: Option<AnyElement>,
 38    children: SmallVec<[AnyElement; 2]>,
 39}
 40
 41impl Tab {
 42    pub fn new(id: impl Into<ElementId>) -> Self {
 43        let id = id.into();
 44        Self {
 45            div: div()
 46                .id(id.clone())
 47                .debug_selector(|| format!("TAB-{}", id)),
 48            selected: false,
 49            position: TabPosition::First,
 50            close_side: TabCloseSide::End,
 51            start_slot: None,
 52            end_slot: None,
 53            children: SmallVec::new(),
 54        }
 55    }
 56
 57    pub fn position(mut self, position: TabPosition) -> Self {
 58        self.position = position;
 59        self
 60    }
 61
 62    pub fn close_side(mut self, close_side: TabCloseSide) -> Self {
 63        self.close_side = close_side;
 64        self
 65    }
 66
 67    pub fn start_slot<E: IntoElement>(mut self, element: impl Into<Option<E>>) -> Self {
 68        self.start_slot = element.into().map(IntoElement::into_any_element);
 69        self
 70    }
 71
 72    pub fn end_slot<E: IntoElement>(mut self, element: impl Into<Option<E>>) -> Self {
 73        self.end_slot = element.into().map(IntoElement::into_any_element);
 74        self
 75    }
 76
 77    pub fn content_height(cx: &mut App) -> Pixels {
 78        DynamicSpacing::Base32.px(cx) - px(1.)
 79    }
 80
 81    pub fn container_height(cx: &mut App) -> Pixels {
 82        DynamicSpacing::Base32.px(cx)
 83    }
 84}
 85
 86impl InteractiveElement for Tab {
 87    fn interactivity(&mut self) -> &mut gpui::Interactivity {
 88        self.div.interactivity()
 89    }
 90}
 91
 92impl StatefulInteractiveElement for Tab {}
 93
 94impl Toggleable for Tab {
 95    fn toggle_state(mut self, selected: bool) -> Self {
 96        self.selected = selected;
 97        self
 98    }
 99}
100
101impl ParentElement for Tab {
102    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
103        self.children.extend(elements)
104    }
105}
106
107impl RenderOnce for Tab {
108    #[allow(refining_impl_trait)]
109    fn render(self, _: &mut Window, cx: &mut App) -> Stateful<Div> {
110        let (text_color, tab_bg, _tab_hover_bg, _tab_active_bg) = match self.selected {
111            false => (
112                cx.theme().colors().text_muted,
113                cx.theme().colors().tab_inactive_background,
114                cx.theme().colors().ghost_element_hover,
115                cx.theme().colors().ghost_element_active,
116            ),
117            true => (
118                cx.theme().colors().text,
119                cx.theme().colors().tab_active_background,
120                cx.theme().colors().element_hover,
121                cx.theme().colors().element_active,
122            ),
123        };
124
125        let (start_slot, end_slot) = {
126            let start_slot = h_flex().size_3().justify_center().children(self.start_slot);
127
128            let end_slot = h_flex().size_3().justify_center().children(self.end_slot);
129
130            match self.close_side {
131                TabCloseSide::End => (start_slot, end_slot),
132                TabCloseSide::Start => (end_slot, start_slot),
133            }
134        };
135
136        self.div
137            .h(Tab::container_height(cx))
138            .bg(tab_bg)
139            .border_color(cx.theme().colors().border)
140            .map(|this| match self.position {
141                TabPosition::First => {
142                    if self.selected {
143                        this.pl_px().border_r_1().pb_px()
144                    } else {
145                        this.pl_px().pr_px().border_b_1()
146                    }
147                }
148                TabPosition::Last => {
149                    if self.selected {
150                        this.border_l_1().border_r_1().pb_px()
151                    } else {
152                        this.pr_px().pl_px().border_b_1().border_r_1()
153                    }
154                }
155                TabPosition::Middle(Ordering::Equal) => this.border_l_1().border_r_1().pb_px(),
156                TabPosition::Middle(Ordering::Less) => this.border_l_1().pr_px().border_b_1(),
157                TabPosition::Middle(Ordering::Greater) => this.border_r_1().pl_px().border_b_1(),
158            })
159            .cursor_pointer()
160            .child(
161                h_flex()
162                    .group("")
163                    .relative()
164                    .h(Tab::content_height(cx))
165                    .px(DynamicSpacing::Base04.px(cx))
166                    .gap(DynamicSpacing::Base04.rems(cx))
167                    .text_color(text_color)
168                    .child(start_slot)
169                    .children(self.children)
170                    .child(end_slot),
171            )
172    }
173}
174
175// View this component preview using `workspace: open component-preview`
176impl ComponentPreview for Tab {
177    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
178        v_flex()
179            .gap_6()
180            .children(vec![example_group_with_title(
181                "Variations",
182                vec![
183                    single_example(
184                        "Default",
185                        Tab::new("default").child("Default Tab").into_any_element(),
186                    ),
187                    single_example(
188                        "Selected",
189                        Tab::new("selected")
190                            .toggle_state(true)
191                            .child("Selected Tab")
192                            .into_any_element(),
193                    ),
194                    single_example(
195                        "First",
196                        Tab::new("first")
197                            .position(TabPosition::First)
198                            .child("First Tab")
199                            .into_any_element(),
200                    ),
201                    single_example(
202                        "Middle",
203                        Tab::new("middle")
204                            .position(TabPosition::Middle(Ordering::Equal))
205                            .child("Middle Tab")
206                            .into_any_element(),
207                    ),
208                    single_example(
209                        "Last",
210                        Tab::new("last")
211                            .position(TabPosition::Last)
212                            .child("Last Tab")
213                            .into_any_element(),
214                    ),
215                ],
216            )])
217            .into_any_element()
218    }
219}