tab.rs

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