1use crate::prelude::*;
2use crate::{Icon, IconColor, IconElement, Label, LabelColor};
3use gpui::{red, Div, ElementId, Render, View, VisualContext};
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(LabelColor::Hidden)
96 .set_strikethrough(true),
97 (GitStatus::None, false) => Label::new(self.title.clone()),
98 (GitStatus::Created, false) => {
99 Label::new(self.title.clone()).color(LabelColor::Created)
100 }
101 (GitStatus::Modified, false) => {
102 Label::new(self.title.clone()).color(LabelColor::Modified)
103 }
104 (GitStatus::Renamed, false) => Label::new(self.title.clone()).color(LabelColor::Accent),
105 (GitStatus::Conflict, false) => Label::new(self.title.clone()),
106 };
107
108 let close_icon = || IconElement::new(Icon::Close).color(IconColor::Muted);
109
110 let (tab_bg, tab_hover_bg, tab_active_bg) = match self.current {
111 false => (
112 cx.theme().colors().tab_inactive_background,
113 cx.theme().colors().ghost_element_hover,
114 cx.theme().colors().ghost_element_active,
115 ),
116 true => (
117 cx.theme().colors().tab_active_background,
118 cx.theme().colors().element_hover,
119 cx.theme().colors().element_active,
120 ),
121 };
122
123 let drag_state = TabDragState {
124 title: self.title.clone(),
125 };
126
127 div()
128 .id(self.id.clone())
129 .on_drag(move |_view, cx| cx.build_view(|cx| drag_state.clone()))
130 .drag_over::<TabDragState>(|d| d.bg(cx.theme().colors().drop_target_background))
131 .on_drop(|_view, state: View<TabDragState>, cx| {
132 eprintln!("{:?}", state.read(cx));
133 })
134 .px_2()
135 .py_0p5()
136 .flex()
137 .items_center()
138 .justify_center()
139 .bg(tab_bg)
140 .hover(|h| h.bg(tab_hover_bg))
141 .active(|a| a.bg(tab_active_bg))
142 .child(
143 div()
144 .px_1()
145 .flex()
146 .items_center()
147 .gap_1p5()
148 .children(has_fs_conflict.then(|| {
149 IconElement::new(Icon::ExclamationTriangle)
150 .size(crate::IconSize::Small)
151 .color(IconColor::Warning)
152 }))
153 .children(self.icon.map(IconElement::new))
154 .children(if self.close_side == IconSide::Left {
155 Some(close_icon())
156 } else {
157 None
158 })
159 .child(label)
160 .children(if self.close_side == IconSide::Right {
161 Some(close_icon())
162 } else {
163 None
164 }),
165 )
166 }
167}
168
169#[cfg(feature = "stories")]
170pub use stories::*;
171
172#[cfg(feature = "stories")]
173mod stories {
174 use super::*;
175 use crate::{h_stack, v_stack, Icon, Story};
176 use strum::IntoEnumIterator;
177
178 pub struct TabStory;
179
180 impl Render for TabStory {
181 type Element = Div<Self>;
182
183 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
184 let git_statuses = GitStatus::iter();
185 let fs_statuses = FileSystemStatus::iter();
186
187 Story::container(cx)
188 .child(Story::title_for::<_, Tab>(cx))
189 .child(
190 h_stack().child(
191 v_stack()
192 .gap_2()
193 .child(Story::label(cx, "Default"))
194 .child(Tab::new("default")),
195 ),
196 )
197 .child(
198 h_stack().child(
199 v_stack().gap_2().child(Story::label(cx, "Current")).child(
200 h_stack()
201 .gap_4()
202 .child(
203 Tab::new("current")
204 .title("Current".to_string())
205 .current(true),
206 )
207 .child(
208 Tab::new("not_current")
209 .title("Not Current".to_string())
210 .current(false),
211 ),
212 ),
213 ),
214 )
215 .child(
216 h_stack().child(
217 v_stack()
218 .gap_2()
219 .child(Story::label(cx, "Titled"))
220 .child(Tab::new("titled").title("label".to_string())),
221 ),
222 )
223 .child(
224 h_stack().child(
225 v_stack()
226 .gap_2()
227 .child(Story::label(cx, "With Icon"))
228 .child(
229 Tab::new("with_icon")
230 .title("label".to_string())
231 .icon(Some(Icon::Envelope)),
232 ),
233 ),
234 )
235 .child(
236 h_stack().child(
237 v_stack()
238 .gap_2()
239 .child(Story::label(cx, "Close Side"))
240 .child(
241 h_stack()
242 .gap_4()
243 .child(
244 Tab::new("left")
245 .title("Left".to_string())
246 .close_side(IconSide::Left),
247 )
248 .child(Tab::new("right").title("Right".to_string())),
249 ),
250 ),
251 )
252 .child(
253 v_stack()
254 .gap_2()
255 .child(Story::label(cx, "Git Status"))
256 .child(h_stack().gap_4().children(git_statuses.map(|git_status| {
257 Tab::new("git_status")
258 .title(git_status.to_string())
259 .git_status(git_status)
260 }))),
261 )
262 .child(
263 v_stack()
264 .gap_2()
265 .child(Story::label(cx, "File System Status"))
266 .child(h_stack().gap_4().children(fs_statuses.map(|fs_status| {
267 Tab::new("file_system_status")
268 .title(fs_status.to_string())
269 .fs_status(fs_status)
270 }))),
271 )
272 }
273 }
274}