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)
+ }
+}