tab.rs

  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}