Indicate collaborators' presence (grayscale), speaking and muted status

Max Brunsfeld created

Change summary

crates/collab_ui2/src/collab_titlebar_item.rs | 167 +++++++++++---------
crates/collab_ui2/src/face_pile.rs            |   5 
crates/ui2/src/components/avatar.rs           |  69 +++++---
3 files changed, 132 insertions(+), 109 deletions(-)

Detailed changes

crates/collab_ui2/src/collab_titlebar_item.rs 🔗

@@ -1,15 +1,14 @@
 use crate::face_pile::FacePile;
-use call::{ActiveCall, Room};
+use call::{ActiveCall, ParticipantLocation, Room};
 use client::{proto::PeerId, Client, ParticipantIndex, User, UserStore};
 use gpui::{
-    actions, canvas, div, point, px, rems, AppContext, Div, Element, InteractiveElement,
-    IntoElement, Model, ParentElement, Path, Render, RenderOnce, Stateful,
-    StatefulInteractiveElement, Styled, Subscription, ViewContext, VisualContext, WeakView,
-    WindowBounds,
+    actions, canvas, div, point, px, rems, AppContext, Div, Element, Hsla, InteractiveElement,
+    IntoElement, Model, ParentElement, Path, Render, Stateful, StatefulInteractiveElement, Styled,
+    Subscription, ViewContext, VisualContext, WeakView, WindowBounds,
 };
 use project::{Project, RepositoryEntry};
 use std::sync::Arc;
