Add `TabBar` component

Marshall Bowers created

Change summary

crates/storybook2/src/stories/components.rs         |  1 
crates/storybook2/src/stories/components/tab_bar.rs | 56 +++++++++
crates/storybook2/src/story_selector.rs             |  2 
crates/ui2/src/components.rs                        |  2 
crates/ui2/src/components/tab_bar.rs                | 85 +++++++++++++++
5 files changed, 146 insertions(+)

Detailed changes

crates/storybook2/src/stories/components/tab_bar.rs 🔗

@@ -0,0 +1,56 @@
+use std::marker::PhantomData;
+
+use ui::prelude::*;
+use ui::{Tab, TabBar};
+
+use crate::story::Story;
+
+#[derive(Element)]
+pub struct TabBarStory<S: 'static + Send + Sync + Clone> {
+    state_type: PhantomData<S>,
+}
+
+impl<S: 'static + Send + Sync + Clone> TabBarStory<S> {
+    pub fn new() -> Self {
+        Self {
+            state_type: PhantomData,
+        }
+    }
+
+    fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
+        Story::container(cx)
+            .child(Story::title_for::<_, TabBar<S>>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(TabBar::new(vec![
+                Tab::new()
+                    .title("Cargo.toml".to_string())
+                    .current(false)
+                    .git_status(GitStatus::Modified),
+                Tab::new()
+                    .title("Channels Panel".to_string())
+                    .current(false),
+                Tab::new()
+                    .title("channels_panel.rs".to_string())
+                    .current(true)
+                    .git_status(GitStatus::Modified),
+                Tab::new()
+                    .title("workspace.rs".to_string())
+                    .current(false)
+                    .git_status(GitStatus::Modified),
+                Tab::new()
+                    .title("icon_button.rs".to_string())
+                    .current(false),
+                Tab::new()
+                    .title("storybook.rs".to_string())
+                    .current(false)
+                    .git_status(GitStatus::Created),
+                Tab::new().title("theme.rs".to_string()).current(false),
+                Tab::new()
+                    .title("theme_registry.rs".to_string())
+                    .current(false),
+                Tab::new()
+                    .title("styleable_helpers.rs".to_string())
+                    .current(false),
+            ]))
+    }
+}

crates/storybook2/src/story_selector.rs 🔗

@@ -39,6 +39,7 @@ pub enum ComponentStory {
     Panel,
     ProjectPanel,
     Tab,
+    TabBar,
     Terminal,
     Workspace,
 }
@@ -55,6 +56,7 @@ impl ComponentStory {
             Self::Panel => components::panel::PanelStory::new().into_any(),
             Self::ProjectPanel => components::project_panel::ProjectPanelStory::new().into_any(),
             Self::Tab => components::tab::TabStory::new().into_any(),
+            Self::TabBar => components::tab_bar::TabBarStory::new().into_any(),
             Self::Terminal => components::terminal::TerminalStory::new().into_any(),
             Self::Workspace => components::workspace::WorkspaceStory::new().into_any(),
         }

crates/ui2/src/components.rs 🔗

@@ -7,6 +7,7 @@ mod panes;
 mod project_panel;
 mod status_bar;
 mod tab;
+mod tab_bar;
 mod terminal;
 mod workspace;
 
@@ -19,5 +20,6 @@ pub use panes::*;
 pub use project_panel::*;
 pub use status_bar::*;
 pub use tab::*;
+pub use tab_bar::*;
 pub use terminal::*;
 pub use workspace::*;

crates/ui2/src/components/tab_bar.rs 🔗

@@ -0,0 +1,85 @@
+use std::marker::PhantomData;
+
+use crate::prelude::*;
+use crate::{theme, Icon, IconButton, Tab};
+
+#[derive(Element)]
+pub struct TabBar<S: 'static + Send + Sync + Clone> {
+    state_type: PhantomData<S>,
+    scroll_state: ScrollState,
+    tabs: Vec<Tab<S>>,
+}
+
+impl<S: 'static + Send + Sync + Clone> TabBar<S> {
+    pub fn new(tabs: Vec<Tab<S>>) -> Self {
+        Self {
+            state_type: PhantomData,
+            scroll_state: ScrollState::default(),
+            tabs,
+        }
+    }
+
+    pub fn bind_scroll_state(&mut self, scroll_state: ScrollState) {
+        self.scroll_state = scroll_state;
+    }
+
+    fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
+        let theme = theme(cx);
+        let can_navigate_back = true;
+        let can_navigate_forward = false;
+
+        div()
+            .w_full()
+            .flex()
+            .fill(theme.middle.base.default.background)
+            // Left Side
+            .child(
+                div()
+                    .px_1()
+                    .flex()
+                    .flex_none()
+                    .gap_2()
+                    // Nav Buttons
+                    .child(
+                        div()
+                            .flex()
+                            .items_center()
+                            .gap_px()
+                            .child(
+                                IconButton::new(Icon::ArrowLeft)
+                                    .state(InteractionState::Enabled.if_enabled(can_navigate_back)),
+                            )
+                            .child(
+                                IconButton::new(Icon::ArrowRight).state(
+                                    InteractionState::Enabled.if_enabled(can_navigate_forward),
+                                ),
+                            ),
+                    ),
+            )
+            .child(
+                div().w_0().flex_1().h_full().child(
+                    div()
+                        .flex()
+                        .overflow_x_scroll(self.scroll_state.clone())
+                        .children(self.tabs.clone()),
+                ),
+            )
+            // Right Side
+            .child(
+                div()
+                    .px_1()
+                    .flex()
+                    .flex_none()
+                    .gap_2()
+                    // Nav Buttons
+                    .child(
+                        div()
+                            .flex()
+                            .items_center()
+                            .gap_px()
+                            .child(IconButton::new(Icon::Plus))
+                            .child(IconButton::new(Icon::Split)),
+                    ),
+            )
+    }
+}