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