Add facepile, indicator, follow_group

Nate Butler created

Change summary

crates/storybook/src/prelude.rs                   |  2 
crates/storybook/src/ui.rs                        | 27 +++----
crates/storybook/src/ui/component.rs              |  2 
crates/storybook/src/ui/component/facepile.rs     | 31 ++++++++++
crates/storybook/src/ui/component/follow_group.rs | 52 +++++++++++++++++
crates/storybook/src/ui/element.rs                |  1 
crates/storybook/src/ui/element/avatar.rs         |  2 
crates/storybook/src/ui/element/indicator.rs      | 32 ++++++++++
crates/storybook/src/ui/module/title_bar.rs       | 12 +++
9 files changed, 142 insertions(+), 19 deletions(-)

Detailed changes

crates/storybook/src/prelude.rs 🔗

@@ -5,7 +5,7 @@ pub enum ButtonVariant {
     Filled,
 }
 
-#[derive(Default, PartialEq)]
+#[derive(Default, PartialEq, Clone, Copy)]
 pub enum Shape {
     #[default]
     Circle,

crates/storybook/src/ui.rs 🔗

@@ -2,20 +2,17 @@ mod component;
 mod element;
 mod module;
 
-pub use component::tab::tab;
-pub use component::tab::Tab;
+pub use component::facepile::*;
+pub use component::follow_group::*;
+pub use component::tab::*;
 
-pub use module::chat_panel::chat_panel;
-pub use module::status_bar::status_bar;
-pub use module::status_bar::StatusBar;
-pub use module::tab_bar::tab_bar;
-pub use module::title_bar::title_bar;
+pub use module::chat_panel::*;
+pub use module::status_bar::*;
+pub use module::tab_bar::*;
+pub use module::title_bar::*;
 
-pub use element::avatar::avatar;
-pub use element::avatar::Avatar;
-pub use element::icon_button::icon_button;
-pub use element::icon_button::IconButton;
-pub use element::text_button::text_button;
-pub use element::text_button::TextButton;
-pub use element::tool_divider::tool_divider;
-pub use element::tool_divider::ToolDivider;
+pub use element::avatar::*;
+pub use element::icon_button::*;
+pub use element::indicator::*;
+pub use element::text_button::*;
+pub use element::tool_divider::*;

crates/storybook/src/ui/component/facepile.rs 🔗

@@ -0,0 +1,31 @@
+use crate::theme::theme;
+use crate::ui::Avatar;
+use gpui2::style::StyleHelpers;
+use gpui2::{elements::div, IntoElement};
+use gpui2::{Element, ParentElement, ViewContext};
+
+#[derive(Element)]
+pub struct Facepile {
+    players: Vec<Avatar>,
+}
+
+pub fn facepile(players: Vec<Avatar>) -> Facepile {
+    Facepile { players }
+}
+
+impl Facepile {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+        let player_list = self
+            .players
+            .iter()
+            .map(|player| div().right_1().child(player.clone()));
+
+        div()
+            .relative()
+            .p_1()
+            .flex()
+            .items_center()
+            .children(player_list)
+    }
+}

crates/storybook/src/ui/component/follow_group.rs 🔗

@@ -0,0 +1,52 @@
+use crate::theme::theme;
+use crate::ui::{facepile, indicator, Avatar};
+use gpui2::style::StyleHelpers;
+use gpui2::{elements::div, IntoElement};
+use gpui2::{Element, ParentElement, ViewContext};
+
+#[derive(Element)]
+pub struct FollowGroup {
+    player: usize,
+    players: Vec<Avatar>,
+}
+
+pub fn follow_group(players: Vec<Avatar>) -> FollowGroup {
+    FollowGroup { player: 0, players }
+}
+
+impl FollowGroup {
+    pub fn player(mut self, player: usize) -> Self {
+        self.player = player;
+        self
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+        let player_bg = theme.players[self.player].selection;
+
+        div()
+            .h_full()
+            .flex()
+            .flex_col()
+            .gap_px()
+            .justify_center()
+            .child(
+                div()
+                    .flex()
+                    .justify_center()
+                    .w_full()
+                    .child(indicator().player(self.player)),
+            )
+            .child(
+                div()
+                    .flex()
+                    .items_center()
+                    .justify_center()
+                    .h_6()
+                    .px_1()
+                    .rounded_lg()
+                    .fill(player_bg)
+                    .child(facepile(self.players.clone())),
+            )
+    }
+}

crates/storybook/src/ui/element.rs 🔗

@@ -1,4 +1,5 @@
 pub(crate) mod avatar;
 pub(crate) mod icon_button;
+pub(crate) mod indicator;
 pub(crate) mod text_button;
 pub(crate) mod tool_divider;

crates/storybook/src/ui/element/avatar.rs 🔗

@@ -7,7 +7,7 @@ use gpui2::{Element, ViewContext};
 
 pub type UnknownString = ArcCow<'static, str>;
 
-#[derive(Element)]
+#[derive(Element, Clone)]
 pub struct Avatar {
     src: ArcCow<'static, str>,
     shape: Shape,

crates/storybook/src/ui/element/indicator.rs 🔗

@@ -0,0 +1,32 @@
+use crate::theme::theme;
+use gpui2::style::StyleHelpers;
+use gpui2::{elements::div, IntoElement};
+use gpui2::{Element, ViewContext};
+
+#[derive(Element)]
+pub struct Indicator {
+    player: usize,
+}
+
+pub fn indicator() -> Indicator {
+    Indicator { player: 0 }
+}
+
+impl Indicator {
+    pub fn player(mut self, player: usize) -> Self {
+        self.player = player;
+        self
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+        let player_color = theme.players[self.player].cursor;
+
+        div()
+            .w_4()
+            .h_1()
+            .rounded_bl_sm()
+            .rounded_br_sm()
+            .fill(player_color)
+    }
+}

crates/storybook/src/ui/module/title_bar.rs 🔗

@@ -2,7 +2,7 @@ use std::marker::PhantomData;
 
 use crate::prelude::Shape;
 use crate::theme::theme;
-use crate::ui::{avatar, icon_button, text_button, tool_divider};
+use crate::ui::{avatar, follow_group, icon_button, text_button, tool_divider};
 use gpui2::style::StyleHelpers;
 use gpui2::{elements::div, IntoElement};
 use gpui2::{Element, ParentElement, ViewContext};
@@ -21,6 +21,10 @@ pub fn title_bar<V: 'static>() -> TitleBar<V> {
 impl<V: 'static> TitleBar<V> {
     fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
         let theme = theme(cx);
+        let player_list = vec![
+            avatar("https://avatars.githubusercontent.com/u/1714999?v=4"),
+            avatar("https://avatars.githubusercontent.com/u/1714999?v=4"),
+        ];
 
         div()
             .flex()
@@ -70,9 +74,13 @@ impl<V: 'static> TitleBar<V> {
                             .flex()
                             .items_center()
                             .gap_1()
+                            .child(text_button("maxbrunsfeld"))
                             .child(text_button("zed"))
                             .child(text_button("nate/gpui2-ui-components")),
-                    ),
+                    )
+                    .child(follow_group(player_list.clone()).player(0))
+                    .child(follow_group(player_list.clone()).player(1))
+                    .child(follow_group(player_list.clone()).player(2)),
             )
             .child(
                 div()