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