diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index 7e5354c601..f6605ec14c 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -1,42 +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, ParticipantLocation, 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, + 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 theme::ActiveTheme; +use std::sync::Arc; +use theme::{ActiveTheme, PlayerColors}; use ui::{ h_stack, popover_menu, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon, IconButton, IconElement, KeyBinding, Tooltip, @@ -44,8 +16,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 +27,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)); @@ -83,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>, @@ -99,28 +55,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,262 +80,222 @@ 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( + .children(self.render_project_branch(cx)) + .when_some( 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()?; + .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, + peer_id, + ParticipantLocation::SharedProject { project_id }, + room.is_speaking(), + room.is_muted(cx), + &room, + project_id, + ¤t_user, + )) + .children( + remote_participants.iter().filter_map(|collaborator| { + let face_pile = self.render_collaborator( + &collaborator.user, + collaborator.peer_id, + collaborator.location.clone(), + collaborator.speaking, + collaborator.muted, + &room, + project_id, + ¤t_user, + )?; + 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); + 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); }) - .log_err(); - } + .ok(); + }) }) - .into_any_element(), + .tooltip({ + let login = collaborator.user.github_login.clone(); + move |cx| { + Tooltip::text(format!("Follow {login}"), cx) + } + }), ) + }), + ) + }, + ), + ) + // 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(); + })) + } + })), + ) } } -// impl Entity for CollabTitlebarItem { -// type Event = (); -// } - -// impl View for CollabTitlebarItem { -// fn ui_name() -> &'static str { -// "CollabTitlebarItem" -// } - -// fn render(&mut self, cx: &mut ViewContext) -> AnyElement { -// let workspace = if let Some(workspace) = self.workspace.upgrade(cx) { -// workspace -// } else { -// return Empty::new().into_any(); -// }; - -// let theme = theme::current(cx).clone(); -// let mut left_container = Flex::row(); -// let mut right_container = Flex::row().align_children_center(); - -// left_container.add_child(self.collect_title_root_names(theme.clone(), cx)); - -// let user = self.user_store.read(cx).current_user(); -// let peer_id = self.client.peer_id(); -// if let Some(((user, peer_id), room)) = user -// .as_ref() -// .zip(peer_id) -// .zip(ActiveCall::global(cx).read(cx).room().cloned()) -// { -// if room.read(cx).can_publish() { -// right_container -// .add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx)); -// } -// right_container.add_child(self.render_leave_call(&theme, cx)); -// let muted = room.read(cx).is_muted(cx); -// let speaking = room.read(cx).is_speaking(); -// left_container.add_child( -// self.render_current_user(&workspace, &theme, &user, peer_id, muted, speaking, cx), -// ); -// left_container.add_children(self.render_collaborators(&workspace, &theme, &room, cx)); -// if room.read(cx).can_publish() { -// right_container.add_child(self.render_toggle_mute(&theme, &room, cx)); -// } -// right_container.add_child(self.render_toggle_deafen(&theme, &room, cx)); -// if room.read(cx).can_publish() { -// right_container -// .add_child(self.render_toggle_screen_sharing_button(&theme, &room, cx)); -// } -// } - -// let status = workspace.read(cx).client().status(); -// let status = &*status.borrow(); -// if matches!(status, client::Status::Connected { .. }) { -// let avatar = user.as_ref().and_then(|user| user.avatar.clone()); -// right_container.add_child(self.render_user_menu_button(&theme, avatar, cx)); -// } else { -// right_container.add_children(self.render_connection_status(status, cx)); -// right_container.add_child(self.render_sign_in_button(&theme, cx)); -// right_container.add_child(self.render_user_menu_button(&theme, None, cx)); -// } - -// Stack::new() -// .with_child(left_container) -// .with_child( -// Flex::row() -// .with_child( -// right_container.contained().with_background_color( -// theme -// .titlebar -// .container -// .background_color -// .unwrap_or_else(|| Color::transparent_black()), -// ), -// ) -// .aligned() -// .right(), -// ) -// .into_any() -// } -// } +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; + 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 CollabTitlebarItem { pub fn new(workspace: &Workspace, cx: &mut ViewContext) -> Self { @@ -435,7 +334,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,169 +408,58 @@ impl CollabTitlebarItem { ) } - // fn collect_title_root_names( - // &self, - // theme: Arc, - // cx: &mut ViewContext, - // ) -> AnyElement { - // let project = self.project.read(cx); - - // let (name, entry) = { - // let mut names_and_branches = project.visible_worktrees(cx).map(|worktree| { - // let worktree = worktree.read(cx); - // (worktree.root_name(), worktree.root_git_entry()) - // }); - - // names_and_branches.next().unwrap_or(("", None)) - // }; - - // let name = util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH); - // let branch_prepended = entry - // .as_ref() - // .and_then(RepositoryEntry::branch) - // .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH)); - // let project_style = theme.titlebar.project_menu_button.clone(); - // let git_style = theme.titlebar.git_menu_button.clone(); - // let item_spacing = theme.titlebar.item_spacing; - - // let mut ret = Flex::row(); - - // if let Some(project_host) = self.collect_project_host(theme.clone(), cx) { - // ret = ret.with_child(project_host) - // } - - // ret = ret.with_child( - // Stack::new() - // .with_child( - // MouseEventHandler::new::(0, cx, |mouse_state, cx| { - // let style = project_style - // .in_state(self.project_popover.is_some()) - // .style_for(mouse_state); - // enum RecentProjectsTooltip {} - // Label::new(name, style.text.clone()) - // .contained() - // .with_style(style.container) - // .aligned() - // .left() - // .with_tooltip::( - // 0, - // "Recent projects", - // Some(Box::new(recent_projects::OpenRecent)), - // theme.tooltip.clone(), - // cx, - // ) - // .into_any_named("title-project-name") - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_down(MouseButton::Left, move |_, this, cx| { - // this.toggle_project_menu(&Default::default(), cx) - // }) - // .on_click(MouseButton::Left, move |_, _, _| {}), - // ) - // .with_children(self.render_project_popover_host(&theme.titlebar, cx)), - // ); - // if let Some(git_branch) = branch_prepended { - // ret = ret.with_child( - // Flex::row().with_child( - // Stack::new() - // .with_child( - // MouseEventHandler::new::(0, cx, |mouse_state, cx| { - // enum BranchPopoverTooltip {} - // let style = git_style - // .in_state(self.branch_popover.is_some()) - // .style_for(mouse_state); - // Label::new(git_branch, style.text.clone()) - // .contained() - // .with_style(style.container.clone()) - // .with_margin_right(item_spacing) - // .aligned() - // .left() - // .with_tooltip::( - // 0, - // "Recent branches", - // Some(Box::new(ToggleVcsMenu)), - // theme.tooltip.clone(), - // cx, - // ) - // .into_any_named("title-project-branch") - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_down(MouseButton::Left, move |_, this, cx| { - // this.toggle_vcs_menu(&Default::default(), cx) - // }) - // .on_click(MouseButton::Left, move |_, _, _| {}), - // ) - // .with_children(self.render_branches_popover_host(&theme.titlebar, cx)), - // ), - // ) - // } - // ret.into_any() - // } - - // fn collect_project_host( - // &self, - // theme: Arc, - // cx: &mut ViewContext, - // ) -> Option> { - // if ActiveCall::global(cx).read(cx).room().is_none() { - // return None; - // } - // let project = self.project.read(cx); - // let user_store = self.user_store.read(cx); - - // if project.is_local() { - // return None; - // } - - // let Some(host) = project.host() else { - // return None; - // }; - // let (Some(host_user), Some(participant_index)) = ( - // user_store.get_cached_user(host.user_id), - // user_store.participant_indices().get(&host.user_id), - // ) else { - // return None; - // }; - - // enum ProjectHost {} - // enum ProjectHostTooltip {} - - // let host_style = theme.titlebar.project_host.clone(); - // let selection_style = theme - // .editor - // .selection_style_for_room_participant(participant_index.0); - // let peer_id = host.peer_id.clone(); - - // Some( - // MouseEventHandler::new::(0, cx, |mouse_state, _| { - // let mut host_style = host_style.style_for(mouse_state).clone(); - // host_style.text.color = selection_style.cursor; - // Label::new(host_user.github_login.clone(), host_style.text) - // .contained() - // .with_style(host_style.container) - // .aligned() - // .left() - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, move |_, this, cx| { - // if let Some(workspace) = this.workspace.upgrade(cx) { - // if let Some(task) = - // workspace.update(cx, |workspace, cx| workspace.follow(peer_id, cx)) - // { - // task.detach_and_log_err(cx); - // } - // } - // }) - // .with_tooltip::( - // 0, - // host_user.github_login.clone() + " is sharing this project. Click to follow.", - // None, - // theme.tooltip.clone(), - // cx, - // ) - // .into_any_named("project-host"), - // ) - // } + fn render_collaborator( + &self, + user: &Arc, + peer_id: PeerId, + location: ParticipantLocation, + is_speaking: bool, + is_muted: bool, + room: &Room, + project_id: u64, + current_user: &Arc, + ) -> Option { + 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()) + .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 = 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 + .clone() + .map(|avatar| div().child(Avatar::data(avatar.clone())).into_any_element()) + })), + ); + Some(pile) + } fn window_activation_changed(&mut self, cx: &mut ViewContext) { let project = if cx.is_window_active() { @@ -868,245 +656,6 @@ impl CollabTitlebarItem { // cx.notify(); // } - // fn render_toggle_screen_sharing_button( - // &self, - // theme: &Theme, - // room: &ModelHandle, - // cx: &mut ViewContext, - // ) -> AnyElement { - // let icon; - // let tooltip; - // if room.read(cx).is_screen_sharing() { - // icon = "icons/desktop.svg"; - // tooltip = "Stop Sharing Screen" - // } else { - // icon = "icons/desktop.svg"; - // tooltip = "Share Screen"; - // } - - // let active = room.read(cx).is_screen_sharing(); - // let titlebar = &theme.titlebar; - // MouseEventHandler::new::(0, cx, |state, _| { - // let style = titlebar - // .screen_share_button - // .in_state(active) - // .style_for(state); - - // Svg::new(icon) - // .with_color(style.color) - // .constrained() - // .with_width(style.icon_width) - // .aligned() - // .constrained() - // .with_width(style.button_width) - // .with_height(style.button_width) - // .contained() - // .with_style(style.container) - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, move |_, _, cx| { - // toggle_screen_sharing(&Default::default(), cx) - // }) - // .with_tooltip::( - // 0, - // tooltip, - // Some(Box::new(ToggleScreenSharing)), - // theme.tooltip.clone(), - // cx, - // ) - // .aligned() - // .into_any() - // } - // fn render_toggle_mute( - // &self, - // theme: &Theme, - // room: &ModelHandle, - // cx: &mut ViewContext, - // ) -> AnyElement { - // let icon; - // let tooltip; - // let is_muted = room.read(cx).is_muted(cx); - // if is_muted { - // icon = "icons/mic-mute.svg"; - // tooltip = "Unmute microphone"; - // } else { - // icon = "icons/mic.svg"; - // tooltip = "Mute microphone"; - // } - - // let titlebar = &theme.titlebar; - // MouseEventHandler::new::(0, cx, |state, _| { - // let style = titlebar - // .toggle_microphone_button - // .in_state(is_muted) - // .style_for(state); - // let image = Svg::new(icon) - // .with_color(style.color) - // .constrained() - // .with_width(style.icon_width) - // .aligned() - // .constrained() - // .with_width(style.button_width) - // .with_height(style.button_width) - // .contained() - // .with_style(style.container); - // if let Some(color) = style.container.background_color { - // image.with_background_color(color) - // } else { - // image - // } - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, move |_, _, cx| { - // toggle_mute(&Default::default(), cx) - // }) - // .with_tooltip::( - // 0, - // tooltip, - // Some(Box::new(ToggleMute)), - // theme.tooltip.clone(), - // cx, - // ) - // .aligned() - // .into_any() - // } - // fn render_toggle_deafen( - // &self, - // theme: &Theme, - // room: &ModelHandle, - // cx: &mut ViewContext, - // ) -> AnyElement { - // let icon; - // let tooltip; - // let is_deafened = room.read(cx).is_deafened().unwrap_or(false); - // if is_deafened { - // icon = "icons/speaker-off.svg"; - // tooltip = "Unmute speakers"; - // } else { - // icon = "icons/speaker-loud.svg"; - // tooltip = "Mute speakers"; - // } - - // let titlebar = &theme.titlebar; - // MouseEventHandler::new::(0, cx, |state, _| { - // let style = titlebar - // .toggle_speakers_button - // .in_state(is_deafened) - // .style_for(state); - // Svg::new(icon) - // .with_color(style.color) - // .constrained() - // .with_width(style.icon_width) - // .aligned() - // .constrained() - // .with_width(style.button_width) - // .with_height(style.button_width) - // .contained() - // .with_style(style.container) - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, move |_, _, cx| { - // toggle_deafen(&Default::default(), cx) - // }) - // .with_tooltip::( - // 0, - // tooltip, - // Some(Box::new(ToggleDeafen)), - // theme.tooltip.clone(), - // cx, - // ) - // .aligned() - // .into_any() - // } - // fn render_leave_call(&self, theme: &Theme, cx: &mut ViewContext) -> AnyElement { - // let icon = "icons/exit.svg"; - // let tooltip = "Leave call"; - - // let titlebar = &theme.titlebar; - // MouseEventHandler::new::(0, cx, |state, _| { - // let style = titlebar.leave_call_button.style_for(state); - // Svg::new(icon) - // .with_color(style.color) - // .constrained() - // .with_width(style.icon_width) - // .aligned() - // .constrained() - // .with_width(style.button_width) - // .with_height(style.button_width) - // .contained() - // .with_style(style.container) - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, move |_, _, cx| { - // ActiveCall::global(cx) - // .update(cx, |call, cx| call.hang_up(cx)) - // .detach_and_log_err(cx); - // }) - // .with_tooltip::( - // 0, - // tooltip, - // Some(Box::new(LeaveCall)), - // theme.tooltip.clone(), - // cx, - // ) - // .aligned() - // .into_any() - // } - // fn render_in_call_share_unshare_button( - // &self, - // workspace: &ViewHandle, - // theme: &Theme, - // cx: &mut ViewContext, - // ) -> Option> { - // let project = workspace.read(cx).project(); - // if project.read(cx).is_remote() { - // return None; - // } - - // let is_shared = project.read(cx).is_shared(); - // let label = if is_shared { "Stop Sharing" } else { "Share" }; - // let tooltip = if is_shared { - // "Stop sharing project with call participants" - // } else { - // "Share project with call participants" - // }; - - // let titlebar = &theme.titlebar; - - // enum ShareUnshare {} - // Some( - // Stack::new() - // .with_child( - // MouseEventHandler::new::(0, cx, |state, _| { - // //TODO: Ensure this button has consistent width for both text variations - // let style = titlebar.share_button.inactive_state().style_for(state); - // Label::new(label, style.text.clone()) - // .contained() - // .with_style(style.container) - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, move |_, this, cx| { - // if is_shared { - // this.unshare_project(&Default::default(), cx); - // } else { - // this.share_project(&Default::default(), cx); - // } - // }) - // .with_tooltip::( - // 0, - // tooltip.to_owned(), - // None, - // theme.tooltip.clone(), - // cx, - // ), - // ) - // .aligned() - // .contained() - // .with_margin_left(theme.titlebar.item_spacing) - // .into_any(), - // ) - // } - // fn render_user_menu_button( // &self, // theme: &Theme, @@ -1199,284 +748,6 @@ impl CollabTitlebarItem { // .into_any() // } - // fn render_collaborators( - // &self, - // workspace: &ViewHandle, - // theme: &Theme, - // room: &ModelHandle, - // cx: &mut ViewContext, - // ) -> Vec> { - // let mut participants = room - // .read(cx) - // .remote_participants() - // .values() - // .cloned() - // .collect::>(); - // participants.sort_by_cached_key(|p| p.user.github_login.clone()); - - // participants - // .into_iter() - // .filter_map(|participant| { - // let project = workspace.read(cx).project().read(cx); - // let replica_id = project - // .collaborators() - // .get(&participant.peer_id) - // .map(|collaborator| collaborator.replica_id); - // let user = participant.user.clone(); - // Some( - // Container::new(self.render_face_pile( - // &user, - // replica_id, - // participant.peer_id, - // Some(participant.location), - // participant.muted, - // participant.speaking, - // workspace, - // theme, - // cx, - // )) - // .with_margin_right(theme.titlebar.face_pile_spacing), - // ) - // }) - // .collect() - // } - - // fn render_current_user( - // &self, - // workspace: &ViewHandle, - // theme: &Theme, - // user: &Arc, - // peer_id: PeerId, - // muted: bool, - // speaking: bool, - // cx: &mut ViewContext, - // ) -> AnyElement { - // let replica_id = workspace.read(cx).project().read(cx).replica_id(); - - // Container::new(self.render_face_pile( - // user, - // Some(replica_id), - // peer_id, - // None, - // muted, - // speaking, - // workspace, - // theme, - // cx, - // )) - // .with_margin_right(theme.titlebar.item_spacing) - // .into_any() - // } - - // fn render_face_pile( - // &self, - // user: &User, - // _replica_id: Option, - // peer_id: PeerId, - // location: Option, - // muted: bool, - // speaking: bool, - // workspace: &ViewHandle, - // theme: &Theme, - // cx: &mut ViewContext, - // ) -> AnyElement { - // let user_id = user.id; - // let project_id = workspace.read(cx).project().read(cx).remote_id(); - // let room = ActiveCall::global(cx).read(cx).room().cloned(); - // let self_peer_id = workspace.read(cx).client().peer_id(); - // let self_following = workspace.read(cx).is_being_followed(peer_id); - // let self_following_initialized = self_following - // && room.as_ref().map_or(false, |room| match project_id { - // None => true, - // Some(project_id) => room - // .read(cx) - // .followers_for(peer_id, project_id) - // .iter() - // .any(|&follower| Some(follower) == self_peer_id), - // }); - - // let leader_style = theme.titlebar.leader_avatar; - // let follower_style = theme.titlebar.follower_avatar; - - // let microphone_state = if muted { - // Some(theme.titlebar.muted) - // } else if speaking { - // Some(theme.titlebar.speaking) - // } else { - // None - // }; - - // let mut background_color = theme - // .titlebar - // .container - // .background_color - // .unwrap_or_default(); - - // let participant_index = self - // .user_store - // .read(cx) - // .participant_indices() - // .get(&user_id) - // .copied(); - // if let Some(participant_index) = participant_index { - // if self_following_initialized { - // let selection = theme - // .editor - // .selection_style_for_room_participant(participant_index.0) - // .selection; - // background_color = Color::blend(selection, background_color); - // background_color.a = 255; - // } - // } - - // enum TitlebarParticipant {} - - // let content = MouseEventHandler::new::( - // peer_id.as_u64() as usize, - // cx, - // move |_, cx| { - // Stack::new() - // .with_children(user.avatar.as_ref().map(|avatar| { - // let face_pile = FacePile::new(theme.titlebar.follower_avatar_overlap) - // .with_child(Self::render_face( - // avatar.clone(), - // Self::location_style(workspace, location, leader_style, cx), - // background_color, - // microphone_state, - // )) - // .with_children( - // (|| { - // let project_id = project_id?; - // let room = room?.read(cx); - // let followers = room.followers_for(peer_id, project_id); - // Some(followers.into_iter().filter_map(|&follower| { - // if Some(follower) == self_peer_id { - // return None; - // } - // let participant = - // room.remote_participant_for_peer_id(follower)?; - // Some(Self::render_face( - // participant.user.avatar.clone()?, - // follower_style, - // background_color, - // None, - // )) - // })) - // })() - // .into_iter() - // .flatten(), - // ) - // .with_children( - // self_following_initialized - // .then(|| self.user_store.read(cx).current_user()) - // .and_then(|user| { - // Some(Self::render_face( - // user?.avatar.clone()?, - // follower_style, - // background_color, - // None, - // )) - // }), - // ); - - // let mut container = face_pile - // .contained() - // .with_style(theme.titlebar.leader_selection); - - // if let Some(participant_index) = participant_index { - // if self_following_initialized { - // let color = theme - // .editor - // .selection_style_for_room_participant(participant_index.0) - // .selection; - // container = container.with_background_color(color); - // } - // } - - // container - // })) - // .with_children((|| { - // let participant_index = participant_index?; - // let color = theme - // .editor - // .selection_style_for_room_participant(participant_index.0) - // .cursor; - // Some( - // AvatarRibbon::new(color) - // .constrained() - // .with_width(theme.titlebar.avatar_ribbon.width) - // .with_height(theme.titlebar.avatar_ribbon.height) - // .aligned() - // .bottom(), - // ) - // })()) - // }, - // ); - - // if Some(peer_id) == self_peer_id { - // return content.into_any(); - // } - - // content - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, move |_, this, cx| { - // let Some(workspace) = this.workspace.upgrade(cx) else { - // return; - // }; - // if let Some(task) = - // workspace.update(cx, |workspace, cx| workspace.follow(peer_id, cx)) - // { - // task.detach_and_log_err(cx); - // } - // }) - // .with_tooltip::( - // peer_id.as_u64() as usize, - // format!("Follow {}", user.github_login), - // Some(Box::new(FollowNextCollaborator)), - // theme.tooltip.clone(), - // cx, - // ) - // .into_any() - // } - - // fn location_style( - // workspace: &ViewHandle, - // location: Option, - // mut style: AvatarStyle, - // cx: &ViewContext, - // ) -> AvatarStyle { - // if let Some(location) = location { - // if let ParticipantLocation::SharedProject { project_id } = location { - // if Some(project_id) != workspace.read(cx).project().read(cx).remote_id() { - // style.image.grayscale = true; - // } - // } else { - // style.image.grayscale = true; - // } - // } - - // style - // } - - // fn render_face( - // avatar: Arc, - // avatar_style: AvatarStyle, - // background_color: Color, - // microphone_state: Option, - // ) -> AnyElement { - // Image::from_data(avatar) - // .with_style(avatar_style.image) - // .aligned() - // .contained() - // .with_background_color(microphone_state.unwrap_or(background_color)) - // .with_corner_radius(avatar_style.outer_corner_radius) - // .constrained() - // .with_width(avatar_style.outer_width) - // .with_height(avatar_style.outer_width) - // .aligned() - // .into_any() - // } - // fn render_connection_status( // &self, // status: &client::Status, @@ -1536,76 +807,3 @@ impl CollabTitlebarItem { // } // } } - -// pub struct AvatarRibbon { -// color: Color, -// } - -// impl AvatarRibbon { -// pub fn new(color: Color) -> AvatarRibbon { -// AvatarRibbon { color } -// } -// } - -// impl Element for AvatarRibbon { -// type LayoutState = (); - -// type PaintState = (); - -// fn layout( -// &mut self, -// constraint: gpui::SizeConstraint, -// _: &mut CollabTitlebarItem, -// _: &mut ViewContext, -// ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) { -// (constraint.max, ()) -// } - -// fn paint( -// &mut self, -// bounds: RectF, -// _: RectF, -// _: &mut Self::LayoutState, -// _: &mut CollabTitlebarItem, -// cx: &mut ViewContext, -// ) -> Self::PaintState { -// let mut path = PathBuilder::new(); -// path.reset(bounds.lower_left()); -// path.curve_to( -// bounds.origin() + vec2f(bounds.height(), 0.), -// bounds.origin(), -// ); -// path.line_to(bounds.upper_right() - vec2f(bounds.height(), 0.)); -// path.curve_to(bounds.lower_right(), bounds.upper_right()); -// path.line_to(bounds.lower_left()); -// cx.scene().push_path(path.build(self.color, None)); -// } - -// fn rect_for_text_range( -// &self, -// _: Range, -// _: RectF, -// _: RectF, -// _: &Self::LayoutState, -// _: &Self::PaintState, -// _: &CollabTitlebarItem, -// _: &ViewContext, -// ) -> Option { -// None -// } - -// fn debug( -// &self, -// bounds: RectF, -// _: &Self::LayoutState, -// _: &Self::PaintState, -// _: &CollabTitlebarItem, -// _: &ViewContext, -// ) -> gpui::json::Value { -// json::json!({ -// "type": "AvatarRibbon", -// "bounds": bounds.to_json(), -// "color": self.color.to_json(), -// }) -// } -// } diff --git a/crates/collab_ui2/src/face_pile.rs b/crates/collab_ui2/src/face_pile.rs index 162a3f261a..52b30062ea 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, } @@ -15,64 +16,15 @@ impl RenderOnce for FacePile { let player_list = self.faces.into_iter().enumerate().map(|(ix, player)| { let isnt_last = ix < player_count - 1; - div().when(isnt_last, |div| div.neg_mr_1()).child(player) + div() + .z_index((player_count - ix) as u32) + .when(isnt_last, |div| div.neg_mr_1()) + .child(player) }); div().p_1().flex().items_center().children(player_list) } } -// impl Element for FacePile { -// type State = (); -// fn layout( -// &mut self, -// state: Option, -// cx: &mut WindowContext, -// ) -> (LayoutId, Self::State) { -// let mut width = 0.; -// let mut max_height = 0.; -// let mut faces = Vec::with_capacity(self.faces.len()); -// for face in &mut self.faces { -// let layout = face.layout(cx); -// width += layout.x(); -// max_height = f32::max(max_height, layout.y()); -// faces.push(layout); -// } -// width -= self.overlap * self.faces.len().saturating_sub(1) as f32; -// (cx.request_layout(&Style::default(), faces), ()) -// // ( -// // Vector2F::new(width, max_height.clamp(1., constraint.max.y())), -// // (), -// // )) -// } - -// fn paint( -// &mut self, -// bounds: RectF, -// visible_bounds: RectF, -// _layout: &mut Self::LayoutState, -// view: &mut V, -// cx: &mut ViewContext, -// ) -> Self::PaintState { -// let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default(); - -// let origin_y = bounds.upper_right().y(); -// let mut origin_x = bounds.upper_right().x(); - -// for face in self.faces.iter_mut().rev() { -// let size = face.size(); -// origin_x -= size.x(); -// let origin_y = origin_y + (bounds.height() - size.y()) / 2.0; - -// cx.scene().push_layer(None); -// face.paint(vec2f(origin_x, origin_y), visible_bounds, view, cx); -// cx.scene().pop_layer(); -// origin_x += self.overlap; -// } - -// () -// } -// } - impl Extend for FacePile { fn extend>(&mut self, children: T) { self.faces.extend(children); 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) + } +} 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