1use std::marker::PhantomData;
2
3use crate::prelude::*;
4use crate::{Icon, IconColor, IconElement, Label, LabelColor};
5
6#[derive(Element, Clone)]
7pub struct Tab<S: 'static + Send + Sync + Clone> {
8 state_type: PhantomData<S>,
9 id: ElementId,
10 title: String,
11 icon: Option<Icon>,
12 current: bool,
13 dirty: bool,
14 fs_status: FileSystemStatus,
15 git_status: GitStatus,
16 diagnostic_status: DiagnosticStatus,
17 close_side: IconSide,
18}
19
20#[derive(Clone, Debug)]
21struct TabDragState {
22 title: String,
23}
24
25impl<S: 'static + Send + Sync + Clone> Tab<S> {
26 pub fn new(id: impl Into<ElementId>) -> Self {
27 Self {
28 state_type: PhantomData,
29 id: id.into(),
30 title: "untitled".to_string(),
31 icon: None,
32 current: false,
33 dirty: false,
34 fs_status: FileSystemStatus::None,
35 git_status: GitStatus::None,
36 diagnostic_status: DiagnosticStatus::None,
37 close_side: IconSide::Right,
38 }
39 }
40
41 pub fn current(mut self, current: bool) -> Self {
42 self.current = current;
43 self
44 }
45
46 pub fn title(mut self, title: String) -> Self {
47 self.title = title;
48 self
49 }
50
51 pub fn icon<I>(mut self, icon: I) -> Self
52 where
53 I: Into<Option<Icon>>,
54 {
55 self.icon = icon.into();
56 self
57 }
58
59 pub fn dirty(mut self, dirty: bool) -> Self {
60 self.dirty = dirty;
61 self
62 }
63
64 pub fn fs_status(mut self, fs_status: FileSystemStatus) -> Self {
65 self.fs_status = fs_status;
66 self
67 }
68
69 pub fn git_status(mut self, git_status: GitStatus) -> Self {
70 self.git_status = git_status;
71 self
72 }
73
74 pub fn diagnostic_status(mut self, diagnostic_status: DiagnosticStatus) -> Self {
75 self.diagnostic_status = diagnostic_status;
76 self
77 }
78
79 pub fn close_side(mut self, close_side: IconSide) -> Self {
80 self.close_side = close_side;
81 self
82 }
83
84 fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
85 let color = ThemeColor::new(cx);
86 let has_fs_conflict = self.fs_status == FileSystemStatus::Conflict;
87 let is_deleted = self.fs_status == FileSystemStatus::Deleted;
88
89 let label = match (self.git_status, is_deleted) {
90 (_, true) | (GitStatus::Deleted, false) => Label::new(self.title.clone())
91 .color(LabelColor::Hidden)
92 .set_strikethrough(true),
93 (GitStatus::None, false) => Label::new(self.title.clone()),
94 (GitStatus::Created, false) => {
95 Label::new(self.title.clone()).color(LabelColor::Created)
96 }
97 (GitStatus::Modified, false) => {
98 Label::new(self.title.clone()).color(LabelColor::Modified)
99 }
100 (GitStatus::Renamed, false) => Label::new(self.title.clone()).color(LabelColor::Accent),
101 (GitStatus::Conflict, false) => Label::new(self.title.clone()),
102 };
103
104 let close_icon = || IconElement::new(Icon::Close).color(IconColor::Muted);
105
106 let (tab_bg, tab_hover_bg, tab_active_bg) = match self.current {
107 true => (
108 color.ghost_element,
109 color.ghost_element_hover,
110 color.ghost_element_active,
111 ),
112 false => (
113 color.filled_element,
114 color.filled_element_hover,
115 color.filled_element_active,
116 ),
117 };
118
119 let drag_state = TabDragState {
120 title: self.title.clone(),
121 };
122
123 div()
124 .id(self.id.clone())
125 .on_drag(move |_view, _cx| {
126 Drag::new(drag_state.clone(), |view, cx| div().w_8().h_4().bg(red()))
127 })
128 .drag_over::<TabDragState>(|d| d.bg(black()))
129 .on_drop(|_view, state: TabDragState, cx| {
130 dbg!(state);
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_1()
146 .children(has_fs_conflict.then(|| {
147 IconElement::new(Icon::ExclamationTriangle)
148 .size(crate::IconSize::Small)
149 .color(IconColor::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
167use gpui2::{black, red, Drag, ElementId};
168#[cfg(feature = "stories")]
169pub use stories::*;
170
171#[cfg(feature = "stories")]
172mod stories {
173 use strum::IntoEnumIterator;
174
175 use crate::{h_stack, v_stack, Icon, Story};
176
177 use super::*;
178
179 #[derive(Element)]
180 pub struct TabStory<S: 'static + Send + Sync + Clone> {
181 state_type: PhantomData<S>,
182 }
183
184 impl<S: 'static + Send + Sync + Clone> TabStory<S> {
185 pub fn new() -> Self {
186 Self {
187 state_type: PhantomData,
188 }
189 }
190
191 fn render(
192 &mut self,
193 _view: &mut S,
194 cx: &mut ViewContext<S>,
195 ) -> impl Element<ViewState = S> {
196 let git_statuses = GitStatus::iter();
197 let fs_statuses = FileSystemStatus::iter();
198
199 Story::container(cx)
200 .child(Story::title_for::<_, Tab<S>>(cx))
201 .child(
202 h_stack().child(
203 v_stack()
204 .gap_2()
205 .child(Story::label(cx, "Default"))
206 .child(Tab::new("default")),
207 ),
208 )
209 .child(
210 h_stack().child(
211 v_stack().gap_2().child(Story::label(cx, "Current")).child(
212 h_stack()
213 .gap_4()
214 .child(
215 Tab::new("current")
216 .title("Current".to_string())
217 .current(true),
218 )
219 .child(
220 Tab::new("not_current")
221 .title("Not Current".to_string())
222 .current(false),
223 ),
224 ),
225 ),
226 )
227 .child(
228 h_stack().child(
229 v_stack()
230 .gap_2()
231 .child(Story::label(cx, "Titled"))
232 .child(Tab::new("titled").title("label".to_string())),
233 ),
234 )
235 .child(
236 h_stack().child(
237 v_stack()
238 .gap_2()
239 .child(Story::label(cx, "With Icon"))
240 .child(
241 Tab::new("with_icon")
242 .title("label".to_string())
243 .icon(Some(Icon::Envelope)),
244 ),
245 ),
246 )
247 .child(
248 h_stack().child(
249 v_stack()
250 .gap_2()
251 .child(Story::label(cx, "Close Side"))
252 .child(
253 h_stack()
254 .gap_4()
255 .child(
256 Tab::new("left")
257 .title("Left".to_string())
258 .close_side(IconSide::Left),
259 )
260 .child(Tab::new("right").title("Right".to_string())),
261 ),
262 ),
263 )
264 .child(
265 v_stack()
266 .gap_2()
267 .child(Story::label(cx, "Git Status"))
268 .child(h_stack().gap_4().children(git_statuses.map(|git_status| {
269 Tab::new("git_status")
270 .title(git_status.to_string())
271 .git_status(git_status)
272 }))),
273 )
274 .child(
275 v_stack()
276 .gap_2()
277 .child(Story::label(cx, "File System Status"))
278 .child(h_stack().gap_4().children(fs_statuses.map(|fs_status| {
279 Tab::new("file_system_status")
280 .title(fs_status.to_string())
281 .fs_status(fs_status)
282 }))),
283 )
284 }
285 }
286}