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,