-use theme::ActiveTheme;
+use theme::{ActiveTheme, PlayerColors};
 use ui::{
     h_stack, popover_menu, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon,
     IconButton, IconElement, KeyBinding, Tooltip,
@@ -43,11 +42,8 @@ pub fn init(cx: &mut AppContext) {
 
 pub struct CollabTitlebarItem {
     project: Model<Project>,
-    #[allow(unused)] // todo!()
     user_store: Model<UserStore>,
-    #[allow(unused)] // todo!()
     client: Arc<Client>,
-    #[allow(unused)] // todo!()
     workspace: WeakView<Workspace>,
     //branch_popover: Option<ViewHandle<BranchList>>,
     //project_popover: Option<ViewHandle<recent_projects::RecentProjects>>,
@@ -92,62 +88,64 @@ impl Render for CollabTitlebarItem {
                     .child(self.render_project_name(cx))
                     .children(self.render_project_branch(cx))
                     .when_some(
-                        current_user.clone().zip(room.clone()).zip(project_id),
-                        |this, ((current_user, room), project_id)| {
-                            let remote_participants = room
-                                .read(cx)
-                                .remote_participants()
-                                .values()
-                                .map(|participant| {
-                                    (
-                                        participant.user.clone(),
-                                        participant.participant_index,
-                                        participant.peer_id,
-                                    )
-                                })
-                                .collect::<Vec<_>>();
-
-                            this.children(
-                                self.render_collaborator(
-                                    &current_user,
-                                    client.peer_id().expect("todo!()"),
-                                    &room,
-                                    project_id,
-                                    &remote_participants,
-                                    cx,
-                                )
-                                .map(|pile| pile.render(cx)),
-                            )
+                        current_user
+                            .clone()
+                            .zip(client.peer_id())
+                            .zip(room.clone())
+                            .zip(project_id),
+                        |this, (((current_user, peer_id), room), project_id)| {
+                            let player_colors = cx.theme().players();
+                            let room = room.read(cx);
+                            let mut remote_participants =
+                                room.remote_participants().values().collect::<Vec<_>>();
+                            remote_participants.sort_by_key(|p| p.participant_index.0);
+
+                            this.children(self.render_collaborator(
+                                &current_user,
+                                peer_id,
+                                ParticipantLocation::SharedProject { project_id },
+                                room.is_speaking(),
+                                room.is_muted(cx),
+                                &room,
+                                project_id,
+                                &current_user,
+                            ))
                             .children(
-                                remote_participants.iter().filter_map(
-                                    |(user, participant_index, peer_id)| {
-                                        let peer_id = *peer_id;
-                                        let face_pile = self
-                                            .render_collaborator(
-                                                user,
-                                                peer_id,
-                                                &room,
-                                                project_id,
-                                                &remote_participants,
-                                                cx,
-                                            )?
-                                            .render(cx);
-                                        Some(
-                                            v_stack()
-                                                .id(("collaborator", user.id))
-                                                .child(face_pile)
-                                                .child(render_color_ribbon(*participant_index, cx))
-                                                .cursor_pointer()
-                                                .on_click(cx.listener(move |this, _, cx| {
+                                remote_participants.iter().filter_map(|collaborator| {
+                                    // collaborator.is_
+
+                                    let face_pile = self.render_collaborator(
+                                        &collaborator.user,
+                                        collaborator.peer_id,
+                                        collaborator.location.clone(),
+                                        collaborator.speaking,
+                                        collaborator.muted,
+                                        &room,
+                                        project_id,
+                                        &current_user,
+                                    )?;
+
+                                    Some(
+                                        v_stack()
+                                            .id(("collaborator", collaborator.user.id))
+                                            .child(face_pile)
+                                            .child(render_color_ribbon(
+                                                collaborator.participant_index,
+                                                player_colors,
+                                            ))
+                                            .cursor_pointer()
+                                            .on_click({
+                                                let peer_id = collaborator.peer_id;
+                                                cx.listener(move |this, _, cx| {
                                                     this.workspace
                                                         .update(cx, |workspace, cx| {
                                                             workspace.follow(peer_id, cx);
                                                         })
                                                         .ok();
-                                                })),
-                                        )
-                                    },
-                                ),
+                                                })
+                                            }),
+                                    )
+                                }),
                             )
                         },
                     ),
@@ -280,15 +278,8 @@ impl Render for CollabTitlebarItem {
     }
 }
 
-fn render_color_ribbon(
-    participant_index: ParticipantIndex,
-    cx: &mut WindowContext,
-) -> gpui::Canvas {
-    let color = cx
-        .theme()
-        .players()
-        .color_for_participant(participant_index.0)
-        .cursor;
+fn render_color_ribbon(participant_index: ParticipantIndex, colors: &PlayerColors) -> gpui::Canvas {
+    let color = colors.color_for_participant(participant_index.0).cursor;
     canvas(move |bounds, cx| {
         let mut path = Path::new(bounds.lower_left());
         let height = bounds.size.height;
@@ -417,25 +408,45 @@ impl CollabTitlebarItem {
         &self,
         user: &Arc<User>,
         peer_id: PeerId,
-        room: &Model<Room>,
+        location: ParticipantLocation,
+        is_speaking: bool,
+        is_muted: bool,
+        room: &Room,
         project_id: u64,
-        collaborators: &[(Arc<User>, ParticipantIndex, PeerId)],
-        cx: &mut WindowContext,
+        current_user: &Arc<User>,
     ) -> Option<FacePile> {
-        let room = room.read(cx);
         let followers = room.followers_for(peer_id, project_id);
-
         let mut pile = FacePile::default();
         pile.extend(
             user.avatar
                 .clone()
-                .map(|avatar| div().child(Avatar::data(avatar.clone())).into_any_element())
+                .map(|avatar| {
+                    div()
+                        .child(
+                            Avatar::data(avatar.clone())
+                                .grayscale(
+                                    location != ParticipantLocation::SharedProject { project_id },
+                                )
+                                .border_color(if is_speaking {
+                                    gpui::blue()
+                                } else if is_muted {
+                                    gpui::red()
+                                } else {
+                                    Hsla::default()
+                                }),
+                        )
+                        .into_any_element()
+                })
                 .into_iter()
                 .chain(followers.iter().filter_map(|follower_peer_id| {
-                    let follower = collaborators
-                        .iter()
-                        .find(|(_, _, peer_id)| *peer_id == *follower_peer_id)?
-                        .0
+                    let follower = room
+                        .remote_participants()
+                        .values()
+                        .find_map(|p| (p.peer_id == *follower_peer_id).then_some(&p.user))
+                        .or_else(|| {
+                            (self.client.peer_id() == Some(*follower_peer_id))
+                                .then_some(current_user)
+                        })?
                         .clone();
                     follower
                         .avatar

crates/collab_ui2/src/face_pile.rs 🔗

@@ -1,8 +1,9 @@
 use gpui::{
-    div, AnyElement, Div, IntoElement as _, ParentElement as _, RenderOnce, Styled, WindowContext,
+    div, AnyElement, Div, ElementId, IntoElement, ParentElement as _, RenderOnce, Styled,
+    WindowContext,
 };
 
-#[derive(Default)]
+#[derive(Default, IntoElement)]
 pub struct FacePile {
     pub faces: Vec<AnyElement>,
 }

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

@@ -1,7 +1,6 @@
-use std::sync::Arc;
-
 use crate::prelude::*;
-use gpui::{img, rems, Div, ImageData, ImageSource, IntoElement, Styled};
+use gpui::{img, Div, Hsla, ImageData, ImageSource, Img, IntoElement, Styled};
+use std::sync::Arc;
 
 #[derive(Debug, Default, PartialEq, Clone)]
 pub enum Shape {
@@ -12,35 +11,39 @@ pub enum Shape {
 
 #[derive(IntoElement)]
 pub struct Avatar {
-    src: ImageSource,
+    image: Img,
+    border_color: Option<Hsla>,
     is_available: Option<bool>,
-    shape: Shape,
 }
 
 impl RenderOnce for Avatar {
     type Rendered = Div;
 
-    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
-        let mut img = img(self.src);
-
-        if self.shape == Shape::Circle {
-            img = img.rounded_full();
-        } else {
-            img = img.rounded_md();
+    fn render(mut self, cx: &mut WindowContext) -> Self::Rendered {
+        if self.image.style().corner_radii.top_left.is_none() {
+            self = self.shape(Shape::Circle);
         }
 
-        let size = rems(1.0);
+        let size = cx.rem_size();
 
         div()
-            .size(size)
+            .size(size + px(2.))
+            .map(|mut div| {
+                div.style().corner_radii = self.image.style().corner_radii.clone();
+                div
+            })
+            .when_some(self.border_color, |this, color| {
+                this.border().border_color(color)
+            })
             .child(
-                img.size(size)
+                self.image
+                    .size(size)
                     // todo!(Pull the avatar fallback background from the theme.)
                     .bg(gpui::red()),
             )
             .children(self.is_available.map(|is_free| {
                 // HACK: non-integer sizes result in oval indicators.
-                let indicator_size = (size.0 * cx.rem_size() * 0.4).round();
+                let indicator_size = (size * 0.4).round();
 
                 div()
                     .absolute()
@@ -56,31 +59,39 @@ impl RenderOnce for Avatar {
 
 impl Avatar {
     pub fn uri(src: impl Into<SharedString>) -> Self {
-        Self {
-            src: src.into().into(),
-            shape: Shape::Circle,
-            is_available: None,
-        }
+        Self::source(src.into().into())
     }
+
     pub fn data(src: Arc<ImageData>) -> Self {
-        Self {
-            src: src.into(),
-            shape: Shape::Circle,
-            is_available: None,
-        }
+        Self::source(src.into())
     }
 
     pub fn source(src: ImageSource) -> Self {
         Self {
-            src,
-            shape: Shape::Circle,
+            image: img(src),
             is_available: None,
+            border_color: None,
         }
     }
+
     pub fn shape(mut self, shape: Shape) -> Self {
-        self.shape = shape;
+        self.image = match shape {
+            Shape::Circle => self.image.rounded_full(),
+            Shape::RoundedRectangle => self.image.rounded_md(),
+        };
+        self
+    }
+
+    pub fn grayscale(mut self, grayscale: bool) -> Self {
+        self.image = self.image.grayscale(grayscale);
         self
     }
+
+    pub fn border_color(mut self, color: impl Into<Hsla>) -> Self {
+        self.border_color = Some(color.into());
+        self
+    }
+
     pub fn availability_indicator(mut self, is_available: impl Into<Option<bool>>) -> Self {
         self.is_available = is_available.into();
         self