Checkpoint

Nate Butler created

Change summary

crates/storybook/src/components.rs              |   6 
crates/storybook/src/components/avatar.rs       |  39 ++++
crates/storybook/src/components/icon_button.rs  |  36 ++-
crates/storybook/src/components/tool_divider.rs |  19 ++
crates/storybook/src/modules.rs                 |   4 
crates/storybook/src/modules/chat_panel.rs      |  74 +++++++++
crates/storybook/src/modules/tab_bar.rs         |  29 ++
crates/storybook/src/modules/title_bar.rs       | 154 +++++++++++++++++++
crates/storybook/src/prelude.rs                 |  26 +++
crates/storybook/src/storybook.rs               |   1 
crates/storybook/src/workspace.rs               |  10 
11 files changed, 373 insertions(+), 25 deletions(-)

Detailed changes

crates/storybook/src/components.rs 🔗

@@ -4,11 +4,15 @@ use gpui2::{
 };
 use std::{marker::PhantomData, rc::Rc};
 
+mod avatar;
 mod icon_button;
 mod tab;
+mod tool_divider;
 
-pub(crate) use icon_button::{icon_button, ButtonVariant};
+pub(crate) use avatar::avatar;
+pub(crate) use icon_button::icon_button;
 pub(crate) use tab::tab;
+pub(crate) use tool_divider::tool_divider;
 
 struct ButtonHandlers<V, D> {
     click: Option<Rc<dyn Fn(&mut V, &D, &mut EventContext<V>)>>,

crates/storybook/src/components/avatar.rs 🔗

@@ -0,0 +1,39 @@
+use crate::prelude::Shape;
+use crate::theme::theme;
+use gpui2::elements::img;
+use gpui2::style::StyleHelpers;
+use gpui2::{ArcCow, IntoElement};
+use gpui2::{Element, ViewContext};
+
+pub type UnknownString = ArcCow<'static, str>;
+
+#[derive(Element)]
+pub(crate) struct Avatar {
+    src: ArcCow<'static, str>,
+    shape: Shape,
+}
+
+pub fn avatar<V: 'static>(src: impl Into<ArcCow<'static, str>>, shape: Shape) -> impl Element<V> {
+    Avatar {
+        src: src.into(),
+        shape,
+    }
+}
+
+impl Avatar {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        let mut img = img();
+
+        if self.shape == Shape::Circle {
+            img = img.rounded_full();
+        } else {
+            img = img.rounded_md();
+        }
+
+        img.uri(self.src.clone())
+            .size_4()
+            .fill(theme.middle.warning.default.foreground)
+    }
+}

crates/storybook/src/components/icon_button.rs 🔗

@@ -1,3 +1,4 @@
+use crate::prelude::{ButtonVariant, UIState};
 use crate::theme::theme;
 use gpui2::elements::svg;
 use gpui2::style::{StyleHelpers, Styleable};
@@ -8,22 +9,33 @@ use gpui2::{Element, ParentElement, ViewContext};
 pub(crate) struct IconButton {
     path: &'static str,
     variant: ButtonVariant,
+    state: UIState,
 }
 
-#[derive(PartialEq)]
-pub enum ButtonVariant {
-    Ghost,
-    Filled,
-}
-
-pub fn icon_button<V: 'static>(path: &'static str, variant: ButtonVariant) -> impl Element<V> {
-    IconButton { path, variant }
+pub fn icon_button<V: 'static>(
+    path: &'static str,
+    variant: ButtonVariant,
+    state: UIState,
+) -> impl Element<V> {
+    IconButton {
+        path,
+        variant,
+        state,
+    }
 }
 
 impl IconButton {
     fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
         let theme = theme(cx);
 
+        let icon_color;
+
+        if self.state == UIState::Disabled {
+            icon_color = theme.highest.base.disabled.foreground;
+        } else {
+            icon_color = theme.highest.base.default.foreground;
+        }
+
         let mut div = div();
         if self.variant == ButtonVariant::Filled {
             div = div.fill(theme.highest.on.default.background);
@@ -39,12 +51,6 @@ impl IconButton {
             .fill(theme.highest.base.hovered.background)
             .active()
             .fill(theme.highest.base.pressed.background)
-            .child(
-                svg()
-                    .path(self.path)
-                    .w_4()
-                    .h_4()
-                    .fill(theme.highest.variant.default.foreground),
-            )
+            .child(svg().path(self.path).w_4().h_4().fill(icon_color))
     }
 }

crates/storybook/src/components/tool_divider.rs 🔗

@@ -0,0 +1,19 @@
+use crate::theme::theme;
+use gpui2::style::StyleHelpers;
+use gpui2::{elements::div, IntoElement};
+use gpui2::{Element, ViewContext};
+
+#[derive(Element)]
+pub(crate) struct ToolDivider {}
+
+pub fn tool_divider<V: 'static>() -> impl Element<V> {
+    ToolDivider {}
+}
+
+impl ToolDivider {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        div().w_px().h_3().fill(theme.lowest.base.default.border)
+    }
+}

crates/storybook/src/modules.rs 🔗

