1use std::cmp::Ordering;
2use std::rc::Rc;
3
4use gpui::{AnyElement, AnyView, ClickEvent, IntoElement, MouseButton};
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)]
31pub struct Tab {
32 id: ElementId,
33 selected: bool,
34 position: TabPosition,
35 close_side: TabCloseSide,
36 on_click: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
37 tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView + 'static>>,
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 Self {
46 id: id.into(),
47 selected: false,
48 position: TabPosition::First,
49 close_side: TabCloseSide::End,
50 on_click: None,
51 tooltip: None,
52 start_slot: None,
53 end_slot: None,
54 children: SmallVec::new(),
55 }
56 }
57
58 pub fn position(mut self, position: TabPosition) -> Self {
59 self.position = position;
60 self
61 }
62
63 pub fn close_side(mut self, close_side: TabCloseSide) -> Self {
64 self.close_side = close_side;
65 self
66 }
67
68 pub fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
69 self.on_click = Some(Rc::new(handler));
70 self
71 }
72
73 pub fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
74 self.tooltip = Some(Box::new(tooltip));
75 self
76 }
77
78 pub fn start_slot<E: IntoElement>(mut self, element: impl Into<Option<E>>) -> Self {
79 self.start_slot = element.into().map(IntoElement::into_any_element);
80 self
81 }
82
83 pub fn end_slot<E: IntoElement>(mut self, element: impl Into<Option<E>>) -> Self {
84 self.end_slot = element.into().map(IntoElement::into_any_element);
85 self
86 }
87}
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 children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
98 &mut self.children
99 }
100}
101
102impl RenderOnce for Tab {
103 type Rendered = Div;
104
105 fn render(self, cx: &mut WindowContext) -> Self::Rendered {
106 const HEIGHT_IN_REMS: f32 = 30. / 16.;
107
108 let (text_color, tab_bg, _tab_hover_bg, _tab_active_bg) = match self.selected {
109 false => (
110 cx.theme().colors().text_muted,
111 cx.theme().colors().tab_inactive_background,
112 cx.theme().colors().ghost_element_hover,
113 cx.theme().colors().ghost_element_active,
114 ),
115 true => (
116 cx.theme().colors().text,
117 cx.theme().colors().tab_active_background,
118 cx.theme().colors().element_hover,
119 cx.theme().colors().element_active,
120 ),
121 };
122
123 div()
124 .h(rems(HEIGHT_IN_REMS))
125 .bg(tab_bg)
126 .border_color(cx.theme().colors().border)
127 .map(|this| match self.position {
128 TabPosition::First => {
129 if self.selected {
130 this.pl_px().border_r().pb_px()
131 } else {
132 this.pl_px().pr_px().border_b()
133 }
134 }
135 TabPosition::Last => {
136 if self.selected {
137 this.border_l().border_r().pb_px()
138 } else {
139 this.pr_px().pl_px().border_b()
140 }
141 }
142 TabPosition::Middle(Ordering::Equal) => this.border_l().border_r().pb_px(),
143 TabPosition::Middle(Ordering::Less) => this.border_l().pr_px().border_b(),
144 TabPosition::Middle(Ordering::Greater) => this.border_r().pl_px().border_b(),
145 })
146 .child(
147 h_stack()
148 .group("")
149 .id(self.id)
150 .relative()
151 .h_full()
152 .px_5()
153 .gap_1()
154 .text_color(text_color)
155 // .hover(|style| style.bg(tab_hover_bg))
156 // .active(|style| style.bg(tab_active_bg))
157 .when_some(self.on_click, |tab, on_click| {
158 tab.cursor_pointer().on_click(move |event, cx| {
159 // HACK: GPUI currently fires `on_click` with any mouse button,
160 // but we only care about the left button.
161 if event.down.button == MouseButton::Left {
162 (on_click)(event, cx)
163 }
164 })
165 })
166 .when_some(self.tooltip, |tab, tooltip| {
167 tab.tooltip(move |cx| tooltip(cx))
168 })
169 .child(
170 h_stack()
171 .w_3()
172 .h_3()
173 .justify_center()
174 .absolute()
175 .map(|this| match self.close_side {
176 TabCloseSide::Start => this.right_1(),
177 TabCloseSide::End => this.left_1(),
178 })
179 .children(self.start_slot),
180 )
181 .child(
182 h_stack()
183 .invisible()
184 .w_3()
185 .h_3()
186 .justify_center()
187 .absolute()
188 .map(|this| match self.close_side {
189 TabCloseSide::Start => this.left_1(),
190 TabCloseSide::End => this.right_1(),
191 })
192 .group_hover("", |style| style.visible())
193 .children(self.end_slot),
194 )
195 .children(self.children),
196 )
197 }
198}