Add `CollabPanel` component

Marshall Bowers created

Change summary

crates/storybook2/src/stories/components.rs              |   1 
crates/storybook2/src/stories/components/collab_panel.rs |  26 +
crates/storybook2/src/story_selector.rs                  |   2 
crates/ui2/src/components.rs                             |   2 
crates/ui2/src/components/collab_panel.rs                | 160 ++++++++++
crates/ui2/src/static_data.rs                            |  83 +++++
6 files changed, 271 insertions(+), 3 deletions(-)

Detailed changes

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

@@ -0,0 +1,26 @@
+use std::marker::PhantomData;
+
+use ui::prelude::*;
+use ui::CollabPanel;
+
+use crate::story::Story;
+
+#[derive(Element)]
+pub struct CollabPanelStory<S: 'static + Send + Sync + Clone> {
+    state_type: PhantomData<S>,
+}
+
+impl<S: 'static + Send + Sync + Clone> CollabPanelStory<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::<_, CollabPanel<S>>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(CollabPanel::new(ScrollState::default()))
+    }
+}

crates/storybook2/src/story_selector.rs 🔗

@@ -40,6 +40,7 @@ pub enum ComponentStory {
     Breadcrumb,
     Buffer,
     ChatPanel,
+    CollabPanel,
     Facepile,
     Panel,
     ProjectPanel,
@@ -63,6 +64,7 @@ impl ComponentStory {
             Self::Buffer => components::buffer::BufferStory::new().into_any(),
             Self::Breadcrumb => components::breadcrumb::BreadcrumbStory::new().into_any(),
             Self::ChatPanel => components::chat_panel::ChatPanelStory::new().into_any(),
+            Self::CollabPanel => components::collab_panel::CollabPanelStory::new().into_any(),
             Self::Facepile => components::facepile::FacepileStory::new().into_any(),
             Self::Panel => components::panel::PanelStory::new().into_any(),
             Self::ProjectPanel => components::project_panel::ProjectPanelStory::new().into_any(),

crates/ui2/src/components.rs 🔗

@@ -2,6 +2,7 @@ mod assistant_panel;
 mod breadcrumb;
 mod buffer;
 mod chat_panel;
+mod collab_panel;
 mod editor_pane;
 mod facepile;
 mod icon_button;
@@ -23,6 +24,7 @@ pub use assistant_panel::*;
 pub use breadcrumb::*;
 pub use buffer::*;
 pub use chat_panel::*;
+pub use collab_panel::*;
 pub use editor_pane::*;
 pub use facepile::*;
 pub use icon_button::*;

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

@@ -0,0 +1,160 @@
+use std::marker::PhantomData;
+
+use gpui3::{img, svg, ArcCow};
+
+use crate::prelude::*;
+use crate::theme::{theme, Theme};
+use crate::{
+    static_collab_panel_channels, static_collab_panel_current_call, v_stack, Icon, List,
+    ListHeader, ToggleState,
+};
+
+#[derive(Element)]
+pub struct CollabPanel<S: 'static + Send + Sync + Clone> {
+    view_type: PhantomData<S>,
+    scroll_state: ScrollState,
+}
+
+impl<S: 'static + Send + Sync + Clone> CollabPanel<S> {
+    pub fn new(scroll_state: ScrollState) -> Self {
+        Self {
+            view_type: PhantomData,
+            scroll_state,
+        }
+    }
+
+    fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
+        let theme = theme(cx);
+
+        v_stack()
+            .w_64()
+            .h_full()
+            .fill(theme.middle.base.default.background)
+            .child(
+                v_stack()
+                    .w_full()
+                    .overflow_y_scroll(self.scroll_state.clone())
+                    .child(
+                        div()
+                            .fill(theme.lowest.base.default.background)
+                            .pb_1()
+                            .border_color(theme.lowest.base.default.border)
+                            .border_b()
+                            .child(
+                                List::new(static_collab_panel_current_call())
+                                    .header(
+                                        ListHeader::new("CRDB")
+                                            .left_icon(Icon::Hash.into())
+                                            .set_toggle(ToggleState::Toggled),
+                                    )
+                                    .set_toggle(ToggleState::Toggled),
+                            ),
+                    )
+                    .child(
+                        v_stack().py_1().child(
+                            List::new(static_collab_panel_channels())
+                                .header(
+                                    ListHeader::new("CHANNELS").set_toggle(ToggleState::Toggled),
+                                )
+                                .empty_message("No channels yet. Add a channel to get started.")
+                                .set_toggle(ToggleState::Toggled),
+                        ),
+                    )
+                    .child(
+                        v_stack().py_1().child(
+                            List::new(static_collab_panel_current_call())
+                                .header(
+                                    ListHeader::new("CONTACTS – ONLINE")
+                                        .set_toggle(ToggleState::Toggled),
+                                )
+                                .set_toggle(ToggleState::Toggled),
+                        ),
+                    )
+                    .child(
+                        v_stack().py_1().child(
+                            List::new(static_collab_panel_current_call())
+                                .header(
+                                    ListHeader::new("CONTACTS – OFFLINE")
+                                        .set_toggle(ToggleState::NotToggled),
+                                )
+                                .set_toggle(ToggleState::NotToggled),
+                        ),
+                    ),
+            )
+            .child(
+                div()
+                    .h_7()
+                    .px_2()
+                    .border_t()
+                    .border_color(theme.middle.variant.default.border)
+                    .flex()
+                    .items_center()
+                    .child(
+                        div()
+                            .text_sm()
+                            .text_color(theme.middle.variant.default.foreground)
+                            .child("Find..."),
+                    ),
+            )
+    }
+
+    fn list_section_header(
+        &self,
+        label: impl Into<ArcCow<'static, str>>,
+        expanded: bool,
+        theme: &Theme,
+    ) -> impl Element<State = S> {
+        div()
+            .h_7()
+            .px_2()
+            .flex()
+            .justify_between()
+            .items_center()
+            .child(div().flex().gap_1().text_sm().child(label.into()))
+            .child(
+                div().flex().h_full().gap_1().items_center().child(
+                    svg()
+                        .path(if expanded {
+                            "icons/caret_down.svg"
+                        } else {
+                            "icons/caret_up.svg"
+                        })
+                        .w_3p5()
+                        .h_3p5()
+                        .fill(theme.middle.variant.default.foreground),
+                ),
+            )
+    }
+
+    fn list_item(
+        &self,
+        avatar_uri: impl Into<ArcCow<'static, str>>,
+        label: impl Into<ArcCow<'static, str>>,
+        theme: &Theme,
+    ) -> impl Element<State = S> {
+        div()
+            .h_7()
+            .px_2()
+            .flex()
+            .items_center()
+            // .hover()
+            // .fill(theme.lowest.variant.hovered.background)
+            // .active()
+            // .fill(theme.lowest.variant.pressed.background)
+            .child(
+                div()
+                    .flex()
+                    .items_center()
+                    .gap_1()
+                    .text_sm()
+                    .child(
+                        img()
+                            .uri(avatar_uri)
+                            .size_3p5()
+                            .rounded_full()
+                            .fill(theme.middle.positive.default.foreground),
+                    )
+                    .child(label.into()),
+            )
+    }
+}

