1use crate::layout::h_stack;
6use crate::theme::{ActiveTheme, ControlSize, Themeable};
7use crate::traits::control_sized::ControlSized;
8use crate::traits::disableable::Disableable;
9use gpui::{
10 Context, ElementId, EventEmitter, InteractiveElement, IntoElement, MouseButton, ParentElement,
11 Render, SharedString, StatefulInteractiveElement, Styled, Window, div, prelude::*, px,
12};
13
14pub struct TabChanged {
16 pub tab_id: SharedString,
18}
19
20#[derive(Clone)]
22pub struct Tab {
23 pub id: SharedString,
25 pub label: SharedString,
27 pub disabled: bool,
29}
30
31impl Tab {
32 pub fn new(id: impl Into<SharedString>, label: impl Into<SharedString>) -> Self {
34 Self {
35 id: id.into(),
36 label: label.into(),
37 disabled: false,
38 }
39 }
40}
41
42impl Disableable for Tab {
43 fn is_disabled(&self) -> bool {
44 self.disabled
45 }
46
47 fn disabled(mut self, disabled: bool) -> Self {
48 self.disabled = disabled;
49 self
50 }
51}
52
53pub struct Tabs {
55 id: ElementId,
56 tabs: Vec<Tab>,
57 selected: Option<SharedString>,
58 disabled: bool,
59 size: ControlSize,
60}
61
62impl EventEmitter<TabChanged> for Tabs {}
63
64impl Tabs {
65 pub fn new(id: impl Into<ElementId>) -> Self {
67 Self {
68 id: id.into(),
69 tabs: Vec::new(),
70 selected: None,
71 disabled: false,
72 size: ControlSize::default(),
73 }
74 }
75
76 pub fn tab(mut self, tab: Tab) -> Self {
78 if self.selected.is_none() && !tab.disabled {
80 self.selected = Some(tab.id.clone());
81 }
82 self.tabs.push(tab);
83 self
84 }
85
86 pub fn selected(mut self, tab_id: impl Into<SharedString>) -> Self {
88 self.selected = Some(tab_id.into());
89 self
90 }
91
92 pub fn selected_option(mut self, tab_id: Option<SharedString>) -> Self {
94 self.selected = tab_id;
95 self
96 }
97
98 pub fn disabled(mut self, disabled: bool) -> Self {
100 self.disabled = disabled;
101 self
102 }
103
104 pub fn get_selected(&self) -> Option<&SharedString> {
106 self.selected.as_ref()
107 }
108
109 pub fn set_selected(&mut self, tab_id: SharedString, cx: &mut Context<Self>) {
111 if self.selected.as_ref() != Some(&tab_id) {
112 self.selected = Some(tab_id.clone());
113 cx.emit(TabChanged { tab_id });
114 cx.notify();
115 }
116 }
117
118 fn select_tab(&mut self, index: usize, cx: &mut Context<Self>) {
120 if let Some(tab) = self.tabs.get(index) {
121 if !tab.disabled && !self.disabled {
122 self.set_selected(tab.id.clone(), cx);
123 }
124 }
125 }
126}
127
128impl Render for Tabs {
129 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
130 let theme = cx.theme();
131 let metrics = theme.control(self.size);
132 let group_disabled = self.disabled;
133 let selected = self.selected.clone();
134
135 let tab_list = h_stack()
140 .gap(metrics.gap)
141 .border_b_1()
142 .border_color(theme.border());
143
144 div().id(self.id.clone()).flex().flex_col().child(
145 tab_list.children(
146 self.tabs
147 .iter()
148 .enumerate()
149 .map(|(index, tab)| {
150 let is_selected = selected.as_ref() == Some(&tab.id);
151 let is_disabled = group_disabled || tab.disabled;
152 let label = tab.label.clone();
153
154 let text_color = if is_disabled {
155 theme.fg_disabled()
156 } else if is_selected {
157 theme.accent()
158 } else {
159 theme.fg_muted()
160 };
161
162 let bg = if is_selected && !is_disabled {
163 theme.accent_bg()
164 } else {
165 gpui::Hsla::transparent_black()
166 };
167
168 let border_color = if is_selected && !is_disabled {
169 theme.accent()
170 } else {
171 gpui::Hsla::transparent_black()
172 };
173
174 div()
175 .id(ElementId::NamedInteger("tab".into(), index as u64))
176 .flex()
177 .items_center()
178 .justify_center()
179 .h(metrics.height)
180 .px(metrics.padding_x)
181 .text_size(metrics.text_size)
182 .line_height(metrics.line_height)
183 .text_color(text_color)
184 .bg(bg)
185 .border_b_2()
186 .border_color(border_color)
187 .mb(px(-1.0)) .rounded_t(metrics.radius)
189 .when(!is_disabled, |this| {
190 this.cursor_pointer()
191 .on_mouse_down(MouseButton::Left, |_, window, _| {
192 window.prevent_default()
193 })
194 .on_click(cx.listener(move |this, _, _, cx| {
195 this.select_tab(index, cx);
196 }))
197 .hover(|style| {
198 if is_selected {
199 style
200 } else {
201 style
202 .text_color(theme.fg())
203 .bg(theme.surface_secondary())
204 }
205 })
206 })
207 .when(is_disabled, |this| this.cursor_not_allowed().opacity(0.65))
208 .child(label)
209 })
210 .collect::<Vec<_>>(),
211 ),
212 )
213 }
214}
215
216pub fn tabs(id: impl Into<ElementId>) -> Tabs {
218 Tabs::new(id)
219}
220
221pub fn tab(id: impl Into<SharedString>, label: impl Into<SharedString>) -> Tab {
223 Tab::new(id, label)
224}
225
226impl Disableable for Tabs {
227 fn is_disabled(&self) -> bool {
228 self.disabled
229 }
230
231 fn disabled(mut self, disabled: bool) -> Self {
232 self.disabled = disabled;
233 self
234 }
235}
236
237impl ControlSized for Tabs {
238 fn control_size(mut self, size: ControlSize) -> Self {
239 self.size = size;
240 self
241 }
242}