Limit number of participants shown in channel face piles

Max Brunsfeld and Mikayla created

Co-authored-by: Mikayla <mikayla@zed.dev>

Change summary

crates/collab_ui/src/collab_panel.rs  | 45 ++++++++++++++++++++--------
crates/collab_ui/src/face_pile.rs     |  1 
crates/theme/src/theme.rs             |  1 
styles/src/style_tree/collab_panel.ts |  9 +++++
4 files changed, 43 insertions(+), 13 deletions(-)

Detailed changes

crates/collab_ui/src/collab_panel.rs 🔗

@@ -1514,6 +1514,8 @@ impl CollabPanel {
     ) -> AnyElement<Self> {
         let channel_id = channel.id;
 
+        const FACEPILE_LIMIT: usize = 4;
+
         MouseEventHandler::<Channel, Self>::new(channel.id as usize, cx, |state, cx| {
             Flex::row()
                 .with_child(
@@ -1532,20 +1534,37 @@ impl CollabPanel {
                         .left()
                         .flex(1., true),
                 )
-                .with_child(
-                    FacePile::new(theme.face_overlap).with_children(
-                        self.channel_store
-                            .read(cx)
-                            .channel_participants(channel_id)
-                            .iter()
-                            .filter_map(|user| {
-                                Some(
-                                    Image::from_data(user.avatar.clone()?)
-                                        .with_style(theme.contact_avatar),
+                .with_children({
+                    let participants = self.channel_store.read(cx).channel_participants(channel_id);
+                    if !participants.is_empty() {
+                        let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
+
+                        Some(
+                            FacePile::new(theme.face_overlap)
+                                .with_children(
+                                    participants
+                                        .iter()
+                                        .filter_map(|user| {
+                                            Some(
+                                                Image::from_data(user.avatar.clone()?)
+                                                    .with_style(theme.contact_avatar),
+                                            )
+                                        })
+                                        .take(FACEPILE_LIMIT),
                                 )
-                            }),
-                    ),
-                )
+                                .with_children((extra_count > 0).then(|| {
+                                    Label::new(
+                                        format!("+{}", extra_count),
+                                        theme.extra_participant_label.text.clone(),
+                                    )
+                                    .contained()
+                                    .with_style(theme.extra_participant_label.container)
+                                })),
+                        )
+                    } else {
+                        None
+                    }
+                })
                 .align_children_center()
                 .constrained()
                 .with_height(theme.row_height)

crates/collab_ui/src/face_pile.rs 🔗

@@ -68,6 +68,7 @@ impl<V: View> Element<V> for FacePile<V> {
         for face in self.faces.iter_mut().rev() {
             let size = face.size();
             origin_x -= size.x();
+            let origin_y = origin_y + (bounds.height() - size.y()) / 2.0;
             scene.paint_layer(None, |scene| {
                 face.paint(scene, vec2f(origin_x, origin_y), visible_bounds, view, cx);
             });

crates/theme/src/theme.rs 🔗

@@ -241,6 +241,7 @@ pub struct CollabPanel {
     pub project_row: Toggleable<Interactive<ProjectRow>>,
     pub tree_branch: Toggleable<Interactive<TreeBranch>>,
     pub contact_avatar: ImageStyle,
+    pub extra_participant_label: ContainedText,
     pub contact_status_free: ContainerStyle,
     pub contact_status_busy: ContainerStyle,
     pub contact_username: ContainedText,

styles/src/style_tree/collab_panel.ts 🔗

@@ -245,6 +245,15 @@ export default function contacts_panel(): any {
             corner_radius: 10,
             width: 20,
         },
+        extra_participant_label: {
+            corner_radius: 10,
+            padding: {
+                left: 10,
+                right: 4,
+            },
+            background: background(layer, "hovered"),
+            ...text(layer, "ui_sans", "hovered", { size: "xs" })
+        },
         contact_status_free: indicator({ layer, color: "positive" }),
         contact_status_busy: indicator({ layer, color: "negative" }),
         contact_username: {