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