1use crate::prelude::*;
2use crate::{Icon, IconElement, Label, TextColor};
3use gpui::{prelude::*, red, Div, ElementId, Render, RenderOnce, Stateful, View};
4
5#[derive(RenderOnce, Clone)]
6pub struct Tab {
7 id: ElementId,
8 title: String,
9 icon: Option<Icon>,
10 current: bool,
11 dirty: bool,
12 fs_status: FileSystemStatus,
13 git_status: GitStatus,
14 diagnostic_status: DiagnosticStatus,
15 close_side: IconSide,
16}
17
18#[derive(Clone, Debug)]
19struct TabDragState {
20 title: String,
21}
22
23impl Render<Self> for TabDragState {
24 type Element = Div<Self>;
25
26 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
27 div().w_8().h_4().bg(red())
28 }
29}
30
31impl<V: 'static> Component<V> for Tab {
32 type Rendered = Stateful<V, Div<V>>;
33
34 fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> Self::Rendered {
35 let has_fs_conflict = self.fs_status == FileSystemStatus::Conflict;
36 let is_deleted = self.fs_status == FileSystemStatus::Deleted;
37
38 let label = match (self.git_status, is_deleted) {
39 (_, true) | (GitStatus::Deleted, false) => Label::new(self.title.clone())
40 .color(TextColor::Hidden)
41 .set_strikethrough(true),
42 (GitStatus::None, false) => Label::new(self.title.clone()),
43 (GitStatus::Created, false) => Label::new(self.title.clone()).color(TextColor::Created),
44 (GitStatus::Modified, false) => {
45 Label::new(self.title.clone()).color(TextColor::Modified)
46 }
47 (GitStatus::Renamed, false) => Label::new(self.title.clone()).color(TextColor::Accent),
48 (GitStatus::Conflict, false) => Label::new(self.title.clone()),
49 };
50
51 let close_icon = || IconElement::new(Icon::Close).color(TextColor::Muted);
52
53 let (tab_bg, tab_hover_bg, tab_active_bg) = match self.current {
54 false => (
55 cx.theme().colors().tab_inactive_background,
56 cx.theme().colors().ghost_element_hover,
57 cx.theme().colors().ghost_element_active,
58 ),
59 true => (
60 cx.theme().colors().tab_active_background,
61 cx.theme().colors().element_hover,
62 cx.theme().colors().element_active,
63 ),
64 };
65
66 let drag_state = TabDragState {
67 title: self.title.clone(),
68 };
69
70 div()
71 .id(self.id.clone())
72 .on_drag(move |_view, cx| cx.build_view(|cx| drag_state.clone()))
73 .drag_over::<TabDragState>(|d| d.bg(cx.theme().colors().drop_target_background))
74 .on_drop(|_view, state: View<TabDragState>, cx| {
75 eprintln!("{:?}", state.read(cx));
76 })
77 .px_2()
78 .py_0p5()
79 .flex()
80 .items_center()
81 .justify_center()
82 .bg(tab_bg)
83 .hover(|h| h.bg(tab_hover_bg))
84 .active(|a| a.bg(tab_active_bg))
85 .child(
86 div()
87 .px_1()
88 .flex()
89 .items_center()
90 .gap_1p5()
91 .children(has_fs_conflict.then(|| {
92 IconElement::new(Icon::ExclamationTriangle)
93 .size(crate::IconSize::Small)
94 .color(TextColor::Warning)
95 }))
96 .children(self.icon.map(IconElement::new))
97 .children(if self.close_side == IconSide::Left {
98 Some(close_icon())
99 } else {
100 None
101 })
102 .child(label)
103 .children(if self.close_side == IconSide::Right {
104 Some(close_icon())
105 } else {
106 None
107 }),
108 )
109 }
110}
111
112impl Tab {
113 pub fn new(id: impl Into<ElementId>) -> Self {
114 Self {
115 id: id.into(),
116 title: "untitled".to_string(),
117 icon: None,
118 current: false,
119 dirty: false,
120 fs_status: FileSystemStatus::None,
121 git_status: GitStatus::None,
122 diagnostic_status: DiagnosticStatus::None,
123 close_side: IconSide::Right,
124 }
125 }
126
127 pub fn current(mut self, current: bool) -> Self {
128 self.current = current;
129 self
130 }
131
132 pub fn title(mut self, title: String) -> Self {
133 self.title = title;
134 self
135 }
136
137 pub fn icon<I>(mut self, icon: I) -> Self
138 where
139 I: Into<Option<Icon>>,
140 {
141 self.icon = icon.into();
142 self
143 }
144
145 pub fn dirty(mut self, dirty: bool) -> Self {
146 self.dirty = dirty;
147 self
148 }
149
150 pub fn fs_status(mut self, fs_status: FileSystemStatus) -> Self {
151 self.fs_status = fs_status;
152 self
153 }
154
155 pub fn git_status(mut self, git_status: GitStatus) -> Self {
156 self.git_status = git_status;
157 self
158 }
159
160 pub fn diagnostic_status(mut self, diagnostic_status: DiagnosticStatus) -> Self {
161 self.diagnostic_status = diagnostic_status;
162 self
163 }
164
165 pub fn close_side(mut self, close_side: IconSide) -> Self {
166 self.close_side = close_side;
167 self
168 }
169}
170
171#[cfg(feature = "stories")]
172pub use stories::*;
173
174#[cfg(feature = "stories")]
175mod stories {
176 use super::*;
177 use crate::{h_stack, v_stack, Icon, Story};
178 use strum::IntoEnumIterator;
179
180 pub struct TabStory;
181
182 impl Render<Self> for TabStory {
183 type Element = Div<Self>;
184
185 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
186 let git_statuses = GitStatus::iter();
187 let fs_statuses = FileSystemStatus::iter();
188
189 Story::container(cx)
190 .child(Story::title_for::<_, Tab>(cx))
191 .child(
192 h_stack().child(
193 v_stack()
194 .gap_2()
195 .child(Story::label(cx, "Default"))
196 .child(Tab::new("default")),
197 ),
198 )
199 .child(
200 h_stack().child(
201 v_stack().gap_2().child(Story::label(cx, "Current")).child(
202 h_stack()
203 .gap_4()
204 .child(
205 Tab::new("current")
206 .title("Current".to_string())
207 .current(true),
208 )
209 .child(
210 Tab::new("not_current")
211 .title("Not Current".to_string())
212 .current(false),
213 ),
214 ),
215 ),
216 )
217 .child(
218 h_stack().child(
219 v_stack()
220 .gap_2()
221 .child(Story::label(cx, "Titled"))
222 .child(Tab::new("titled").title("label".to_string())),
223 ),
224 )
225 .child(
226 h_stack().child(
227 v_stack()
228 .gap_2()
229 .child(Story::label(cx, "With Icon"))
230 .child(
231 Tab::new("with_icon")
232 .title("label".to_string())
233 .icon(Some(Icon::Envelope)),
234 ),
235 ),
236 )
237 .child(
238 h_stack().child(
239 v_stack()
240 .gap_2()
241 .child(Story::label(cx, "Close Side"))
242 .child(
243 h_stack()
244 .gap_4()
245 .child(
246 Tab::new("left")
247 .title("Left".to_string())
248 .close_side(IconSide::Left),
249 )
250 .child(Tab::new("right").title("Right".to_string())),
251 ),
252 ),
253 )
254 .child(
255 v_stack()
256 .gap_2()
257 .child(Story::label(cx, "Git Status"))
258 .child(h_stack().gap_4().children(git_statuses.map(|git_status| {
259 Tab::new("git_status")
260 .title(git_status.to_string())
261 .git_status(git_status)
262 }))),
263 )
264 .child(
265 v_stack()
266 .gap_2()
267 .child(Story::label(cx, "File System Status"))
268 .child(h_stack().gap_4().children(fs_statuses.map(|fs_status| {
269 Tab::new("file_system_status")
270 .title(fs_status.to_string())
271 .fs_status(fs_status)
272 }))),
273 )
274 }
275 }
276}