@@ -1,3 +1,7 @@
+mod chat_panel;
 mod tab_bar;
+mod title_bar;
 
+pub(crate) use chat_panel::chat_panel;
 pub(crate) use tab_bar::tab_bar;
+pub(crate) use title_bar::title_bar;

crates/storybook/src/modules/chat_panel.rs 🔗

@@ -0,0 +1,74 @@
+use std::marker::PhantomData;
+
+use crate::components::icon_button;
+use crate::prelude::{ButtonVariant, UIState};
+use crate::theme::theme;
+use gpui2::elements::div::ScrollState;
+use gpui2::style::StyleHelpers;
+use gpui2::{elements::div, IntoElement};
+use gpui2::{Element, ParentElement, ViewContext};
+
+#[derive(Element)]
+pub struct ChatPanel<V: 'static> {
+    view_type: PhantomData<V>,
+    scroll_state: ScrollState,
+}
+
+pub fn chat_panel<V: 'static>(scroll_state: ScrollState) -> ChatPanel<V> {
+    ChatPanel {
+        view_type: PhantomData,
+        scroll_state,
+    }
+}
+
+impl<V: 'static> ChatPanel<V> {
+    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        div()
+            .h_full()
+            .flex()
+            // Header
+            .child(
+                div()
+                    .px_2()
+                    .flex()
+                    .gap_2()
+                    // Nav Buttons
+                    .child("#gpui2"),
+            )
+            // Chat Body
+            .child(
+                div()
+                    .w_full()
+                    .flex()
+                    .flex_col()
+                    .overflow_y_scroll(self.scroll_state.clone())
+                    .child("body"),
+            )
+            // Composer
+            .child(
+                div()
+                    .px_2()
+                    .flex()
+                    .gap_2()
+                    // Nav Buttons
+                    .child(
+                        div()
+                            .flex()
+                            .items_center()
+                            .gap_px()
+                            .child(icon_button(
+                                "icons/plus.svg",
+                                ButtonVariant::Ghost,
+                                UIState::Default,
+                            ))
+                            .child(icon_button(
+                                "icons/split.svg",
+                                ButtonVariant::Ghost,
+                                UIState::Default,
+                            )),
+                    ),
+            )
+    }
+}

crates/storybook/src/modules/tab_bar.rs 🔗

@@ -1,6 +1,7 @@
 use std::marker::PhantomData;
 
-use crate::components::{icon_button, tab, ButtonVariant};
+use crate::components::{icon_button, tab};
+use crate::prelude::{ButtonVariant, UIState};
 use crate::theme::theme;
 use gpui2::elements::div::ScrollState;
 use gpui2::style::StyleHelpers;
@@ -40,15 +41,23 @@ impl<V: 'static> TabBar<V> {
                             .flex()
                             .items_center()
                             .gap_px()
-                            .child(icon_button("icons/arrow_left.svg", ButtonVariant::Filled))
-                            .child(icon_button("icons/arrow_right.svg", ButtonVariant::Ghost)),
+                            .child(icon_button(
+                                "icons/arrow_left.svg",
+                                ButtonVariant::Ghost,
+                                UIState::Default,
+                            ))
+                            .child(icon_button(
+                                "icons/arrow_right.svg",
+                                ButtonVariant::Ghost,
+                                UIState::Disabled,
+                            )),
                     ),
             )
             .child(
                 div().w_0().flex_1().h_full().child(
                     div()
                         .flex()
-                        .gap_px()
+                        .gap_8()
                         .overflow_x_scroll(self.scroll_state.clone())
                         .child(tab("Cargo.toml", false))
                         .child(tab("Channels Panel", true))
@@ -74,8 +83,16 @@ impl<V: 'static> TabBar<V> {
                             .flex()
                             .items_center()
                             .gap_px()
-                            .child(icon_button("icons/plus.svg", ButtonVariant::Ghost))
-                            .child(icon_button("icons/split.svg", ButtonVariant::Ghost)),
+                            .child(icon_button(
+                                "icons/plus.svg",
+                                ButtonVariant::Ghost,
+                                UIState::Default,
+                            ))
+                            .child(icon_button(
+                                "icons/split.svg",
+                                ButtonVariant::Ghost,
+                                UIState::Default,
+                            )),
                     ),
             )
     }

crates/storybook/src/modules/title_bar.rs 🔗

