From 282e4398a0acda207e953b47dcf666bb1fe30c2c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 7 Dec 2023 13:09:18 -0800 Subject: [PATCH] In titlebar, render followers and allow following people --- crates/collab_ui2/src/collab_titlebar_item.rs | 480 +++++++++--------- crates/gpui2/src/window.rs | 6 + 2 files changed, 255 insertions(+), 231 deletions(-) diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index 7e5354c601..bc30766c66 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -1,41 +1,14 @@ -// use crate::{ -// face_pile::FacePile, toggle_deafen, toggle_mute, toggle_screen_sharing, LeaveCall, -// ToggleDeafen, ToggleMute, ToggleScreenSharing, -// }; -// use auto_update::AutoUpdateStatus; -// use call::{ActiveCall, ParticipantLocation, Room}; -// use client::{proto::PeerId, Client, SignIn, SignOut, User, UserStore}; -// use clock::ReplicaId; -// use context_menu::{ContextMenu, ContextMenuItem}; -// use gpui::{ -// actions, -// color::Color, -// elements::*, -// geometry::{rect::RectF, vector::vec2f, PathBuilder}, -// json::{self, ToJson}, -// platform::{CursorStyle, MouseButton}, -// AppContext, Entity, ImageData, ModelHandle, Subscription, View, ViewContext, ViewHandle, -// WeakViewHandle, -// }; -// use picker::PickerEvent; -// use project::{Project, RepositoryEntry}; -// use recent_projects::{build_recent_projects, RecentProjects}; -// use std::{ops::Range, sync::Arc}; -// use theme::{AvatarStyle, Theme}; -// use util::ResultExt; -// use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu}; -// use workspace::{FollowNextCollaborator, Workspace, WORKSPACE_DB}; - -use std::sync::Arc; - -use call::ActiveCall; -use client::{Client, UserStore}; +use crate::face_pile::FacePile; +use call::{ActiveCall, Room}; +use client::{proto::PeerId, Client, ParticipantIndex, User, UserStore}; use gpui::{ - actions, div, px, rems, AppContext, Div, Element, InteractiveElement, IntoElement, Model, - MouseButton, ParentElement, Render, RenderOnce, Stateful, StatefulInteractiveElement, Styled, - Subscription, ViewContext, VisualContext, WeakView, WindowBounds, + actions, canvas, div, point, px, rems, AppContext, Div, Element, InteractiveElement, + IntoElement, Model, ParentElement, Path, Render, RenderOnce, Stateful, + StatefulInteractiveElement, Styled, Subscription, ViewContext, VisualContext, WeakView, + WindowBounds, }; use project::{Project, RepositoryEntry}; +use std::sync::Arc; use theme::ActiveTheme; use ui::{ h_stack, popover_menu, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon, @@ -44,8 +17,6 @@ use ui::{ use util::ResultExt; use workspace::{notifications::NotifyResultExt, Workspace}; -use crate::face_pile::FacePile; - const MAX_PROJECT_NAME_LENGTH: usize = 40; const MAX_BRANCH_NAME_LENGTH: usize = 40; @@ -57,17 +28,6 @@ actions!( SwitchBranch ); -// actions!( -// collab, -// [ -// ToggleUserMenu, -// ToggleProjectMenu, -// SwitchBranch, -// ShareProject, -// UnshareProject, -// ] -// ); - pub fn init(cx: &mut AppContext) { cx.observe_new_views(|workspace: &mut Workspace, cx| { let titlebar_item = cx.build_view(|cx| CollabTitlebarItem::new(workspace, cx)); @@ -99,28 +59,11 @@ impl Render for CollabTitlebarItem { type Element = Stateful
; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - let room = ActiveCall::global(cx).read(cx).room(); - let is_in_room = room.is_some(); - let is_shared = is_in_room && self.project.read(cx).is_shared(); + let room = ActiveCall::global(cx).read(cx).room().cloned(); let current_user = self.user_store.read(cx).current_user(); let client = self.client.clone(); - let remote_participants = room.map(|room| { - room.read(cx) - .remote_participants() - .values() - .map(|participant| (participant.user.clone(), participant.peer_id)) - .collect::>() - }); - let is_muted = room.map_or(false, |room| room.read(cx).is_muted(cx)); - let is_deafened = room - .and_then(|room| room.read(cx).is_deafened()) - .unwrap_or(false); - let speakers_icon = if is_deafened { - ui::Icon::AudioOff - } else { - ui::Icon::AudioOn - }; - let workspace = self.workspace.clone(); + let project_id = self.project.read(cx).remote_id(); + h_stack() .id("titlebar") .justify_between() @@ -141,182 +84,224 @@ impl Render for CollabTitlebarItem { cx.zoom_window(); } }) + // left side .child( h_stack() .gap_1() - .when(is_in_room, |this| { - this.children(self.render_project_owner(cx)) - }) + .children(self.render_project_host(cx)) .child(self.render_project_name(cx)) - .children(self.render_project_branch(cx)), - ) - .when_some( - remote_participants.zip(current_user.clone()), - |this, (remote_participants, current_user)| { - let mut pile = FacePile::default(); - pile.extend( - current_user - .avatar - .clone() - .map(|avatar| { - div().child(Avatar::data(avatar.clone())).into_any_element() - }) - .into_iter() - .chain(remote_participants.into_iter().filter_map( - |(user, peer_id)| { - let avatar = user.avatar.as_ref()?; - Some( - div() - .child( - Avatar::data(avatar.clone()) - .into_element() - .into_any(), - ) - .on_mouse_down(MouseButton::Left, { - let workspace = workspace.clone(); - move |_, cx| { - workspace - .update(cx, |this, cx| { - this.open_shared_screen(peer_id, cx); - }) - .log_err(); - } - }) - .into_any_element(), + .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::>(); + + this.children( + self.render_collaborator( + ¤t_user, + client.peer_id().expect("todo!()"), + &room, + project_id, + &remote_participants, + cx, + ) + .map(|pile| pile.render(cx)), + ) + .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| { + this.workspace + .update(cx, |workspace, cx| { + workspace.follow(peer_id, cx); + }) + .ok(); + })), + ) + }, + ), + ) + }, + ), + ) + // right side + .child( + h_stack() + .gap_1() + .when_some(room, |this, room| { + let room = room.read(cx); + let is_shared = self.project.read(cx).is_shared(); + let is_muted = room.is_muted(cx); + let is_deafened = room.is_deafened().unwrap_or(false); + + this.child( + Button::new( + "toggle_sharing", + if is_shared { "Unshare" } else { "Share" }, + ) + .style(ButtonStyle::Subtle) + .on_click(cx.listener( + move |this, _, cx| { + if is_shared { + this.unshare_project(&Default::default(), cx); + } else { + this.share_project(&Default::default(), cx); + } }, )), - ); - this.child(pile.render(cx)) - }, - ) - .child(div().flex_1()) - .when(is_in_room, |this| { - this.child( - h_stack() - .gap_1() - .child( - h_stack() - .gap_1() - .child( - Button::new( - "toggle_sharing", - if is_shared { "Unshare" } else { "Share" }, - ) - .style(ButtonStyle::Subtle) - .on_click(cx.listener( - move |this, _, cx| { - if is_shared { - this.unshare_project(&Default::default(), cx); - } else { - this.share_project(&Default::default(), cx); - } - }, - )), - ) - .child( - IconButton::new("leave-call", ui::Icon::Exit) - .style(ButtonStyle::Subtle) - .on_click(move |_, cx| { - ActiveCall::global(cx) - .update(cx, |call, cx| call.hang_up(cx)) - .detach_and_log_err(cx); - }), - ), ) .child( - h_stack() - .gap_1() - .child( - IconButton::new( - "mute-microphone", - if is_muted { - ui::Icon::MicMute - } else { - ui::Icon::Mic - }, - ) - .style(ButtonStyle::Subtle) - .selected(is_muted) - .on_click(move |_, cx| { - crate::toggle_mute(&Default::default(), cx) - }), - ) - .child( - IconButton::new("mute-sound", speakers_icon) - .style(ButtonStyle::Subtle) - .selected(is_deafened.clone()) - .tooltip(move |cx| { - Tooltip::with_meta( - "Deafen Audio", - None, - "Mic will be muted", - cx, - ) - }) - .on_click(move |_, cx| { - crate::toggle_mute(&Default::default(), cx) - }), - ) - .child( - IconButton::new("screen-share", ui::Icon::Screen) - .style(ButtonStyle::Subtle) - .on_click(move |_, cx| { - crate::toggle_screen_sharing(&Default::default(), cx) - }), - ) - .pl_2(), - ), - ) - }) - .child(h_stack().px_1p5().map(|this| { - if let Some(user) = current_user { - this.when_some(user.avatar.clone(), |this, avatar| { - // TODO: Finish implementing user menu popover - // - this.child( - popover_menu("user-menu") - .menu(|cx| ContextMenu::build(cx, |menu, _| menu.header("ADADA"))) - .trigger( - ButtonLike::new("user-menu") - .child( - h_stack().gap_0p5().child(Avatar::data(avatar)).child( - IconElement::new(Icon::ChevronDown) - .color(Color::Muted), - ), - ) - .style(ButtonStyle::Subtle) - .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)), - ) - .anchor(gpui::AnchorCorner::TopRight), + IconButton::new("leave-call", ui::Icon::Exit) + .style(ButtonStyle::Subtle) + .on_click(move |_, cx| { + ActiveCall::global(cx) + .update(cx, |call, cx| call.hang_up(cx)) + .detach_and_log_err(cx); + }), + ) + .child( + IconButton::new( + "mute-microphone", + if is_muted { + ui::Icon::MicMute + } else { + ui::Icon::Mic + }, + ) + .style(ButtonStyle::Subtle) + .selected(is_muted) + .on_click(move |_, cx| crate::toggle_mute(&Default::default(), cx)), + ) + .child( + IconButton::new( + "mute-sound", + if is_deafened { + ui::Icon::AudioOff + } else { + ui::Icon::AudioOn + }, + ) + .style(ButtonStyle::Subtle) + .selected(is_deafened.clone()) + .tooltip(move |cx| { + Tooltip::with_meta("Deafen Audio", None, "Mic will be muted", cx) + }) + .on_click(move |_, cx| crate::toggle_mute(&Default::default(), cx)), + ) + .child( + IconButton::new("screen-share", ui::Icon::Screen) + .style(ButtonStyle::Subtle) + .on_click(move |_, cx| { + crate::toggle_screen_sharing(&Default::default(), cx) + }), ) - // this.child( - // ButtonLike::new("user-menu") - // .child( - // h_stack().gap_0p5().child(Avatar::data(avatar)).child( - // IconElement::new(Icon::ChevronDown).color(Color::Muted), - // ), - // ) - // .style(ButtonStyle::Subtle) - // .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)), - // ) }) - } else { - this.child(Button::new("sign_in", "Sign in").on_click(move |_, cx| { - let client = client.clone(); - cx.spawn(move |mut cx| async move { - client - .authenticate_and_connect(true, &cx) - .await - .notify_async_err(&mut cx); - }) - .detach(); - })) - } - })) + .child(h_stack().px_1p5().map(|this| { + if let Some(user) = current_user { + this.when_some(user.avatar.clone(), |this, avatar| { + // TODO: Finish implementing user menu popover + // + this.child( + popover_menu("user-menu") + .menu(|cx| { + ContextMenu::build(cx, |menu, _| menu.header("ADADA")) + }) + .trigger( + ButtonLike::new("user-menu") + .child( + h_stack() + .gap_0p5() + .child(Avatar::data(avatar)) + .child( + IconElement::new(Icon::ChevronDown) + .color(Color::Muted), + ), + ) + .style(ButtonStyle::Subtle) + .tooltip(move |cx| { + Tooltip::text("Toggle User Menu", cx) + }), + ) + .anchor(gpui::AnchorCorner::TopRight), + ) + // this.child( + // ButtonLike::new("user-menu") + // .child( + // h_stack().gap_0p5().child(Avatar::data(avatar)).child( + // IconElement::new(Icon::ChevronDown).color(Color::Muted), + // ), + // ) + // .style(ButtonStyle::Subtle) + // .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)), + // ) + }) + } else { + this.child(Button::new("sign_in", "Sign in").on_click(move |_, cx| { + let client = client.clone(); + cx.spawn(move |mut cx| async move { + client + .authenticate_and_connect(true, &cx) + .await + .notify_async_err(&mut cx); + }) + .detach(); + })) + } + })), + ) } } +fn render_color_ribbon( + participant_index: ParticipantIndex, + cx: &mut WindowContext, +) -> gpui::Canvas { + let color = cx + .theme() + .players() + .color_for_participant(participant_index.0) + .cursor; + canvas(move |bounds, cx| { + let mut path = Path::new(bounds.lower_left()); + let height = bounds.size.height; + path.curve_to(bounds.origin + point(height, px(0.)), bounds.origin); + path.line_to(bounds.upper_right() - point(height, px(0.))); + path.curve_to(bounds.lower_right(), bounds.upper_right()); + path.line_to(bounds.lower_left()); + cx.paint_path(path, color); + }) + .h_1() + .w_full() +} + // impl Entity for CollabTitlebarItem { // type Event = (); // } @@ -435,7 +420,7 @@ impl CollabTitlebarItem { // resolve if you are in a room -> render_project_owner // render_project_owner -> resolve if you are in a room -> Option - pub fn render_project_owner(&self, cx: &mut ViewContext) -> Option { + pub fn render_project_host(&self, cx: &mut ViewContext) -> Option { let host = self.project.read(cx).host()?; let host = self.user_store.read(cx).get_cached_user(host.user_id)?; let participant_index = self @@ -509,6 +494,39 @@ impl CollabTitlebarItem { ) } + fn render_collaborator( + &self, + user: &Arc, + peer_id: PeerId, + room: &Model, + project_id: u64, + collaborators: &[(Arc, ParticipantIndex, PeerId)], + cx: &mut WindowContext, + ) -> 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()) + .into_iter() + .chain(followers.iter().filter_map(|follower_peer_id| { + let follower = collaborators + .iter() + .find(|(_, _, peer_id)| *peer_id == *follower_peer_id)? + .0 + .clone(); + follower + .avatar + .clone() + .map(|avatar| div().child(Avatar::data(avatar.clone())).into_any_element()) + })), + ); + Some(pile) + } + // fn collect_title_root_names( // &self, // theme: Arc, diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index cab41067ce..f98d9820c2 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -2839,3 +2839,9 @@ impl From<(&'static str, usize)> for ElementId { ElementId::NamedInteger(name.into(), id) } } + +impl From<(&'static str, u64)> for ElementId { + fn from((name, id): (&'static str, u64)) -> Self { + ElementId::NamedInteger(name.into(), id as usize) + } +}