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 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(LabelColor::Hidden)
 96                .set_strikethrough(true),
 97            (GitStatus::None, false) => Label::new(self.title.clone()),
 98            (GitStatus::Created, false) => {
 99                Label::new(self.title.clone()).color(LabelColor::Created)
100            }
101            (GitStatus::Modified, false) => {
102                Label::new(self.title.clone()).color(LabelColor::Modified)
103            }
104            (GitStatus::Renamed, false) => Label::new(self.title.clone()).color(LabelColor::Accent),
105            (GitStatus::Conflict, false) => Label::new(self.title.clone()),
106        };
107
108        let close_icon = || IconElement::new(Icon::Close).color(IconColor::Muted);
109
110        let (tab_bg, tab_hover_bg, tab_active_bg) = match self.current {
111            true => (
112                cx.theme().colors().ghost_element,
113                cx.theme().colors().ghost_element_hover,
114                cx.theme().colors().ghost_element_active,
115            ),
116            false => (
117                cx.theme().colors().element,
118                cx.theme().colors().element_hover,
119                cx.theme().colors().element_active,
120            ),
121        };
122
123        let drag_state = TabDragState {
124            title: self.title.clone(),
125        };
126
127        div()
128            .id(self.id.clone())
129            .on_drag(move |_view, cx| cx.build_view(|cx| drag_state.clone()))
130            .drag_over::<TabDragState>(|d| d.bg(black()))
131            .on_drop(|_view, state: View<TabDragState>, cx| {
132                dbg!(state.read(cx));
133            })
134            .px_2()
135            .py_0p5()
136            .flex()
137            .items_center()
138            .justify_center()
139            .bg(tab_bg)
140            .hover(|h| h.bg(tab_hover_bg))
141            .active(|a| a.bg(tab_active_bg))
142            .child(
143                div()
144                    .px_1()
145                    .flex()
146                    .items_center()
147                    .gap_1()
148                    .children(has_fs_conflict.then(|| {
149                        IconElement::new(Icon::ExclamationTriangle)
150                            .size(crate::IconSize::Small)
151                            .color(IconColor::Warning)
152                    }))
153                    .children(self.icon.map(IconElement::new))
154                    .children(if self.close_side == IconSide::Left {
155                        Some(close_icon())
156                    } else {
157                        None
158                    })
159                    .child(label)
160                    .children(if self.close_side == IconSide::Right {
161                        Some(close_icon())
162                    } else {
163                        None
164                    }),
165            )
166    }
167}
168
169#[cfg(feature = "stories")]
170pub use stories::*;
171
172#[cfg(feature = "stories")]
173mod stories {
174    use super::*;
175    use crate::{h_stack, v_stack, Icon, Story};
176    use strum::IntoEnumIterator;
177
178    pub struct TabStory;
179
180    impl Render for TabStory {
181        type Element = Div<Self>;
182
183        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
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}