1use crate::prelude::*;
2use crate::{Icon, IconColor, IconElement, Label, LabelColor};
3
4#[derive(Component, Clone)]
5pub struct Tab {
6 id: ElementId,
7 title: String,
8 icon: Option<Icon>,
9 current: bool,
10 dirty: bool,
11 fs_status: FileSystemStatus,
12 git_status: GitStatus,
13 diagnostic_status: DiagnosticStatus,
14 close_side: IconSide,
15}
16
17#[derive(Clone, Debug)]
18struct TabDragState {
19 title: String,
20}
21
22impl Tab {
23 pub fn new(id: impl Into<ElementId>) -> Self {
24 Self {
25 id: id.into(),
26 title: "untitled".to_string(),
27 icon: None,
28 current: false,
29 dirty: false,
30 fs_status: FileSystemStatus::None,
31 git_status: GitStatus::None,
32 diagnostic_status: DiagnosticStatus::None,
33 close_side: IconSide::Right,
34 }
35 }
36
37 pub fn current(mut self, current: bool) -> Self {
38 self.current = current;
39 self
40 }
41
42 pub fn title(mut self, title: String) -> Self {
43 self.title = title;
44 self
45 }
46
47 pub fn icon<I>(mut self, icon: I) -> Self
48 where
49 I: Into<Option<Icon>>,
50 {
51 self.icon = icon.into();
52 self
53 }
54
55 pub fn dirty(mut self, dirty: bool) -> Self {
56 self.dirty = dirty;
57 self
58 }
59
60 pub fn fs_status(mut self, fs_status: FileSystemStatus) -> Self {
61 self.fs_status = fs_status;
62 self
63 }
64
65 pub fn git_status(mut self, git_status: GitStatus) -> Self {
66 self.git_status = git_status;
67 self
68 }
69
70 pub fn diagnostic_status(mut self, diagnostic_status: DiagnosticStatus) -> Self {
71 self.diagnostic_status = diagnostic_status;
72 self
73 }
74
75 pub fn close_side(mut self, close_side: IconSide) -> Self {
76 self.close_side = close_side;
77 self
78 }
79
80 fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
81 let theme = theme(cx);
82 let has_fs_conflict = self.fs_status == FileSystemStatus::Conflict;
83 let is_deleted = self.fs_status == FileSystemStatus::Deleted;
84
85 let label = match (self.git_status, is_deleted) {
86 (_, true) | (GitStatus::Deleted, false) => Label::new(self.title.clone())
87 .color(LabelColor::Hidden)
88 .set_strikethrough(true),
89 (GitStatus::None, false) => Label::new(self.title.clone()),
90 (GitStatus::Created, false) => {
91 Label::new(self.title.clone()).color(LabelColor::Created)
92 }
93 (GitStatus::Modified, false) => {
94 Label::new(self.title.clone()).color(LabelColor::Modified)
95 }
96 (GitStatus::Renamed, false) => Label::new(self.title.clone()).color(LabelColor::Accent),
97 (GitStatus::Conflict, false) => Label::new(self.title.clone()),
98 };
99
100 let close_icon = || IconElement::new(Icon::Close).color(IconColor::Muted);
101
102 let (tab_bg, tab_hover_bg, tab_active_bg) = match self.current {
103 true => (
104 theme.ghost_element,
105 theme.ghost_element_hover,
106 theme.ghost_element_active,
107 ),
108 false => (
109 theme.filled_element,
110 theme.filled_element_hover,
111 theme.filled_element_active,
112 ),
113 };
114
115 let drag_state = TabDragState {
116 title: self.title.clone(),
117 };
118
119 div()
120 .id(self.id.clone())
121 .on_drag(move |_view, _cx| {
122 Drag::new(drag_state.clone(), |view, cx| div().w_8().h_4().bg(red()))
123 })
124 .drag_over::<TabDragState>(|d| d.bg(black()))
125 .on_drop(|_view, state: TabDragState, cx| {
126 dbg!(state);
127 })
128 .px_2()
129 .py_0p5()
130 .flex()
131 .items_center()
132 .justify_center()
133 .bg(tab_bg)
134 .hover(|h| h.bg(tab_hover_bg))
135 .active(|a| a.bg(tab_active_bg))
136 .child(
137 div()
138 .px_1()
139 .flex()
140 .items_center()
141 .gap_1()
142 .children(has_fs_conflict.then(|| {
143 IconElement::new(Icon::ExclamationTriangle)
144 .size(crate::IconSize::Small)
145 .color(IconColor::Warning)
146 }))
147 .children(self.icon.map(IconElement::new))
148 .children(if self.close_side == IconSide::Left {
149 Some(close_icon())
150 } else {
151 None
152 })
153 .child(label)
154 .children(if self.close_side == IconSide::Right {
155 Some(close_icon())
156 } else {
157 None
158 }),
159 )
160 }
161}
162
163use gpui2::{black, red, Drag, ElementId};
164#[cfg(feature = "stories")]
165pub use stories::*;
166
167#[cfg(feature = "stories")]
168mod stories {
169 use strum::IntoEnumIterator;
170
171 use crate::{h_stack, v_stack, Icon, Story};
172
173 use super::*;
174
175 #[derive(Component)]
176 pub struct TabStory;
177
178 impl TabStory {
179 pub fn new() -> Self {
180 Self
181 }
182
183 fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
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}