tab.rs

  1use std::cmp::Ordering;
  2
  3use gpui::{AnyElement, IntoElement, Stateful};
  4use smallvec::SmallVec;
  5
  6use crate::{prelude::*, BASE_REM_SIZE_IN_PX};
  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)]
 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 const CONTAINER_HEIGHT_IN_REMS: f32 = 29. / BASE_REM_SIZE_IN_PX;
 57
 58    const CONTENT_HEIGHT_IN_REMS: f32 = 28. / BASE_REM_SIZE_IN_PX;
 59
 60    pub fn position(mut self, position: TabPosition) -> Self {
 61        self.position = position;
 62        self
 63    }
 64
 65    pub fn close_side(mut self, close_side: TabCloseSide) -> Self {
 66        self.close_side = close_side;
 67        self
 68    }
 69
 70    pub fn start_slot<E: IntoElement>(mut self, element: impl Into<Option<E>>) -> Self {
 71        self.start_slot = element.into().map(IntoElement::into_any_element);
 72        self
 73    }
 74
 75    pub fn end_slot<E: IntoElement>(mut self, element: impl Into<Option<E>>) -> Self {
 76        self.end_slot = element.into().map(IntoElement::into_any_element);
 77        self
 78    }
 79}
 80
 81impl InteractiveElement for Tab {
 82    fn interactivity(&mut self) -> &mut gpui::Interactivity {
 83        self.div.interactivity()
 84    }
 85}
 86
 87impl StatefulInteractiveElement for Tab {}
 88
 89impl Selectable for Tab {
 90    fn selected(mut self, selected: bool) -> Self {
 91        self.selected = selected;
 92        self
 93    }
 94}
 95
 96impl ParentElement for Tab {
 97    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
 98        self.children.extend(elements)
 99    }
100}
101
102impl RenderOnce for Tab {
103    #[allow(refining_impl_trait)]
104    fn render(self, cx: &mut WindowContext) -> Stateful<Div> {
105        let (text_color, tab_bg, _tab_hover_bg, _tab_active_bg) = match self.selected {
106            false => (
107                cx.theme().colors().text_muted,
108                cx.theme().colors().tab_inactive_background,
109                cx.theme().colors().ghost_element_hover,
110                cx.theme().colors().ghost_element_active,
111            ),
112            true => (
113                cx.theme().colors().text,
114                cx.theme().colors().tab_active_background,
115                cx.theme().colors().element_hover,
116                cx.theme().colors().element_active,
117            ),
118        };
119
120        let (start_slot, end_slot) = {
121            let start_slot = h_flex().size_3().justify_center().children(self.start_slot);
122
123            let end_slot = h_flex().size_3().justify_center().children(self.end_slot);
124
125            match self.close_side {
126                TabCloseSide::End => (start_slot, end_slot),
127                TabCloseSide::Start => (end_slot, start_slot),
128            }
129        };
130
131        self.div
132            .h(rems(Self::CONTAINER_HEIGHT_IN_REMS))
133            .bg(tab_bg)
134            .border_color(cx.theme().colors().border)
135            .map(|this| match self.position {
136                TabPosition::First => {
137                    if self.selected {
138                        this.pl_px().border_r_1().pb_px()
139                    } else {
140                        this.pl_px().pr_px().border_b_1()
141                    }
142                }
143                TabPosition::Last => {
144                    if self.selected {
145                        this.border_l_1().border_r_1().pb_px()
146                    } else {
147                        this.pr_px().pl_px().border_b_1().border_r_1()
148                    }
149                }
150                TabPosition::Middle(Ordering::Equal) => this.border_l_1().border_r_1().pb_px(),
151                TabPosition::Middle(Ordering::Less) => this.border_l_1().pr_px().border_b_1(),
152                TabPosition::Middle(Ordering::Greater) => this.border_r_1().pl_px().border_b_1(),
153            })
154            .cursor_pointer()
155            .child(
156                h_flex()
157                    .group("")
158                    .relative()
159                    .h(rems(Self::CONTENT_HEIGHT_IN_REMS))
160                    .px(crate::custom_spacing(cx, 4.))
161                    .gap(Spacing::Small.rems(cx))
162                    .text_color(text_color)
163                    .child(start_slot)
164                    .children(self.children)
165                    .child(end_slot),
166            )
167    }
168}