tab.rs

  1use crate::prelude::*;
  2use crate::{Icon, IconElement, Label, TextColor};
  3use gpui::{prelude::*, red, Div, ElementId, Render, View};
  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 has_fs_conflict = self.fs_status == FileSystemStatus::Conflict;
 91        let is_deleted = self.fs_status == FileSystemStatus::Deleted;
 92
 93        let label = match (self.git_status, is_deleted) {
 94            (_, true) | (GitStatus::Deleted, false) => Label::new(self.title.clone())
 95                .color(TextColor::Hidden)
 96                .set_strikethrough(true),
 97            (GitStatus::None, false) => Label::new(self.title.clone()),
 98            (GitStatus::Created, false) => Label::new(self.title.clone()).color(TextColor::Created),
 99            (GitStatus::Modified, false) => {
100                Label::new(self.title.clone()).color(TextColor::Modified)
101            }
102            (GitStatus::Renamed, false) => Label::new(self.title.clone()).color(TextColor::Accent),
103            (GitStatus::Conflict, false) => Label::new(self.title.clone()),
104        };
105
106        let close_icon = || IconElement::new(Icon::Close).color(TextColor::Muted);
107
108        let (tab_bg, tab_hover_bg, tab_active_bg) = match self.current {
109            false => (
110                cx.theme().colors().tab_inactive_background,
111                cx.theme().colors().ghost_element_hover,
112                cx.theme().colors().ghost_element_active,
113            ),
114            true => (
115                cx.theme().colors().tab_active_background,
116                cx.theme().colors().element_hover,
117                cx.theme().colors().element_active,
118            ),
119        };
120
121        let drag_state = TabDragState {
122            title: self.title.clone(),
123        };
124
125        div()
126            .id(self.id.clone())
127            .on_drag(move |_view, cx| cx.build_view(|cx| drag_state.clone()))
128            .drag_over::<TabDragState>(|d| d.bg(cx.theme().colors().drop_target_background))
129            .on_drop(|_view, state: View<TabDragState>, cx| {
130                eprintln!("{:?}", state.read(cx));
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_1p5()
146                    .children(has_fs_conflict.then(|| {
147                        IconElement::new(Icon::ExclamationTriangle)
148                            .size(crate::IconSize::Small)
149                            .color(TextColor::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
167#[cfg(feature = "stories")]
168pub use stories::*;
169
170#[cfg(feature = "stories")]
171mod stories {
172    use super::*;
173    use crate::{h_stack, v_stack, Icon, Story};
174    use strum::IntoEnumIterator;
175
176    pub struct TabStory;
177
178    impl Render for TabStory {
179        type Element = Div<Self>;
180
181        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
182            let git_statuses = GitStatus::iter();
183            let fs_statuses = FileSystemStatus::iter();
184
185            Story::container(cx)
186                .child(Story::title_for::<_, Tab>(cx))
187                .child(
188                    h_stack().child(
189                        v_stack()
190                            .gap_2()
191                            .child(Story::label(cx, "Default"))
192                            .child(Tab::new("default")),
193                    ),
194                )
195                .child(
196                    h_stack().child(
197                        v_stack().gap_2().child(Story::label(cx, "Current")).child(
198                            h_stack()
199                                .gap_4()
200                                .child(
201                                    Tab::new("current")
202                                        .title("Current".to_string())
203                                        .current(true),
204                                )
205                                .child(
206                                    Tab::new("not_current")
207                                        .title("Not Current".to_string())
208                                        .current(false),
209                                ),
210                        ),
211                    ),
212                )
213                .child(
214                    h_stack().child(
215                        v_stack()
216                            .gap_2()
217                            .child(Story::label(cx, "Titled"))
218                            .child(Tab::new("titled").title("label".to_string())),
219                    ),
220                )
221                .child(
222                    h_stack().child(
223                        v_stack()
224                            .gap_2()
225                            .child(Story::label(cx, "With Icon"))
226                            .child(
227                                Tab::new("with_icon")
228                                    .title("label".to_string())
229                                    .icon(Some(Icon::Envelope)),
230                            ),
231                    ),
232                )
233                .child(
234                    h_stack().child(
235                        v_stack()
236                            .gap_2()
237                            .child(Story::label(cx, "Close Side"))
238                            .child(
239                                h_stack()
240                                    .gap_4()
241                                    .child(
242                                        Tab::new("left")
243                                            .title("Left".to_string())
244                                            .close_side(IconSide::Left),
245                                    )
246                                    .child(Tab::new("right").title("Right".to_string())),
247                            ),
248                    ),
249                )
250                .child(
251                    v_stack()
252                        .gap_2()
253                        .child(Story::label(cx, "Git Status"))
254                        .child(h_stack().gap_4().children(git_statuses.map(|git_status| {
255                            Tab::new("git_status")
256                                .title(git_status.to_string())
257                                .git_status(git_status)
258                        }))),
259                )
260                .child(
261                    v_stack()
262                        .gap_2()
263                        .child(Story::label(cx, "File System Status"))
264                        .child(h_stack().gap_4().children(fs_statuses.map(|fs_status| {
265                            Tab::new("file_system_status")
266                                .title(fs_status.to_string())
267                                .fs_status(fs_status)
268                        }))),
269                )
270        }
271    }
272}