tab.rs

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