From 6900e0c2ac98d7dad8c75007581616d46d6ff2a4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 23 Feb 2023 15:39:59 +0100 Subject: [PATCH] Merge pull request #2205 from zed-industries/call-ui-follow-up Refine new call UI --- .github/workflows/ci.yml | 4 +- Cargo.lock | 1 + assets/icons/ellipsis_14.svg | 3 + crates/call/src/room.rs | 45 +-- crates/client/src/client.rs | 22 +- crates/collab/src/db.rs | 33 +- crates/collab/src/tests/integration_tests.rs | 8 +- crates/collab_ui/Cargo.toml | 1 + crates/collab_ui/src/collab_titlebar_item.rs | 341 +++++++++---------- crates/context_menu/src/context_menu.rs | 45 ++- crates/theme/src/theme.rs | 1 + styles/src/styleTree/workspace.ts | 5 + 12 files changed, 253 insertions(+), 256 deletions(-) create mode 100644 assets/icons/ellipsis_14.svg diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index baa5da9bf7..a32f25fbe9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,9 @@ env: jobs: rustfmt: name: Check formatting - runs-on: self-hosted + runs-on: + - self-hosted + - test steps: - name: Install Rust run: | diff --git a/Cargo.lock b/Cargo.lock index e8410b25f0..c622fc7afe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1257,6 +1257,7 @@ dependencies = [ "client", "clock", "collections", + "context_menu", "editor", "futures 0.3.25", "fuzzy", diff --git a/assets/icons/ellipsis_14.svg b/assets/icons/ellipsis_14.svg new file mode 100644 index 0000000000..5d45af2b6f --- /dev/null +++ b/assets/icons/ellipsis_14.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 51b125577d..da31a223cb 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -277,14 +277,12 @@ impl Room { ) -> Result<()> { let mut client_status = client.status(); loop { - let is_connected = client_status - .next() - .await - .map_or(false, |s| s.is_connected()); - + let _ = client_status.try_recv(); + let is_connected = client_status.borrow().is_connected(); // Even if we're initially connected, any future change of the status means we momentarily disconnected. if !is_connected || client_status.next().await.is_some() { log::info!("detected client disconnection"); + this.upgrade(&cx) .ok_or_else(|| anyhow!("room was dropped"))? .update(&mut cx, |this, cx| { @@ -298,12 +296,7 @@ impl Room { let client_reconnection = async { let mut remaining_attempts = 3; while remaining_attempts > 0 { - log::info!( - "waiting for client status change, remaining attempts {}", - remaining_attempts - ); - let Some(status) = client_status.next().await else { break }; - if status.is_connected() { + if client_status.borrow().is_connected() { log::info!("client reconnected, attempting to rejoin room"); let Some(this) = this.upgrade(&cx) else { break }; @@ -317,7 +310,15 @@ impl Room { } else { remaining_attempts -= 1; } + } else if client_status.borrow().is_signed_out() { + return false; } + + log::info!( + "waiting for client status change, remaining attempts {}", + remaining_attempts + ); + client_status.next().await; } false } @@ -339,18 +340,20 @@ impl Room { } } - // The client failed to re-establish a connection to the server - // or an error occurred while trying to re-join the room. Either way - // we leave the room and return an error. - if let Some(this) = this.upgrade(&cx) { - log::info!("reconnection failed, leaving room"); - let _ = this.update(&mut cx, |this, cx| this.leave(cx)); - } - return Err(anyhow!( - "can't reconnect to room: client failed to re-establish connection" - )); + break; } } + + // The client failed to re-establish a connection to the server + // or an error occurred while trying to re-join the room. Either way + // we leave the room and return an error. + if let Some(this) = this.upgrade(&cx) { + log::info!("reconnection failed, leaving room"); + let _ = this.update(&mut cx, |this, cx| this.leave(cx)); + } + Err(anyhow!( + "can't reconnect to room: client failed to re-establish connection" + )) } fn rejoin(&mut self, cx: &mut ModelContext) -> Task> { diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index eba58304d7..f36fa67d9d 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -66,7 +66,7 @@ pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894"; pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(100); pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5); -actions!(client, [Authenticate]); +actions!(client, [Authenticate, SignOut]); pub fn init(client: Arc, cx: &mut MutableAppContext) { cx.add_global_action({ @@ -79,6 +79,16 @@ pub fn init(client: Arc, cx: &mut MutableAppContext) { .detach(); } }); + cx.add_global_action({ + let client = client.clone(); + move |_: &SignOut, cx| { + let client = client.clone(); + cx.spawn(|cx| async move { + client.disconnect(&cx); + }) + .detach(); + } + }); } pub struct Client { @@ -169,6 +179,10 @@ impl Status { pub fn is_connected(&self) -> bool { matches!(self, Self::Connected { .. }) } + + pub fn is_signed_out(&self) -> bool { + matches!(self, Self::SignedOut | Self::UpgradeRequired) + } } struct ClientState { @@ -1152,11 +1166,9 @@ impl Client { }) } - pub fn disconnect(self: &Arc, cx: &AsyncAppContext) -> Result<()> { - let conn_id = self.connection_id()?; - self.peer.disconnect(conn_id); + pub fn disconnect(self: &Arc, cx: &AsyncAppContext) { + self.peer.teardown(); self.set_status(Status::SignedOut, cx); - Ok(()) } fn connection_id(&self) -> Result { diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 97c9b6d344..9190855208 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -1724,8 +1724,8 @@ impl Database { leader_connection: ConnectionId, follower_connection: ConnectionId, ) -> Result> { - self.room_transaction(|tx| async move { - let room_id = self.room_id_for_project(project_id, &*tx).await?; + let room_id = self.room_id_for_project(project_id).await?; + self.room_transaction(room_id, |tx| async move { follower::ActiveModel { room_id: ActiveValue::set(room_id), project_id: ActiveValue::set(project_id), @@ -1742,7 +1742,8 @@ impl Database { .insert(&*tx) .await?; - Ok((room_id, self.get_room(room_id, &*tx).await?)) + let room = self.get_room(room_id, &*tx).await?; + Ok(room) }) .await } @@ -1753,8 +1754,8 @@ impl Database { leader_connection: ConnectionId, follower_connection: ConnectionId, ) -> Result> { - self.room_transaction(|tx| async move { - let room_id = self.room_id_for_project(project_id, &*tx).await?; + let room_id = self.room_id_for_project(project_id).await?; + self.room_transaction(room_id, |tx| async move { follower::Entity::delete_many() .filter( Condition::all() @@ -1776,30 +1777,12 @@ impl Database { .exec(&*tx) .await?; - Ok((room_id, self.get_room(room_id, &*tx).await?)) + let room = self.get_room(room_id, &*tx).await?; + Ok(room) }) .await } - async fn room_id_for_project( - &self, - project_id: ProjectId, - tx: &DatabaseTransaction, - ) -> Result { - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryAs { - RoomId, - } - - Ok(project::Entity::find_by_id(project_id) - .select_only() - .column(project::Column::RoomId) - .into_values::<_, QueryAs>() - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such project"))?) - } - pub async fn update_room_participant_location( &self, room_id: RoomId, diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 7c4087f540..f70fdfb0ba 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -1083,7 +1083,7 @@ async fn test_calls_on_multiple_connections( assert!(incoming_call_b2.next().await.unwrap().is_none()); // User B disconnects the client that is not on the call. Everything should be fine. - client_b1.disconnect(&cx_b1.to_async()).unwrap(); + client_b1.disconnect(&cx_b1.to_async()); deterministic.advance_clock(RECEIVE_TIMEOUT); client_b1 .authenticate_and_connect(false, &cx_b1.to_async()) @@ -3227,7 +3227,7 @@ async fn test_leaving_project( buffer_b2.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents")); // Drop client B's connection and ensure client A and client C observe client B leaving. - client_b.disconnect(&cx_b.to_async()).unwrap(); + client_b.disconnect(&cx_b.to_async()); deterministic.advance_clock(RECONNECT_TIMEOUT); project_a.read_with(cx_a, |project, _| { assert_eq!(project.collaborators().len(), 1); @@ -5772,7 +5772,7 @@ async fn test_contact_requests( .is_empty()); async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) { - client.disconnect(&cx.to_async()).unwrap(); + client.disconnect(&cx.to_async()); client.clear_contacts(cx).await; client .authenticate_and_connect(false, &cx.to_async()) @@ -6186,7 +6186,7 @@ async fn test_following( ); // Following interrupts when client B disconnects. - client_b.disconnect(&cx_b.to_async()).unwrap(); + client_b.disconnect(&cx_b.to_async()); deterministic.advance_clock(RECONNECT_TIMEOUT); assert_eq!( workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index 2dc4cc769a..899f8cc8b4 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -27,6 +27,7 @@ call = { path = "../call" } client = { path = "../client" } clock = { path = "../clock" } collections = { path = "../collections" } +context_menu = { path = "../context_menu" } editor = { path = "../editor" } fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 061de78b9f..35ffb5c729 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -4,9 +4,10 @@ use crate::{ ToggleScreenSharing, }; use call::{ActiveCall, ParticipantLocation, Room}; -use client::{proto::PeerId, Authenticate, ContactEventKind, User, UserStore}; +use client::{proto::PeerId, Authenticate, ContactEventKind, SignOut, User, UserStore}; use clock::ReplicaId; use contacts_popover::ContactsPopover; +use context_menu::{ContextMenu, ContextMenuItem}; use gpui::{ actions, color::Color, @@ -28,8 +29,9 @@ actions!( [ ToggleCollaboratorList, ToggleContactsMenu, + ToggleUserMenu, ShareProject, - UnshareProject + UnshareProject, ] ); @@ -38,25 +40,20 @@ impl_internal_actions!(collab, [LeaveCall]); #[derive(Copy, Clone, PartialEq)] pub(crate) struct LeaveCall; -#[derive(PartialEq, Eq)] -enum ContactsPopoverSide { - Left, - Right, -} - pub fn init(cx: &mut MutableAppContext) { cx.add_action(CollabTitlebarItem::toggle_collaborator_list_popover); cx.add_action(CollabTitlebarItem::toggle_contacts_popover); cx.add_action(CollabTitlebarItem::share_project); cx.add_action(CollabTitlebarItem::unshare_project); cx.add_action(CollabTitlebarItem::leave_call); + cx.add_action(CollabTitlebarItem::toggle_user_menu); } pub struct CollabTitlebarItem { workspace: WeakViewHandle, user_store: ModelHandle, contacts_popover: Option>, - contacts_popover_side: ContactsPopoverSide, + user_menu: ViewHandle, collaborator_list_popover: Option>, _subscriptions: Vec, } @@ -90,9 +87,9 @@ impl View for CollabTitlebarItem { } let theme = cx.global::().theme.clone(); - let user = workspace.read(cx).user_store().read(cx).current_user(); let mut left_container = Flex::row(); + let mut right_container = Flex::row(); left_container.add_child( Label::new(project_title, theme.workspace.titlebar.title.clone()) @@ -103,41 +100,31 @@ impl View for CollabTitlebarItem { .boxed(), ); - if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { - left_container.add_child(self.render_current_user(&workspace, &theme, &user, cx)); - left_container.add_children(self.render_collaborators(&workspace, &theme, room, cx)); - left_container.add_child(self.render_toggle_contacts_button(&theme, cx)); - } - - let mut right_container = Flex::row(); - - if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { - right_container.add_child(self.render_toggle_screen_sharing_button(&theme, &room, cx)); - right_container.add_child(self.render_leave_call_button(&theme, cx)); - right_container + let user = workspace.read(cx).user_store().read(cx).current_user(); + let peer_id = workspace.read(cx).client().peer_id(); + if let Some(((user, peer_id), room)) = user + .zip(peer_id) + .zip(ActiveCall::global(cx).read(cx).room().cloned()) + { + left_container .add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx)); - } else { - right_container.add_child(self.render_outside_call_share_button(&theme, cx)); + + right_container.add_children(self.render_collaborators(&workspace, &theme, &room, cx)); + right_container + .add_child(self.render_current_user(&workspace, &theme, &user, peer_id, cx)); + right_container.add_child(self.render_toggle_screen_sharing_button(&theme, &room, cx)); } - right_container.add_children(self.render_connection_status(&workspace, cx)); - - if let Some(user) = user { - //TODO: Add style - right_container.add_child( - Label::new( - user.github_login.clone(), - theme.workspace.titlebar.title.clone(), - ) - .aligned() - .contained() - .with_margin_left(theme.workspace.titlebar.item_spacing) - .boxed(), - ); + let status = workspace.read(cx).client().status(); + let status = &*status.borrow(); + if matches!(status, client::Status::Connected { .. }) { + right_container.add_child(self.render_toggle_contacts_button(&theme, cx)); } else { - right_container.add_child(Self::render_authenticate(&theme, cx)); + right_container.add_children(self.render_connection_status(status, cx)); } + right_container.add_child(self.render_user_menu_button(&theme, cx)); + Stack::new() .with_child(left_container.boxed()) .with_child(right_container.aligned().right().boxed()) @@ -186,7 +173,11 @@ impl CollabTitlebarItem { workspace: workspace.downgrade(), user_store: user_store.clone(), contacts_popover: None, - contacts_popover_side: ContactsPopoverSide::Right, + user_menu: cx.add_view(|cx| { + let mut menu = ContextMenu::new(cx); + menu.set_position_mode(OverlayPositionMode::Local); + menu + }), collaborator_list_popover: None, _subscriptions: subscriptions, } @@ -278,12 +269,6 @@ impl CollabTitlebarItem { cx.notify(); }) .detach(); - - self.contacts_popover_side = match ActiveCall::global(cx).read(cx).room() { - Some(_) => ContactsPopoverSide::Left, - None => ContactsPopoverSide::Right, - }; - self.contacts_popover = Some(view); } } @@ -291,6 +276,59 @@ impl CollabTitlebarItem { cx.notify(); } + pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext) { + let theme = cx.global::().theme.clone(); + let avatar_style = theme.workspace.titlebar.avatar.clone(); + let item_style = theme.context_menu.item.disabled_style().clone(); + self.user_menu.update(cx, |user_menu, cx| { + let items = if let Some(user) = self.user_store.read(cx).current_user() { + vec![ + ContextMenuItem::Static(Box::new(move |_| { + Flex::row() + .with_children(user.avatar.clone().map(|avatar| { + Self::render_face( + avatar, + avatar_style.clone(), + Color::transparent_black(), + ) + })) + .with_child( + Label::new(user.github_login.clone(), item_style.label.clone()) + .boxed(), + ) + .contained() + .with_style(item_style.container) + .boxed() + })), + ContextMenuItem::Item { + label: "Sign out".into(), + action: Box::new(SignOut), + }, + ] + } else { + vec![ContextMenuItem::Item { + label: "Sign in".into(), + action: Box::new(Authenticate), + }] + }; + + user_menu.show( + vec2f( + theme + .workspace + .titlebar + .user_menu_button + .default + .button_width, + theme.workspace.titlebar.height, + ), + AnchorCorner::TopRight, + items, + cx, + ); + }); + } + fn leave_call(&mut self, _: &LeaveCall, cx: &mut ViewContext) { ActiveCall::global(cx) .update(cx, |call, cx| call.hang_up(cx)) @@ -328,11 +366,9 @@ impl CollabTitlebarItem { Stack::new() .with_child( MouseEventHandler::::new(0, cx, |state, _| { - let style = titlebar.toggle_contacts_button.style_for( - state, - self.contacts_popover.is_some() - && self.contacts_popover_side == ContactsPopoverSide::Left, - ); + let style = titlebar + .toggle_contacts_button + .style_for(state, self.contacts_popover.is_some()); Svg::new("icons/plus_8.svg") .with_color(style.color) .constrained() @@ -360,11 +396,7 @@ impl CollabTitlebarItem { .boxed(), ) .with_children(badge) - .with_children(self.render_contacts_popover_host( - ContactsPopoverSide::Left, - titlebar, - cx, - )) + .with_children(self.render_contacts_popover_host(titlebar, cx)) .boxed() } @@ -414,40 +446,6 @@ impl CollabTitlebarItem { .boxed() } - fn render_leave_call_button(&self, theme: &Theme, cx: &mut RenderContext) -> ElementBox { - let titlebar = &theme.workspace.titlebar; - - MouseEventHandler::::new(0, cx, |state, _| { - let style = titlebar.call_control.style_for(state, false); - Svg::new("icons/leave_12.svg") - .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) - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, cx| { - cx.dispatch_action(LeaveCall); - }) - .with_tooltip::( - 0, - "Leave call".to_owned(), - Some(Box::new(LeaveCall)), - theme.tooltip.clone(), - cx, - ) - .contained() - .with_margin_left(theme.workspace.titlebar.item_spacing) - .aligned() - .boxed() - } - fn render_in_call_share_unshare_button( &self, workspace: &ViewHandle, @@ -475,11 +473,9 @@ impl CollabTitlebarItem { .with_child( MouseEventHandler::::new(0, cx, |state, _| { //TODO: Ensure this button has consistant width for both text variations - let style = titlebar.share_button.style_for( - state, - self.contacts_popover.is_some() - && self.contacts_popover_side == ContactsPopoverSide::Right, - ); + let style = titlebar + .share_button + .style_for(state, self.contacts_popover.is_some()); Label::new(label, style.text.clone()) .contained() .with_style(style.container) @@ -502,11 +498,6 @@ impl CollabTitlebarItem { ) .boxed(), ) - .with_children(self.render_contacts_popover_host( - ContactsPopoverSide::Right, - titlebar, - cx, - )) .aligned() .contained() .with_margin_left(theme.workspace.titlebar.item_spacing) @@ -514,83 +505,71 @@ impl CollabTitlebarItem { ) } - fn render_outside_call_share_button( - &self, - theme: &Theme, - cx: &mut RenderContext, - ) -> ElementBox { - let tooltip = "Share project with new call"; + fn render_user_menu_button(&self, theme: &Theme, cx: &mut RenderContext) -> ElementBox { let titlebar = &theme.workspace.titlebar; - enum OutsideCallShare {} Stack::new() .with_child( - MouseEventHandler::::new(0, cx, |state, _| { - //TODO: Ensure this button has consistant width for both text variations - let style = titlebar.share_button.style_for( - state, - self.contacts_popover.is_some() - && self.contacts_popover_side == ContactsPopoverSide::Right, - ); - Label::new("Share".to_owned(), style.text.clone()) + MouseEventHandler::::new(0, cx, |state, _| { + let style = titlebar.call_control.style_for(state, false); + Svg::new("icons/ellipsis_14.svg") + .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) .boxed() }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, cx| { - cx.dispatch_action(ToggleContactsMenu); + cx.dispatch_action(ToggleUserMenu); }) - .with_tooltip::( + .with_tooltip::( 0, - tooltip.to_owned(), - None, + "Toggle user menu".to_owned(), + Some(Box::new(ToggleUserMenu)), theme.tooltip.clone(), cx, ) + .contained() + .with_margin_left(theme.workspace.titlebar.item_spacing) + .aligned() .boxed(), ) - .with_children(self.render_contacts_popover_host( - ContactsPopoverSide::Right, - titlebar, - cx, - )) - .aligned() - .contained() - .with_margin_left(theme.workspace.titlebar.item_spacing) + .with_child(ChildView::new(&self.user_menu, cx).boxed()) .boxed() } fn render_contacts_popover_host<'a>( &'a self, - side: ContactsPopoverSide, theme: &'a theme::Titlebar, cx: &'a RenderContext, - ) -> impl Iterator + 'a { - self.contacts_popover - .iter() - .filter(move |_| self.contacts_popover_side == side) - .map(|popover| { - Overlay::new( - ChildView::new(popover, cx) - .contained() - .with_margin_top(theme.height) - .with_margin_left(theme.toggle_contacts_button.default.button_width) - .with_margin_right(-theme.toggle_contacts_button.default.button_width) - .boxed(), - ) - .with_fit_mode(OverlayFitMode::SwitchAnchor) - .with_anchor_corner(AnchorCorner::BottomLeft) - .with_z_index(999) - .boxed() - }) + ) -> Option { + self.contacts_popover.as_ref().map(|popover| { + Overlay::new( + ChildView::new(popover, cx) + .contained() + .with_margin_top(theme.height) + .with_margin_left(theme.toggle_contacts_button.default.button_width) + .with_margin_right(-theme.toggle_contacts_button.default.button_width) + .boxed(), + ) + .with_fit_mode(OverlayFitMode::SwitchAnchor) + .with_anchor_corner(AnchorCorner::BottomLeft) + .with_z_index(999) + .boxed() + }) } fn render_collaborators( &self, workspace: &ViewHandle, theme: &Theme, - room: ModelHandle, + room: &ModelHandle, cx: &mut RenderContext, ) -> Vec { let project = workspace.read(cx).project().read(cx); @@ -622,7 +601,7 @@ impl CollabTitlebarItem { theme, cx, )) - .with_margin_left(theme.workspace.titlebar.face_pile_spacing) + .with_margin_right(theme.workspace.titlebar.face_pile_spacing) .boxed(), ) }) @@ -633,35 +612,21 @@ impl CollabTitlebarItem { &self, workspace: &ViewHandle, theme: &Theme, - user: &Option>, + user: &Arc, + peer_id: PeerId, cx: &mut RenderContext, ) -> ElementBox { - let user = user.as_ref().expect("Active call without user"); let replica_id = workspace.read(cx).project().read(cx).replica_id(); - let peer_id = workspace - .read(cx) - .client() - .peer_id() - .expect("Active call without peer id"); - self.render_face_pile(user, Some(replica_id), peer_id, None, workspace, theme, cx) - } - - fn render_authenticate(theme: &Theme, cx: &mut RenderContext) -> ElementBox { - MouseEventHandler::::new(0, cx, |state, _| { - let style = theme - .workspace - .titlebar - .sign_in_prompt - .style_for(state, false); - Label::new("Sign in", style.text.clone()) - .contained() - .with_style(style.container) - .with_margin_left(theme.workspace.titlebar.item_spacing) - .boxed() - }) - .on_click(MouseButton::Left, |_, cx| cx.dispatch_action(Authenticate)) - .with_cursor_style(CursorStyle::PointingHand) - .aligned() + Container::new(self.render_face_pile( + user, + Some(replica_id), + peer_id, + None, + workspace, + theme, + cx, + )) + .with_margin_right(theme.workspace.titlebar.item_spacing) .boxed() } @@ -717,7 +682,7 @@ impl CollabTitlebarItem { } } - let content = Stack::new() + let mut content = Stack::new() .with_children(user.avatar.as_ref().map(|avatar| { let face_pile = FacePile::new(theme.workspace.titlebar.follower_avatar_overlap) .with_child(Self::render_face( @@ -789,7 +754,10 @@ impl CollabTitlebarItem { if let Some(location) = location { if let Some(replica_id) = replica_id { - MouseEventHandler::::new(replica_id.into(), cx, move |_, _| content) + content = + MouseEventHandler::::new(replica_id.into(), cx, move |_, _| { + content + }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, cx| { cx.dispatch_action(ToggleFollow(peer_id)) @@ -805,12 +773,14 @@ impl CollabTitlebarItem { theme.tooltip.clone(), cx, ) - .boxed() + .boxed(); } else if let ParticipantLocation::SharedProject { project_id } = location { let user_id = user.id; - MouseEventHandler::::new(peer_id.as_u64() as usize, cx, move |_, _| { - content - }) + content = MouseEventHandler::::new( + peer_id.as_u64() as usize, + cx, + move |_, _| content, + ) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, cx| { cx.dispatch_action(JoinProject { @@ -825,13 +795,10 @@ impl CollabTitlebarItem { theme.tooltip.clone(), cx, ) - .boxed() - } else { - content + .boxed(); } - } else { - content } + content } fn render_face( @@ -854,13 +821,13 @@ impl CollabTitlebarItem { fn render_connection_status( &self, - workspace: &ViewHandle, + status: &client::Status, cx: &mut RenderContext, ) -> Option { enum ConnectionStatusButton {} let theme = &cx.global::().theme.clone(); - match &*workspace.read(cx).client().status().borrow() { + match status { client::Status::ConnectionError | client::Status::ConnectionLost | client::Status::Reauthenticating { .. } diff --git a/crates/context_menu/src/context_menu.rs b/crates/context_menu/src/context_menu.rs index 6d5a5cb549..eb02334c70 100644 --- a/crates/context_menu/src/context_menu.rs +++ b/crates/context_menu/src/context_menu.rs @@ -5,7 +5,9 @@ use gpui::{ }; use menu::*; use settings::Settings; -use std::{any::TypeId, time::Duration}; +use std::{any::TypeId, borrow::Cow, time::Duration}; + +pub type StaticItem = Box ElementBox>; #[derive(Copy, Clone, PartialEq)] struct Clicked; @@ -24,16 +26,17 @@ pub fn init(cx: &mut MutableAppContext) { pub enum ContextMenuItem { Item { - label: String, + label: Cow<'static, str>, action: Box, }, + Static(StaticItem), Separator, } impl ContextMenuItem { - pub fn item(label: impl ToString, action: impl 'static + Action) -> Self { + pub fn item(label: impl Into>, action: impl 'static + Action) -> Self { Self::Item { - label: label.to_string(), + label: label.into(), action: Box::new(action), } } @@ -42,14 +45,14 @@ impl ContextMenuItem { Self::Separator } - fn is_separator(&self) -> bool { - matches!(self, Self::Separator) + fn is_action(&self) -> bool { + matches!(self, Self::Item { .. }) } fn action_id(&self) -> Option { match self { ContextMenuItem::Item { action, .. } => Some(action.id()), - ContextMenuItem::Separator => None, + ContextMenuItem::Static(..) | ContextMenuItem::Separator => None, } } } @@ -58,6 +61,7 @@ pub struct ContextMenu { show_count: usize, anchor_position: Vector2F, anchor_corner: AnchorCorner, + position_mode: OverlayPositionMode, items: Vec, selected_index: Option, visible: bool, @@ -105,6 +109,7 @@ impl View for ContextMenu { .with_fit_mode(OverlayFitMode::SnapToWindow) .with_anchor_position(self.anchor_position) .with_anchor_corner(self.anchor_corner) + .with_position_mode(self.position_mode) .boxed() } @@ -121,6 +126,7 @@ impl ContextMenu { show_count: 0, anchor_position: Default::default(), anchor_corner: AnchorCorner::TopLeft, + position_mode: OverlayPositionMode::Window, items: Default::default(), selected_index: Default::default(), visible: Default::default(), @@ -188,13 +194,13 @@ impl ContextMenu { } fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext) { - self.selected_index = self.items.iter().position(|item| !item.is_separator()); + self.selected_index = self.items.iter().position(|item| item.is_action()); cx.notify(); } fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext) { for (ix, item) in self.items.iter().enumerate().rev() { - if !item.is_separator() { + if item.is_action() { self.selected_index = Some(ix); cx.notify(); break; @@ -205,7 +211,7 @@ impl ContextMenu { fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { if let Some(ix) = self.selected_index { for (ix, item) in self.items.iter().enumerate().skip(ix + 1) { - if !item.is_separator() { + if item.is_action() { self.selected_index = Some(ix); cx.notify(); break; @@ -219,7 +225,7 @@ impl ContextMenu { fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { if let Some(ix) = self.selected_index { for (ix, item) in self.items.iter().enumerate().take(ix).rev() { - if !item.is_separator() { + if item.is_action() { self.selected_index = Some(ix); cx.notify(); break; @@ -234,7 +240,7 @@ impl ContextMenu { &mut self, anchor_position: Vector2F, anchor_corner: AnchorCorner, - items: impl IntoIterator, + items: Vec, cx: &mut ViewContext, ) { let mut items = items.into_iter().peekable(); @@ -254,6 +260,10 @@ impl ContextMenu { cx.notify(); } + pub fn set_position_mode(&mut self, mode: OverlayPositionMode) { + self.position_mode = mode; + } + fn render_menu_for_measurement(&self, cx: &mut RenderContext) -> impl Element { let window_id = cx.window_id(); let style = cx.global::().theme.context_menu.clone(); @@ -273,6 +283,9 @@ impl ContextMenu { .with_style(style.container) .boxed() } + + ContextMenuItem::Static(f) => f(cx), + ContextMenuItem::Separator => Empty::new() .collapsed() .contained() @@ -302,6 +315,9 @@ impl ContextMenu { ) .boxed() } + + ContextMenuItem::Static(_) => Empty::new().boxed(), + ContextMenuItem::Separator => Empty::new() .collapsed() .constrained() @@ -339,7 +355,7 @@ impl ContextMenu { Flex::row() .with_child( - Label::new(label.to_string(), style.label.clone()) + Label::new(label.clone(), style.label.clone()) .contained() .boxed(), ) @@ -366,6 +382,9 @@ impl ContextMenu { .on_drag(MouseButton::Left, |_, _| {}) .boxed() } + + ContextMenuItem::Static(f) => f(cx), + ContextMenuItem::Separator => Empty::new() .constrained() .with_height(1.) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 17a7c876bb..43e3b6deec 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -88,6 +88,7 @@ pub struct Titlebar { pub share_button: Interactive, pub call_control: Interactive, pub toggle_contacts_button: Interactive, + pub user_menu_button: Interactive, pub toggle_contacts_badge: ContainerStyle, } diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index 659a0e6745..db1f0e7286 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -197,6 +197,11 @@ export default function workspace(colorScheme: ColorScheme) { color: foreground(layer, "variant", "hovered"), }, }, + userMenuButton: { + buttonWidth: 20, + iconWidth: 12, + ...titlebarButton, + }, toggleContactsBadge: { cornerRadius: 3, padding: 2,