crates/ui2/src/static_data.rs 🔗

@@ -5,9 +5,9 @@ use rand::Rng;
 
 use crate::{
     Buffer, BufferRow, BufferRows, Editor, FileSystemStatus, GitStatus, HighlightColor,
-    HighlightedLine, HighlightedText, Icon, Label, LabelColor, ListEntry, ListItem, Livestream,
-    MicStatus, Player, PlayerCallStatus, PlayerWithCallStatus, ScreenShareStatus, Symbol, Tab,
-    Theme, ToggleState, VideoStatus,
+    HighlightedLine, HighlightedText, Icon, Label, LabelColor, ListEntry, ListEntrySize, ListItem,
+    Livestream, MicStatus, Player, PlayerCallStatus, PlayerWithCallStatus, ScreenShareStatus,
+    Symbol, Tab, Theme, ToggleState, VideoStatus,
 };
 
 pub fn static_tabs_example<S: 'static + Send + Sync + Clone>() -> Vec<Tab<S>> {
@@ -464,6 +464,83 @@ pub fn static_project_panel_single_items<S: 'static + Send + Sync + Clone>() ->
     .collect()
 }
 
+pub fn static_collab_panel_current_call<S: 'static + Send + Sync + Clone>() -> Vec<ListItem<S>> {
+    vec![
+        ListEntry::new(Label::new("as-cii")).left_avatar("http://github.com/as-cii.png?s=50"),
+        ListEntry::new(Label::new("nathansobo"))
+            .left_avatar("http://github.com/nathansobo.png?s=50"),
+        ListEntry::new(Label::new("maxbrunsfeld"))
+            .left_avatar("http://github.com/maxbrunsfeld.png?s=50"),
+    ]
+    .into_iter()
+    .map(From::from)
+    .collect()
+}
+
+pub fn static_collab_panel_channels<S: 'static + Send + Sync + Clone>() -> Vec<ListItem<S>> {
+    vec![
+        ListEntry::new(Label::new("zed"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(0),
+        ListEntry::new(Label::new("community"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(1),
+        ListEntry::new(Label::new("dashboards"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+        ListEntry::new(Label::new("feedback"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+        ListEntry::new(Label::new("teams-in-channels-alpha"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+        ListEntry::new(Label::new("current-projects"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(1),
+        ListEntry::new(Label::new("codegen"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+        ListEntry::new(Label::new("gpui2"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+        ListEntry::new(Label::new("livestreaming"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+        ListEntry::new(Label::new("open-source"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+        ListEntry::new(Label::new("replace"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+        ListEntry::new(Label::new("semantic-index"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+        ListEntry::new(Label::new("vim"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+        ListEntry::new(Label::new("web-tech"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+    ]
+    .into_iter()
+    .map(From::from)
+    .collect()
+}
+
 pub fn empty_editor_example<S: 'static + Send + Sync + Clone>() -> Editor<S> {
     Editor {
         tabs: static_tabs_example(),