diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index c5872e660d..95d3606a49 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/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, - #[allow(unused)] // todo!() user_store: Model, - #[allow(unused)] // todo!() client: Arc, - #[allow(unused)] // todo!() workspace: WeakView, //branch_popover: Option>, //project_popover: Option>, @@ -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::>(); + 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::>(); + remote_participants.sort_by_key(|p| p.participant_index.0); - this.children( - self.render_collaborator( - ¤t_user, - client.peer_id().expect("todo!()"), - &room, - project_id, - &remote_participants, - cx, - ) - .map(|pile| pile.render(cx)), - ) + this.children(self.render_collaborator( + ¤t_user, + peer_id, + ParticipantLocation::SharedProject { project_id }, + room.is_speaking(), + room.is_muted(cx), + &room, + project_id, + ¤t_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, + ¤t_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, peer_id: PeerId, - room: &Model, + location: ParticipantLocation, + is_speaking: bool, + is_muted: bool, + room: &Room, project_id: u64, - collaborators: &[(Arc, ParticipantIndex, PeerId)], - cx: &mut WindowContext, + current_user: &Arc, ) -> Option { - 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 diff --git a/crates/collab_ui2/src/face_pile.rs b/crates/collab_ui2/src/face_pile.rs index b7422d2b22..6c424c7805 100644 --- a/crates/collab_ui2/src/face_pile.rs +++ b/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, } diff --git a/crates/ui2/src/components/avatar.rs b/crates/ui2/src/components/avatar.rs index 3c8eefa6db..454daacc42 100644 --- a/crates/ui2/src/components/avatar.rs +++ b/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, is_available: Option, - 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) -> Self { - Self { - src: src.into().into(), - shape: Shape::Circle, - is_available: None, - } + Self::source(src.into().into()) } + pub fn data(src: Arc) -> 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) -> Self { + self.border_color = Some(color.into()); + self + } + pub fn availability_indicator(mut self, is_available: impl Into>) -> Self { self.is_available = is_available.into(); self