mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-20 02:47:34 +03:00
Indicate collaborators' presence (grayscale), speaking and muted status
This commit is contained in:
parent
04d019ef66
commit
d1b47b4059
@ -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<_>>();
|
||||
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(
|
||||
¤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<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
|
||||
|
@ -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>,
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user