tab.rs

  1use crate::prelude::*;
  2use crate::{Icon, IconElement, Label, TextColor};
  3use gpui::{prelude::*, red, Div, ElementId, Render, RenderOnce, Stateful, View};
  4
  5#[derive(RenderOnce, 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<Self> 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<V: 'static> Component<V> for Tab {
 32    type Rendered = Stateful<V, Div<V>>;
 33
 34    fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> Self::Rendered {
 35        let has_fs_conflict = self.fs_status == FileSystemStatus::Conflict;
 36        let is_deleted = self.fs_status == FileSystemStatus::Deleted;
 37
 38        let label = match (self.git_status, is_deleted) {
 39            (_, true) | (GitStatus::Deleted, false) => Label::new(self.title.clone())
 40                .color(TextColor::Hidden)
 41                .set_strikethrough(true),
 42            (GitStatus::None, false) => Label::new(self.title.clone()),
 43            (GitStatus::Created, false) => Label::new(self.title.clone()).color(TextColor::Created),
 44            (GitStatus::Modified, false) => {
 45                Label::new(self.title.clone()).color(TextColor::Modified)
 46            }
 47            (GitStatus::Renamed, false) => Label::new(self.title.clone()).color(TextColor::Accent),
 48            (GitStatus::Conflict, false) => Label::new(self.title.clone()),
 49        };
 50
 51        let close_icon = || IconElement::new(Icon::Close).color(TextColor::Muted);
 52
 53        let (tab_bg, tab_hover_bg, tab_active_bg) = match self.current {
 54            false => (
 55                cx.theme().colors().tab_inactive_background,
 56                cx.theme().colors().ghost_element_hover,
 57                cx.theme().colors().ghost_element_active,
 58            ),
 59            true => (
 60                cx.theme().colors().tab_active_background,
 61                cx.theme().colors().element_hover,
 62                cx.theme().colors().element_active,
 63            ),
 64        };
 65
 66        let drag_state = TabDragState {
 67            title: self.title.clone(),
 68        };
 69
 70        div()
 71            .id(self.id.clone())
 72            .on_drag(move |_view, cx| cx.build_view(|cx| drag_state.clone()))
 73            .drag_over::<TabDragState>(|d| d.bg(cx.theme().colors().drop_target_background))
 74            .on_drop(|_view, state: View<TabDragState>, cx| {
 75                eprintln!("{:?}", state.read(cx));
 76            })
 77            .px_2()
 78            .py_0p5()
 79            .flex()
 80            .items_center()
 81            .justify_center()
 82            .bg(tab_bg)
 83            .hover(|h| h.bg(tab_hover_bg))
 84            .active(|a| a.bg(tab_active_bg))
 85            .child(
 86                div()
 87                    .px_1()
 88                    .flex()
 89                    .items_center()
 90                    .gap_1p5()
 91                    .children(has_fs_conflict.then(|| {
 92                        IconElement::new(Icon::ExclamationTriangle)
 93                            .size(crate::IconSize::Small)
 94                            .color(TextColor::Warning)
 95                    }))
 96                    .children(self.icon.map(IconElement::new))
 97                    .children(if self.close_side == IconSide::Left {
 98                        Some(close_icon())
 99                    } else {
100                        None
101                    })
102                    .child(label)
103                    .children(if self.close_side == IconSide::Right {
104                        Some(close_icon())
105                    } else {
106                        None
107                    }),
108            )
109    }
110}
111
112impl Tab {
113    pub fn new(id: impl Into<ElementId>) -> Self {
114        Self {
115            id: id.into(),
116            title: "untitled".to_string(),
117            icon: None,
118            current: false,
119            dirty: false,
120            fs_status: FileSystemStatus::None,
121            git_status: GitStatus::None,
122            diagnostic_status: DiagnosticStatus::None,
123            close_side: IconSide::Right,
124        }
125    }
126
127    pub fn current(mut self, current: bool) -> Self {
128        self.current = current;
129        self
130    }
131
132    pub fn title(mut self, title: String) -> Self {
133        self.title = title;
134        self
135    }
136
137    pub fn icon<I>(mut self, icon: I) -> Self
138    where
139        I: Into<Option<Icon>>,
140    {
141        self.icon = icon.into();
142        self
143    }
144
145    pub fn dirty(mut self, dirty: bool) -> Self {
146        self.dirty = dirty;
147        self
148    }
149
150    pub fn fs_status(mut self, fs_status: FileSystemStatus) -> Self {
151        self.fs_status = fs_status;
152        self
153    }
154
155    pub fn git_status(mut self, git_status: GitStatus) -> Self {
156        self.git_status = git_status;
157        self
158    }
159
160    pub fn diagnostic_status(mut self, diagnostic_status: DiagnosticStatus) -> Self {
161        self.diagnostic_status = diagnostic_status;
162        self
163    }
164
165    pub fn close_side(mut self, close_side: IconSide) -> Self {
166        self.close_side = close_side;
167        self
168    }
169}
170
171#[cfg(feature = "stories")]
172pub use stories::*;
173
174#[cfg(feature = "stories")]
175mod stories {
176    use super::*;
177    use crate::{h_stack, v_stack, Icon, Story};
178    use strum::IntoEnumIterator;
179
180    pub struct TabStory;
181
182    impl Render<Self> for TabStory {
183        type Element = Div<Self>;
184
185        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
186            let git_statuses = GitStatus::iter();
187            let fs_statuses = FileSystemStatus::iter();
188
189            Story::container(cx)
190                .child(Story::title_for::<_, Tab>(cx))
191                .child(
192                    h_stack().child(
193                        v_stack()
194                            .gap_2()
195                            .child(Story::label(cx, "Default"))
196                            .child(Tab::new("default")),
197                    ),
198                )
199                .child(
200                    h_stack().child(
201                        v_stack().gap_2().child(Story::label(cx, "Current")).child(
202                            h_stack()
203                                .gap_4()
204                                .child(
205                                    Tab::new("current")
206                                        .title("Current".to_string())
207                                        .current(true),
208                                )
209                                .child(
210                                    Tab::new("not_current")
211                                        .title("Not Current".to_string())
212                                        .current(false),
213                                ),
214                        ),
215                    ),
216                )
217                .child(
218                    h_stack().child(
219                        v_stack()
220                            .gap_2()
221                            .child(Story::label(cx, "Titled"))
222                            .child(Tab::new("titled").title("label".to_string())),
223                    ),
224                )
225                .child(
226                    h_stack().child(
227                        v_stack()
228                            .gap_2()
229                            .child(Story::label(cx, "With Icon"))
230                            .child(
231                                Tab::new("with_icon")
232                                    .title("label".to_string())
233                                    .icon(Some(Icon::Envelope)),
234                            ),
235                    ),
236                )
237                .child(
238                    h_stack().child(
239                        v_stack()
240                            .gap_2()
241                            .child(Story::label(cx, "Close Side"))
242                            .child(
243                                h_stack()
244                                    .gap_4()
245                                    .child(
246                                        Tab::new("left")
247                                            .title("Left".to_string())
248                                            .close_side(IconSide::Left),
249                                    )
250                                    .child(Tab::new("right").title("Right".to_string())),
251                            ),
252                    ),
253                )
254                .child(
255                    v_stack()
256                        .gap_2()
257                        .child(Story::label(cx, "Git Status"))
258                        .child(h_stack().gap_4().children(git_statuses.map(|git_status| {
259                            Tab::new("git_status")
260                                .title(git_status.to_string())
261                                .git_status(git_status)
262                        }))),
263                )
264                .child(
265                    v_stack()
266                        .gap_2()
267                        .child(Story::label(cx, "File System Status"))
268                        .child(h_stack().gap_4().children(fs_statuses.map(|fs_status| {
269                            Tab::new("file_system_status")
270                                .title(fs_status.to_string())
271                                .fs_status(fs_status)
272                        }))),
273                )
274        }
275    }
276}