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