@@ -0,0 +1,154 @@
+use std::marker::PhantomData;
+
+use crate::components::{avatar, icon_button, tool_divider};
+use crate::prelude::{ButtonVariant, Shape, UIState};
+use crate::theme::theme;
+use gpui2::style::{StyleHelpers, Styleable};
+use gpui2::{elements::div, IntoElement};
+use gpui2::{Element, ParentElement, ViewContext};
+
+#[derive(Element)]
+pub struct TitleBar<V: 'static> {
+    view_type: PhantomData<V>,
+}
+
+pub fn title_bar<V: 'static>() -> TitleBar<V> {
+    TitleBar {
+        view_type: PhantomData,
+    }
+}
+
+impl<V: 'static> TitleBar<V> {
+    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        div()
+            .flex()
+            .items_center()
+            .justify_between()
+            .w_full()
+            .h_8()
+            .fill(theme.lowest.base.default.background)
+            .child(
+                div()
+                    .flex()
+                    .items_center()
+                    .h_full()
+                    .gap_4()
+                    .px_2()
+                    // === Traffic Lights === //
+                    .child(
+                        div()
+                            .flex()
+                            .items_center()
+                            .gap_2()
+                            .child(
+                                div()
+                                    .w_3()
+                                    .h_3()
+                                    .rounded_full()
+                                    .fill(theme.lowest.positive.default.foreground),
+                            )
+                            .child(
+                                div()
+                                    .w_3()
+                                    .h_3()
+                                    .rounded_full()
+                                    .fill(theme.lowest.warning.default.foreground),
+                            )
+                            .child(
+                                div()
+                                    .w_3()
+                                    .h_3()
+                                    .rounded_full()
+                                    .fill(theme.lowest.negative.default.foreground),
+                            ),
+                    )
+                    // === Project Info === //
+                    .child(
+                        div()
+                            .flex()
+                            .items_center()
+                            .gap_1()
+                            .child(
+                                div()
+                                    .h_full()
+                                    .flex()
+                                    .items_center()
+                                    .justify_center()
+                                    .px_2()
+                                    .rounded_md()
+                                    .hover()
+                                    .fill(theme.lowest.base.hovered.background)
+                                    .active()
+                                    .fill(theme.lowest.base.pressed.background)
+                                    .child(div().text_sm().child("project")),
+                            )
+                            .child(
+                                div()
+                                    .h_full()
+                                    .flex()
+                                    .items_center()
+                                    .justify_center()
+                                    .px_2()
+                                    .rounded_md()
+                                    .text_color(theme.lowest.variant.default.foreground)
+                                    .hover()
+                                    .fill(theme.lowest.base.hovered.background)
+                                    .active()
+                                    .fill(theme.lowest.base.pressed.background)
+                                    .child(div().text_sm().child("branch")),
+                            ),
+                    ),
+            )
+            .child(
+                div()
+                    .flex()
+                    .items_center()
+                    .child(
+                        div()
+                            .px_2()
+                            .flex()
+                            .items_center()
+                            .gap_1()
+                            .child(icon_button(
+                                "icons/stop_sharing.svg",
+                                ButtonVariant::Ghost,
+                                UIState::Default,
+                            ))
+                            .child(icon_button(
+                                "icons/exit.svg",
+                                ButtonVariant::Ghost,
+                                UIState::Default,
+                            )),
+                    )
+                    .child(tool_divider())
+                    .child(
+                        div()
+                            .px_2()
+                            .flex()
+                            .items_center()
+                            .gap_1()
+                            .child(icon_button(
+                                "icons/radix/mic.svg",
+                                ButtonVariant::Ghost,
+                                UIState::Default,
+                            ))
+                            .child(icon_button(
+                                "icons/radix/speaker-loud.svg",
+                                ButtonVariant::Ghost,
+                                UIState::Default,
+                            ))
+                            .child(icon_button(
+                                "icons/radix/desktop.svg",
+                                ButtonVariant::Ghost,
+                                UIState::Default,
+                            )),
+                    )
+                    .child(div().px_2().flex().items_center().child(avatar(
+                        "https://avatars.githubusercontent.com/u/1714999?v=4",
+                        Shape::Squircle,
+                    ))),
+            )
+    }
+}

crates/storybook/src/prelude.rs 🔗

@@ -0,0 +1,26 @@
+#[derive(PartialEq)]
+pub enum ButtonVariant {
+    Ghost,
+    Filled,
+}
+
+#[derive(PartialEq)]
+pub enum Shape {
+    Circle,
+    Squircle,
+}
+
+#[derive(PartialEq)]
+pub enum UIState {
+    Default,
+    Hovered,
+    Active,
+    Focused,
+    Disabled,
+}
+
+#[derive(PartialEq)]
+pub enum UIToggleState {
+    Default,
+    Enabled,
+}

crates/storybook/src/workspace.rs 🔗

@@ -1,4 +1,8 @@
-use crate::{collab_panel::collab_panel, modules::tab_bar, theme::theme};
+use crate::{
+    collab_panel::collab_panel,
+    modules::{chat_panel, tab_bar, title_bar},
+    theme::theme,
+};
 use gpui2::{
     elements::{div, div::ScrollState, img, svg},
     style::{StyleHelpers, Styleable},
@@ -30,7 +34,7 @@ impl WorkspaceElement {
             .items_start()
             .text_color(theme.lowest.base.default.foreground)
             .fill(theme.middle.base.default.background)
-            .child(titlebar())
+            .child(title_bar())
             .child(
                 div()
                     .flex_1()
@@ -52,7 +56,7 @@ impl WorkspaceElement {
                                     .child(tab_bar(self.tab_bar_scroll_state.clone())),
                             ),
                     )
-                    .child(collab_panel(self.right_scroll_state.clone())),
+                    .child(chat_panel(self.right_scroll_state.clone())),
             )
             .child(statusbar())
     }