Detailed changes
@@ -0,0 +1,3 @@
+<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M0 1C0 0.585786 0.335786 0.25 0.75 0.25H7.25C7.66421 0.25 8 0.585786 8 1C8 1.41421 7.66421 1.75 7.25 1.75H1.5V10.25H7.25C7.66421 10.25 8 10.5858 8 11C8 11.4142 7.66421 11.75 7.25 11.75H0.75C0.335786 11.75 0 11.4142 0 11V1ZM8.78148 2.91435C9.10493 2.65559 9.57689 2.70803 9.83565 3.03148L11.8357 5.53148C12.0548 5.80539 12.0548 6.19461 11.8357 6.46852L9.83565 8.96852C9.57689 9.29197 9.10493 9.34441 8.78148 9.08565C8.45803 8.82689 8.40559 8.35493 8.66435 8.03148L9.68953 6.75H3.75C3.33579 6.75 3 6.41421 3 6C3 5.58579 3.33579 5.25 3.75 5.25H9.68953L8.66435 3.96852C8.40559 3.64507 8.45803 3.17311 8.78148 2.91435Z" fill="#ABB2BF"/>
+</svg>
@@ -284,6 +284,18 @@ impl ActiveCall {
}
}
+ pub fn unshare_project(
+ &mut self,
+ project: ModelHandle<Project>,
+ cx: &mut ModelContext<Self>,
+ ) -> Result<()> {
+ if let Some((room, _)) = self.room.as_ref() {
+ room.update(cx, |room, cx| room.unshare_project(project, cx))
+ } else {
+ Err(anyhow!("no active call"))
+ }
+ }
+
pub fn set_location(
&mut self,
project: Option<&ModelHandle<Project>>,
@@ -55,6 +55,7 @@ pub struct Room {
leave_when_empty: bool,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
+ follows_by_leader_id: HashMap<PeerId, Vec<PeerId>>,
subscriptions: Vec<client::Subscription>,
pending_room_update: Option<Task<()>>,
maintain_connection: Option<Task<Option<()>>>,
@@ -148,6 +149,7 @@ impl Room {
pending_room_update: None,
client,
user_store,
+ follows_by_leader_id: Default::default(),
maintain_connection: Some(maintain_connection),
}
}
@@ -457,6 +459,12 @@ impl Room {
self.participant_user_ids.contains(&user_id)
}
+ pub fn followers_for(&self, leader_id: PeerId) -> &[PeerId] {
+ self.follows_by_leader_id
+ .get(&leader_id)
+ .map_or(&[], |v| v.as_slice())
+ }
+
async fn handle_room_updated(
this: ModelHandle<Self>,
envelope: TypedEnvelope<proto::RoomUpdated>,
@@ -487,11 +495,13 @@ impl Room {
.iter()
.map(|p| p.user_id)
.collect::<Vec<_>>();
+
let remote_participant_user_ids = room
.participants
.iter()
.map(|p| p.user_id)
.collect::<Vec<_>>();
+
let (remote_participants, pending_participants) =
self.user_store.update(cx, move |user_store, cx| {
(
@@ -499,6 +509,7 @@ impl Room {
user_store.get_users(pending_participant_user_ids, cx),
)
});
+
self.pending_room_update = Some(cx.spawn(|this, mut cx| async move {
let (remote_participants, pending_participants) =
futures::join!(remote_participants, pending_participants);
@@ -620,6 +631,26 @@ impl Room {
}
}
+ this.follows_by_leader_id.clear();
+ for follower in room.followers {
+ let (leader, follower) = match (follower.leader_id, follower.follower_id) {
+ (Some(leader), Some(follower)) => (leader, follower),
+
+ _ => {
+ log::error!("Follower message {follower:?} missing some state");
+ continue;
+ }
+ };
+
+ let list = this
+ .follows_by_leader_id
+ .entry(leader)
+ .or_insert(Vec::new());
+ if !list.contains(&follower) {
+ list.push(follower);
+ }
+ }
+
this.pending_room_update.take();
if this.should_leave() {
log::info!("room is empty, leaving");
@@ -793,6 +824,20 @@ impl Room {
})
}
+ pub(crate) fn unshare_project(
+ &mut self,
+ project: ModelHandle<Project>,
+ cx: &mut ModelContext<Self>,
+ ) -> Result<()> {
+ let project_id = match project.read(cx).remote_id() {
+ Some(project_id) => project_id,
+ None => return Ok(()),
+ };
+
+ self.client.send(proto::UnshareProject { project_id })?;
+ project.update(cx, |this, cx| this.unshare(cx))
+ }
+
pub(crate) fn set_location(
&mut self,
project: Option<&ModelHandle<Project>>,
@@ -143,3 +143,17 @@ CREATE TABLE "servers" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"environment" VARCHAR NOT NULL
);
+
+CREATE TABLE "followers" (
+ "id" INTEGER PRIMARY KEY AUTOINCREMENT,
+ "room_id" INTEGER NOT NULL REFERENCES rooms (id) ON DELETE CASCADE,
+ "project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
+ "leader_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
+ "leader_connection_id" INTEGER NOT NULL,
+ "follower_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
+ "follower_connection_id" INTEGER NOT NULL
+);
+CREATE UNIQUE INDEX
+ "index_followers_on_project_id_and_leader_connection_server_id_and_leader_connection_id_and_follower_connection_server_id_and_follower_connection_id"
+ON "followers" ("project_id", "leader_connection_server_id", "leader_connection_id", "follower_connection_server_id", "follower_connection_id");
+CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id");
@@ -0,0 +1,15 @@
+CREATE TABLE IF NOT EXISTS "followers" (
+ "id" SERIAL PRIMARY KEY,
+ "room_id" INTEGER NOT NULL REFERENCES rooms (id) ON DELETE CASCADE,
+ "project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
+ "leader_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
+ "leader_connection_id" INTEGER NOT NULL,
+ "follower_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
+ "follower_connection_id" INTEGER NOT NULL
+);
+
+CREATE UNIQUE INDEX
+ "index_followers_on_project_id_and_leader_connection_server_id_and_leader_connection_id_and_follower_connection_server_id_and_follower_connection_id"
+ON "followers" ("project_id", "leader_connection_server_id", "leader_connection_id", "follower_connection_server_id", "follower_connection_id");
+
+CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id");
@@ -1,5 +1,6 @@
mod access_token;
mod contact;
+mod follower;
mod language_server;
mod project;
mod project_collaborator;
@@ -1717,6 +1718,88 @@ impl Database {
.await
}
+ pub async fn follow(
+ &self,
+ project_id: ProjectId,
+ leader_connection: ConnectionId,
+ follower_connection: ConnectionId,
+ ) -> Result<RoomGuard<proto::Room>> {
+ self.room_transaction(|tx| async move {
+ let room_id = self.room_id_for_project(project_id, &*tx).await?;
+ follower::ActiveModel {
+ room_id: ActiveValue::set(room_id),
+ project_id: ActiveValue::set(project_id),
+ leader_connection_server_id: ActiveValue::set(ServerId(
+ leader_connection.owner_id as i32,
+ )),
+ leader_connection_id: ActiveValue::set(leader_connection.id as i32),
+ follower_connection_server_id: ActiveValue::set(ServerId(
+ follower_connection.owner_id as i32,
+ )),
+ follower_connection_id: ActiveValue::set(follower_connection.id as i32),
+ ..Default::default()
+ }
+ .insert(&*tx)
+ .await?;
+
+ Ok((room_id, self.get_room(room_id, &*tx).await?))
+ })
+ .await
+ }
+
+ pub async fn unfollow(
+ &self,
+ project_id: ProjectId,
+ leader_connection: ConnectionId,
+ follower_connection: ConnectionId,
+ ) -> Result<RoomGuard<proto::Room>> {
+ self.room_transaction(|tx| async move {
+ let room_id = self.room_id_for_project(project_id, &*tx).await?;
+ follower::Entity::delete_many()
+ .filter(
+ Condition::all()
+ .add(follower::Column::ProjectId.eq(project_id))
+ .add(
+ follower::Column::LeaderConnectionServerId
+ .eq(leader_connection.owner_id)
+ .and(follower::Column::LeaderConnectionId.eq(leader_connection.id)),
+ )
+ .add(
+ follower::Column::FollowerConnectionServerId
+ .eq(follower_connection.owner_id)
+ .and(
+ follower::Column::FollowerConnectionId
+ .eq(follower_connection.id),
+ ),
+ ),
+ )
+ .exec(&*tx)
+ .await?;
+
+ Ok((room_id, self.get_room(room_id, &*tx).await?))
+ })
+ .await
+ }
+
+ async fn room_id_for_project(
+ &self,
+ project_id: ProjectId,
+ tx: &DatabaseTransaction,
+ ) -> Result<RoomId> {
+ #[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,
@@ -1926,12 +2009,24 @@ impl Database {
}
}
}
+ drop(db_projects);
+
+ let mut db_followers = db_room.find_related(follower::Entity).stream(tx).await?;
+ let mut followers = Vec::new();
+ while let Some(db_follower) = db_followers.next().await {
+ let db_follower = db_follower?;
+ followers.push(proto::Follower {
+ leader_id: Some(db_follower.leader_connection().into()),
+ follower_id: Some(db_follower.follower_connection().into()),
+ });
+ }
Ok(proto::Room {
id: db_room.id.to_proto(),
live_kit_room: db_room.live_kit_room,
participants: participants.into_values().collect(),
pending_participants,
+ followers,
})
}
@@ -3011,6 +3106,7 @@ macro_rules! id_type {
id_type!(AccessTokenId);
id_type!(ContactId);
+id_type!(FollowerId);
id_type!(RoomId);
id_type!(RoomParticipantId);
id_type!(ProjectId);
@@ -0,0 +1,51 @@
+use super::{FollowerId, ProjectId, RoomId, ServerId};
+use rpc::ConnectionId;
+use sea_orm::entity::prelude::*;
+use serde::Serialize;
+
+#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel, Serialize)]
+#[sea_orm(table_name = "followers")]
+pub struct Model {
+ #[sea_orm(primary_key)]
+ pub id: FollowerId,
+ pub room_id: RoomId,
+ pub project_id: ProjectId,
+ pub leader_connection_server_id: ServerId,
+ pub leader_connection_id: i32,
+ pub follower_connection_server_id: ServerId,
+ pub follower_connection_id: i32,
+}
+
+impl Model {
+ pub fn leader_connection(&self) -> ConnectionId {
+ ConnectionId {
+ owner_id: self.leader_connection_server_id.0 as u32,
+ id: self.leader_connection_id as u32,
+ }
+ }
+
+ pub fn follower_connection(&self) -> ConnectionId {
+ ConnectionId {
+ owner_id: self.follower_connection_server_id.0 as u32,
+ id: self.follower_connection_id as u32,
+ }
+ }
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {
+ #[sea_orm(
+ belongs_to = "super::room::Entity",
+ from = "Column::RoomId",
+ to = "super::room::Column::Id"
+ )]
+ Room,
+}
+
+impl Related<super::room::Entity> for Entity {
+ fn to() -> RelationDef {
+ Relation::Room.def()
+ }
+}
+
+impl ActiveModelBehavior for ActiveModel {}
@@ -15,6 +15,8 @@ pub enum Relation {
RoomParticipant,
#[sea_orm(has_many = "super::project::Entity")]
Project,
+ #[sea_orm(has_many = "super::follower::Entity")]
+ Follower,
}
impl Related<super::room_participant::Entity> for Entity {
@@ -29,4 +31,10 @@ impl Related<super::project::Entity> for Entity {
}
}
+impl Related<super::follower::Entity> for Entity {
+ fn to() -> RelationDef {
+ Relation::Follower.def()
+ }
+}
+
impl ActiveModelBehavior for ActiveModel {}
@@ -1312,6 +1312,7 @@ async fn join_project(
.filter(|collaborator| collaborator.connection_id != session.connection_id)
.map(|collaborator| collaborator.to_proto())
.collect::<Vec<_>>();
+
let worktrees = project
.worktrees
.iter()
@@ -1724,6 +1725,7 @@ async fn follow(
.ok_or_else(|| anyhow!("invalid leader id"))?
.into();
let follower_id = session.connection_id;
+
{
let project_connection_ids = session
.db()
@@ -1744,6 +1746,14 @@ async fn follow(
.views
.retain(|view| view.leader_id != Some(follower_id.into()));
response.send(response_payload)?;
+
+ let room = session
+ .db()
+ .await
+ .follow(project_id, leader_id, follower_id)
+ .await?;
+ room_updated(&room, &session.peer);
+
Ok(())
}
@@ -1753,17 +1763,29 @@ async fn unfollow(request: proto::Unfollow, session: Session) -> Result<()> {
.leader_id
.ok_or_else(|| anyhow!("invalid leader id"))?
.into();
- let project_connection_ids = session
+ let follower_id = session.connection_id;
+
+ if !session
.db()
.await
.project_connection_ids(project_id, session.connection_id)
- .await?;
- if !project_connection_ids.contains(&leader_id) {
+ .await?
+ .contains(&leader_id)
+ {
Err(anyhow!("no such peer"))?;
}
+
session
.peer
.forward_send(session.connection_id, leader_id, request)?;
+
+ let room = session
+ .db()
+ .await
+ .unfollow(project_id, leader_id, follower_id)
+ .await?;
+ room_updated(&room, &session.peer);
+
Ok(())
}
@@ -5786,6 +5786,7 @@ async fn test_following(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
+ cx_c: &mut TestAppContext,
) {
deterministic.forbid_parking();
cx_a.update(editor::init);
@@ -5794,9 +5795,13 @@ async fn test_following(
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
+ let client_c = server.create_client(cx_c, "user_c").await;
server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
+ server
+ .make_contacts(&mut [(&client_a, cx_a), (&client_c, cx_c)])
+ .await;
let active_call_a = cx_a.read(ActiveCall::global);
let active_call_b = cx_b.read(ActiveCall::global);
@@ -5827,8 +5832,10 @@ async fn test_following(
.await
.unwrap();
- // Client A opens some editors.
let workspace_a = client_a.build_workspace(&project_a, cx_a);
+ let workspace_b = client_b.build_workspace(&project_b, cx_b);
+
+ // Client A opens some editors.
let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
let editor_a1 = workspace_a
.update(cx_a, |workspace, cx| {
@@ -5848,7 +5855,6 @@ async fn test_following(
.unwrap();
// Client B opens an editor.
- let workspace_b = client_b.build_workspace(&project_b, cx_b);
let editor_b1 = workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
@@ -5858,29 +5864,97 @@ async fn test_following(
.downcast::<Editor>()
.unwrap();
- let client_a_id = project_b.read_with(cx_b, |project, _| {
- project.collaborators().values().next().unwrap().peer_id
- });
- let client_b_id = project_a.read_with(cx_a, |project, _| {
- project.collaborators().values().next().unwrap().peer_id
- });
+ let peer_id_a = client_a.peer_id().unwrap();
+ let peer_id_b = client_b.peer_id().unwrap();
+ let peer_id_c = client_c.peer_id().unwrap();
- // When client B starts following client A, all visible view states are replicated to client B.
+ // Client A updates their selections in those editors
editor_a1.update(cx_a, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([0..1]))
});
editor_a2.update(cx_a, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([2..3]))
});
+
+ // When client B starts following client A, all visible view states are replicated to client B.
workspace_b
.update(cx_b, |workspace, cx| {
workspace
- .toggle_follow(&ToggleFollow(client_a_id), cx)
+ .toggle_follow(&ToggleFollow(peer_id_a), cx)
+ .unwrap()
+ })
+ .await
+ .unwrap();
+
+ // Client A invites client C to the call.
+ active_call_a
+ .update(cx_a, |call, cx| {
+ call.invite(client_c.current_user_id(cx_c).to_proto(), None, cx)
+ })
+ .await
+ .unwrap();
+ cx_c.foreground().run_until_parked();
+ let active_call_c = cx_c.read(ActiveCall::global);
+ active_call_c
+ .update(cx_c, |call, cx| call.accept_incoming(cx))
+ .await
+ .unwrap();
+ let project_c = client_c.build_remote_project(project_id, cx_c).await;
+ let workspace_c = client_c.build_workspace(&project_c, cx_c);
+ active_call_c
+ .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
+ .await
+ .unwrap();
+
+ // Client C also follows client A.
+ workspace_c
+ .update(cx_c, |workspace, cx| {
+ workspace
+ .toggle_follow(&ToggleFollow(peer_id_a), cx)
.unwrap()
})
.await
.unwrap();
+ // All clients see that clients B and C are following client A.
+ cx_c.foreground().run_until_parked();
+ for (name, active_call, cx) in [
+ ("A", &active_call_a, &cx_a),
+ ("B", &active_call_b, &cx_b),
+ ("C", &active_call_c, &cx_c),
+ ] {
+ active_call.read_with(*cx, |call, cx| {
+ let room = call.room().unwrap().read(cx);
+ assert_eq!(
+ room.followers_for(peer_id_a),
+ &[peer_id_b, peer_id_c],
+ "checking followers for A as {name}"
+ );
+ });
+ }
+
+ // Client C unfollows client A.
+ workspace_c.update(cx_c, |workspace, cx| {
+ workspace.toggle_follow(&ToggleFollow(peer_id_a), cx);
+ });
+
+ // All clients see that clients B is following client A.
+ cx_c.foreground().run_until_parked();
+ for (name, active_call, cx) in [
+ ("A", &active_call_a, &cx_a),
+ ("B", &active_call_b, &cx_b),
+ ("C", &active_call_c, &cx_c),
+ ] {
+ active_call.read_with(*cx, |call, cx| {
+ let room = call.room().unwrap().read(cx);
+ assert_eq!(
+ room.followers_for(peer_id_a),
+ &[peer_id_b],
+ "checking followers for A as {name}"
+ );
+ });
+ }
+
let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
workspace
.active_item(cx)
@@ -6033,14 +6107,14 @@ async fn test_following(
workspace_a
.update(cx_a, |workspace, cx| {
workspace
- .toggle_follow(&ToggleFollow(client_b_id), cx)
+ .toggle_follow(&ToggleFollow(peer_id_b), cx)
.unwrap()
})
.await
.unwrap();
assert_eq!(
workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
- Some(client_b_id)
+ Some(peer_id_b)
);
assert_eq!(
workspace_a.read_with(cx_a, |workspace, cx| workspace
@@ -1,5 +1,9 @@
-use crate::{contact_notification::ContactNotification, contacts_popover, ToggleScreenSharing};
-use call::{ActiveCall, ParticipantLocation};
+use crate::{
+ collaborator_list_popover, collaborator_list_popover::CollaboratorListPopover,
+ contact_notification::ContactNotification, contacts_popover, face_pile::FacePile,
+ ToggleScreenSharing,
+};
+use call::{ActiveCall, ParticipantLocation, Room};
use client::{proto::PeerId, Authenticate, ContactEventKind, User, UserStore};
use clock::ReplicaId;
use contacts_popover::ContactsPopover;
@@ -8,26 +12,52 @@ use gpui::{
color::Color,
elements::*,
geometry::{rect::RectF, vector::vec2f, PathBuilder},
+ impl_internal_actions,
json::{self, ToJson},
- Border, CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext,
+ CursorStyle, Entity, ImageData, ModelHandle, MouseButton, MutableAppContext, RenderContext,
Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
};
use settings::Settings;
-use std::ops::Range;
-use theme::Theme;
+use std::{ops::Range, sync::Arc};
+use theme::{AvatarStyle, Theme};
+use util::ResultExt;
use workspace::{FollowNextCollaborator, JoinProject, ToggleFollow, Workspace};
-actions!(collab, [ToggleCollaborationMenu, ShareProject]);
+actions!(
+ collab,
+ [
+ ToggleCollaboratorList,
+ ToggleContactsMenu,
+ ShareProject,
+ UnshareProject
+ ]
+);
+
+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);
}
pub struct CollabTitlebarItem {
workspace: WeakViewHandle<Workspace>,
user_store: ModelHandle<UserStore>,
contacts_popover: Option<ViewHandle<ContactsPopover>>,
+ contacts_popover_side: ContactsPopoverSide,
+ collaborator_list_popover: Option<ViewHandle<CollaboratorListPopover>>,
_subscriptions: Vec<Subscription>,
}
@@ -47,27 +77,71 @@ impl View for CollabTitlebarItem {
return Empty::new().boxed();
};
+ let project = workspace.read(cx).project().read(cx);
+ let mut project_title = String::new();
+ for (i, name) in project.worktree_root_names(cx).enumerate() {
+ if i > 0 {
+ project_title.push_str(", ");
+ }
+ project_title.push_str(name);
+ }
+ if project_title.is_empty() {
+ project_title = "empty project".to_owned();
+ }
+
let theme = cx.global::<Settings>().theme.clone();
+ let user = workspace.read(cx).user_store().read(cx).current_user();
- let mut container = Flex::row();
+ let mut left_container = Flex::row();
- container.add_children(self.render_toggle_screen_sharing_button(&theme, cx));
+ left_container.add_child(
+ Label::new(project_title, theme.workspace.titlebar.title.clone())
+ .contained()
+ .with_margin_right(theme.workspace.titlebar.item_spacing)
+ .aligned()
+ .left()
+ .boxed(),
+ );
- if workspace.read(cx).client().status().borrow().is_connected() {
- let project = workspace.read(cx).project().read(cx);
- if project.is_shared()
- || project.is_remote()
- || ActiveCall::global(cx).read(cx).room().is_none()
- {
- container.add_child(self.render_toggle_contacts_button(&theme, cx));
- } else {
- container.add_child(self.render_share_button(&theme, cx));
- }
+ 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));
}
- container.add_children(self.render_collaborators(&workspace, &theme, cx));
- container.add_children(self.render_current_user(&workspace, &theme, cx));
- container.add_children(self.render_connection_status(&workspace, cx));
- container.boxed()
+
+ 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
+ .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_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(),
+ );
+ } else {
+ right_container.add_child(Self::render_authenticate(&theme, cx));
+ }
+
+ Stack::new()
+ .with_child(left_container.boxed())
+ .with_child(right_container.aligned().right().boxed())
+ .boxed()
}
}
@@ -80,7 +154,7 @@ impl CollabTitlebarItem {
let active_call = ActiveCall::global(cx);
let mut subscriptions = Vec::new();
subscriptions.push(cx.observe(workspace, |_, _, cx| cx.notify()));
- subscriptions.push(cx.observe(&active_call, |_, _, cx| cx.notify()));
+ subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
subscriptions.push(cx.observe_window_activation(|this, active, cx| {
this.window_activation_changed(active, cx)
}));
@@ -112,6 +186,8 @@ impl CollabTitlebarItem {
workspace: workspace.downgrade(),
user_store: user_store.clone(),
contacts_popover: None,
+ contacts_popover_side: ContactsPopoverSide::Right,
+ collaborator_list_popover: None,
_subscriptions: subscriptions,
}
}
@@ -129,6 +205,13 @@ impl CollabTitlebarItem {
}
}
+ fn active_call_changed(&mut self, cx: &mut ViewContext<Self>) {
+ if ActiveCall::global(cx).read(cx).room().is_none() {
+ self.contacts_popover = None;
+ }
+ cx.notify();
+ }
+
fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext<Self>) {
if let Some(workspace) = self.workspace.upgrade(cx) {
let active_call = ActiveCall::global(cx);
@@ -139,41 +222,88 @@ impl CollabTitlebarItem {
}
}
- pub fn toggle_contacts_popover(
+ fn unshare_project(&mut self, _: &UnshareProject, cx: &mut ViewContext<Self>) {
+ if let Some(workspace) = self.workspace.upgrade(cx) {
+ let active_call = ActiveCall::global(cx);
+ let project = workspace.read(cx).project().clone();
+ active_call
+ .update(cx, |call, cx| call.unshare_project(project, cx))
+ .log_err();
+ }
+ }
+
+ pub fn toggle_collaborator_list_popover(
&mut self,
- _: &ToggleCollaborationMenu,
+ _: &ToggleCollaboratorList,
cx: &mut ViewContext<Self>,
) {
- match self.contacts_popover.take() {
+ match self.collaborator_list_popover.take() {
Some(_) => {}
None => {
if let Some(workspace) = self.workspace.upgrade(cx) {
- let project = workspace.read(cx).project().clone();
let user_store = workspace.read(cx).user_store().clone();
- let view = cx.add_view(|cx| ContactsPopover::new(project, user_store, cx));
+ let view = cx.add_view(|cx| CollaboratorListPopover::new(user_store, cx));
+
cx.subscribe(&view, |this, _, event, cx| {
match event {
- contacts_popover::Event::Dismissed => {
- this.contacts_popover = None;
+ collaborator_list_popover::Event::Dismissed => {
+ this.collaborator_list_popover = None;
}
}
cx.notify();
})
.detach();
- self.contacts_popover = Some(view);
+
+ self.collaborator_list_popover = Some(view);
}
}
}
cx.notify();
}
+ pub fn toggle_contacts_popover(&mut self, _: &ToggleContactsMenu, cx: &mut ViewContext<Self>) {
+ if self.contacts_popover.take().is_none() {
+ if let Some(workspace) = self.workspace.upgrade(cx) {
+ let project = workspace.read(cx).project().clone();
+ let user_store = workspace.read(cx).user_store().clone();
+ let view = cx.add_view(|cx| ContactsPopover::new(project, user_store, cx));
+ cx.subscribe(&view, |this, _, event, cx| {
+ match event {
+ contacts_popover::Event::Dismissed => {
+ this.contacts_popover = None;
+ }
+ }
+
+ 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);
+ }
+ }
+
+ cx.notify();
+ }
+
+ fn leave_call(&mut self, _: &LeaveCall, cx: &mut ViewContext<Self>) {
+ ActiveCall::global(cx)
+ .update(cx, |call, cx| call.hang_up(cx))
+ .log_err();
+ }
+
fn render_toggle_contacts_button(
&self,
theme: &Theme,
cx: &mut RenderContext<Self>,
) -> ElementBox {
let titlebar = &theme.workspace.titlebar;
+
let badge = if self
.user_store
.read(cx)
@@ -194,12 +324,15 @@ impl CollabTitlebarItem {
.boxed(),
)
};
+
Stack::new()
.with_child(
- MouseEventHandler::<ToggleCollaborationMenu>::new(0, cx, |state, _| {
- let style = titlebar
- .toggle_contacts_button
- .style_for(state, self.contacts_popover.is_some());
+ MouseEventHandler::<ToggleContactsMenu>::new(0, cx, |state, _| {
+ let style = titlebar.toggle_contacts_button.style_for(
+ state,
+ self.contacts_popover.is_some()
+ && self.contacts_popover_side == ContactsPopoverSide::Left,
+ );
Svg::new("icons/plus_8.svg")
.with_color(style.color)
.constrained()
@@ -214,39 +347,28 @@ impl CollabTitlebarItem {
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
- cx.dispatch_action(ToggleCollaborationMenu);
+ cx.dispatch_action(ToggleContactsMenu);
})
.aligned()
.boxed(),
)
.with_children(badge)
- .with_children(self.contacts_popover.as_ref().map(|popover| {
- Overlay::new(
- ChildView::new(popover, cx)
- .contained()
- .with_margin_top(titlebar.height)
- .with_margin_left(titlebar.toggle_contacts_button.default.button_width)
- .with_margin_right(-titlebar.toggle_contacts_button.default.button_width)
- .boxed(),
- )
- .with_fit_mode(OverlayFitMode::SwitchAnchor)
- .with_anchor_corner(AnchorCorner::BottomLeft)
- .with_z_index(999)
- .boxed()
- }))
+ .with_children(self.render_contacts_popover_host(
+ ContactsPopoverSide::Left,
+ titlebar,
+ cx,
+ ))
.boxed()
}
fn render_toggle_screen_sharing_button(
&self,
theme: &Theme,
+ room: &ModelHandle<Room>,
cx: &mut RenderContext<Self>,
- ) -> Option<ElementBox> {
- let active_call = ActiveCall::global(cx);
- let room = active_call.read(cx).room().cloned()?;
+ ) -> ElementBox {
let icon;
let tooltip;
-
if room.read(cx).is_screen_sharing() {
icon = "icons/disable_screen_sharing_12.svg";
tooltip = "Stop Sharing Screen"
@@ -256,203 +378,409 @@ impl CollabTitlebarItem {
}
let titlebar = &theme.workspace.titlebar;
- Some(
- MouseEventHandler::<ToggleScreenSharing>::new(0, cx, |state, _| {
- let style = titlebar.call_control.style_for(state, false);
- 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)
- .boxed()
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, cx| {
- cx.dispatch_action(ToggleScreenSharing);
- })
- .with_tooltip::<ToggleScreenSharing, _>(
- 0,
- tooltip.into(),
- Some(Box::new(ToggleScreenSharing)),
- theme.tooltip.clone(),
- cx,
- )
- .aligned()
- .boxed(),
+ MouseEventHandler::<ToggleScreenSharing>::new(0, cx, |state, _| {
+ let style = titlebar.call_control.style_for(state, false);
+ 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)
+ .boxed()
+ })
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_click(MouseButton::Left, move |_, cx| {
+ cx.dispatch_action(ToggleScreenSharing);
+ })
+ .with_tooltip::<ToggleScreenSharing, _>(
+ 0,
+ tooltip.into(),
+ Some(Box::new(ToggleScreenSharing)),
+ theme.tooltip.clone(),
+ cx,
)
+ .aligned()
+ .boxed()
}
- fn render_share_button(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
- enum Share {}
-
+ fn render_leave_call_button(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
let titlebar = &theme.workspace.titlebar;
- MouseEventHandler::<Share>::new(0, cx, |state, _| {
- let style = titlebar.share_button.style_for(state, false);
- Label::new("Share", style.text.clone())
+
+ MouseEventHandler::<LeaveCall>::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, |_, cx| cx.dispatch_action(ShareProject))
- .with_tooltip::<Share, _>(
+ .on_click(MouseButton::Left, move |_, cx| {
+ cx.dispatch_action(LeaveCall);
+ })
+ .with_tooltip::<LeaveCall, _>(
0,
- "Share project with call participants".into(),
- None,
+ "Leave call".to_owned(),
+ Some(Box::new(LeaveCall)),
theme.tooltip.clone(),
cx,
)
- .aligned()
.contained()
- .with_margin_left(theme.workspace.titlebar.avatar_margin)
+ .with_margin_left(theme.workspace.titlebar.item_spacing)
+ .aligned()
.boxed()
}
+ fn render_in_call_share_unshare_button(
+ &self,
+ workspace: &ViewHandle<Workspace>,
+ theme: &Theme,
+ cx: &mut RenderContext<Self>,
+ ) -> Option<ElementBox> {
+ 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 { "Unshare" } else { "Share" };
+ let tooltip = if is_shared {
+ "Unshare project from call participants"
+ } else {
+ "Share project with call participants"
+ };
+
+ let titlebar = &theme.workspace.titlebar;
+
+ enum ShareUnshare {}
+ Some(
+ Stack::new()
+ .with_child(
+ MouseEventHandler::<ShareUnshare>::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(label, style.text.clone())
+ .contained()
+ .with_style(style.container)
+ .boxed()
+ })
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_click(MouseButton::Left, move |_, cx| {
+ if is_shared {
+ cx.dispatch_action(UnshareProject);
+ } else {
+ cx.dispatch_action(ShareProject);
+ }
+ })
+ .with_tooltip::<ShareUnshare, _>(
+ 0,
+ tooltip.to_owned(),
+ None,
+ theme.tooltip.clone(),
+ cx,
+ )
+ .boxed(),
+ )
+ .with_children(self.render_contacts_popover_host(
+ ContactsPopoverSide::Right,
+ titlebar,
+ cx,
+ ))
+ .aligned()
+ .contained()
+ .with_margin_left(theme.workspace.titlebar.item_spacing)
+ .boxed(),
+ )
+ }
+
+ fn render_outside_call_share_button(
+ &self,
+ theme: &Theme,
+ cx: &mut RenderContext<Self>,
+ ) -> ElementBox {
+ let tooltip = "Share project with new call";
+ let titlebar = &theme.workspace.titlebar;
+
+ enum OutsideCallShare {}
+ Stack::new()
+ .with_child(
+ MouseEventHandler::<OutsideCallShare>::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())
+ .contained()
+ .with_style(style.container)
+ .boxed()
+ })
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_click(MouseButton::Left, move |_, cx| {
+ cx.dispatch_action(ToggleContactsMenu);
+ })
+ .with_tooltip::<OutsideCallShare, _>(
+ 0,
+ tooltip.to_owned(),
+ None,
+ theme.tooltip.clone(),
+ cx,
+ )
+ .boxed(),
+ )
+ .with_children(self.render_contacts_popover_host(
+ ContactsPopoverSide::Right,
+ titlebar,
+ cx,
+ ))
+ .aligned()
+ .contained()
+ .with_margin_left(theme.workspace.titlebar.item_spacing)
+ .boxed()
+ }
+
+ fn render_contacts_popover_host<'a>(
+ &'a self,
+ side: ContactsPopoverSide,
+ theme: &'a theme::Titlebar,
+ cx: &'a RenderContext<Self>,
+ ) -> impl Iterator<Item = ElementBox> + '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()
+ })
+ }
+
fn render_collaborators(
&self,
workspace: &ViewHandle<Workspace>,
theme: &Theme,
+ room: ModelHandle<Room>,
cx: &mut RenderContext<Self>,
) -> Vec<ElementBox> {
- let active_call = ActiveCall::global(cx);
- if let Some(room) = active_call.read(cx).room().cloned() {
- let project = workspace.read(cx).project().read(cx);
- let mut participants = room
- .read(cx)
- .remote_participants()
- .values()
- .cloned()
- .collect::<Vec<_>>();
- participants.sort_by_key(|p| Some(project.collaborators().get(&p.peer_id)?.replica_id));
- 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(self.render_avatar(
+ let project = workspace.read(cx).project().read(cx);
+
+ let mut participants = room
+ .read(cx)
+ .remote_participants()
+ .values()
+ .cloned()
+ .collect::<Vec<_>>();
+ participants.sort_by_key(|p| Some(project.collaborators().get(&p.peer_id)?.replica_id));
+
+ 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,
- Some((
- participant.peer_id,
- &user.github_login,
- participant.location,
- )),
+ participant.peer_id,
+ Some(participant.location),
workspace,
theme,
cx,
))
- })
- .collect()
- } else {
- Default::default()
- }
+ .with_margin_left(theme.workspace.titlebar.face_pile_spacing)
+ .boxed(),
+ )
+ })
+ .collect()
}
fn render_current_user(
&self,
workspace: &ViewHandle<Workspace>,
theme: &Theme,
+ user: &Option<Arc<User>>,
cx: &mut RenderContext<Self>,
- ) -> Option<ElementBox> {
- let user = workspace.read(cx).user_store().read(cx).current_user();
+ ) -> ElementBox {
+ let user = user.as_ref().expect("Active call without user");
let replica_id = workspace.read(cx).project().read(cx).replica_id();
- let status = *workspace.read(cx).client().status().borrow();
- if let Some(user) = user {
- Some(self.render_avatar(&user, Some(replica_id), None, workspace, theme, cx))
- } else if matches!(status, client::Status::UpgradeRequired) {
- None
- } else {
- Some(
- MouseEventHandler::<Authenticate>::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)
- .boxed()
- })
- .on_click(MouseButton::Left, |_, cx| cx.dispatch_action(Authenticate))
- .with_cursor_style(CursorStyle::PointingHand)
- .aligned()
- .boxed(),
- )
- }
+ 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_avatar(
+ fn render_authenticate(theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
+ MouseEventHandler::<Authenticate>::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()
+ .boxed()
+ }
+
+ fn render_face_pile(
&self,
user: &User,
replica_id: Option<ReplicaId>,
- peer: Option<(PeerId, &str, ParticipantLocation)>,
+ peer_id: PeerId,
+ location: Option<ParticipantLocation>,
workspace: &ViewHandle<Workspace>,
theme: &Theme,
cx: &mut RenderContext<Self>,
) -> ElementBox {
- let is_followed = peer.map_or(false, |(peer_id, _, _)| {
- workspace.read(cx).is_following(peer_id)
- });
+ let room = ActiveCall::global(cx).read(cx).room();
+ let is_being_followed = workspace.read(cx).is_being_followed(peer_id);
+ let followed_by_self = room
+ .map(|room| {
+ is_being_followed
+ && room
+ .read(cx)
+ .followers_for(peer_id)
+ .iter()
+ .any(|&follower| Some(follower) == workspace.read(cx).client().peer_id())
+ })
+ .unwrap_or(false);
- let mut avatar_style;
- if let Some((_, _, location)) = peer.as_ref() {
- if let ParticipantLocation::SharedProject { project_id } = *location {
+ let avatar_style;
+ if let Some(location) = location {
+ if let ParticipantLocation::SharedProject { project_id } = location {
if Some(project_id) == workspace.read(cx).project().read(cx).remote_id() {
- avatar_style = theme.workspace.titlebar.avatar;
+ avatar_style = &theme.workspace.titlebar.avatar;
} else {
- avatar_style = theme.workspace.titlebar.inactive_avatar;
+ avatar_style = &theme.workspace.titlebar.inactive_avatar;
}
} else {
- avatar_style = theme.workspace.titlebar.inactive_avatar;
+ avatar_style = &theme.workspace.titlebar.inactive_avatar;
}
} else {
- avatar_style = theme.workspace.titlebar.avatar;
+ avatar_style = &theme.workspace.titlebar.avatar;
}
- let mut replica_color = None;
+ let mut background_color = theme
+ .workspace
+ .titlebar
+ .container
+ .background_color
+ .unwrap_or_default();
if let Some(replica_id) = replica_id {
- let color = theme.editor.replica_selection_style(replica_id).cursor;
- replica_color = Some(color);
- if is_followed {
- avatar_style.border = Border::all(1.0, color);
+ if followed_by_self {
+ let selection = theme.editor.replica_selection_style(replica_id).selection;
+ background_color = Color::blend(selection, background_color);
+ background_color.a = 255;
}
}
let content = Stack::new()
.with_children(user.avatar.as_ref().map(|avatar| {
- Image::new(avatar.clone())
- .with_style(avatar_style)
- .constrained()
- .with_width(theme.workspace.titlebar.avatar_width)
- .aligned()
- .boxed()
- }))
- .with_children(replica_color.map(|replica_color| {
- AvatarRibbon::new(replica_color)
- .constrained()
- .with_width(theme.workspace.titlebar.avatar_ribbon.width)
- .with_height(theme.workspace.titlebar.avatar_ribbon.height)
- .aligned()
- .bottom()
- .boxed()
+ let face_pile = FacePile::new(theme.workspace.titlebar.follower_avatar_overlap)
+ .with_child(Self::render_face(
+ avatar.clone(),
+ avatar_style.clone(),
+ background_color,
+ ))
+ .with_children(
+ (|| {
+ let room = room?.read(cx);
+ let followers = room.followers_for(peer_id);
+
+ Some(followers.into_iter().flat_map(|&follower| {
+ let avatar = room
+ .remote_participant_for_peer_id(follower)
+ .and_then(|participant| participant.user.avatar.clone())
+ .or_else(|| {
+ if follower == workspace.read(cx).client().peer_id()? {
+ workspace
+ .read(cx)
+ .user_store()
+ .read(cx)
+ .current_user()?
+ .avatar
+ .clone()
+ } else {
+ None
+ }
+ })?;
+
+ Some(Self::render_face(
+ avatar.clone(),
+ theme.workspace.titlebar.follower_avatar.clone(),
+ background_color,
+ ))
+ }))
+ })()
+ .into_iter()
+ .flatten(),
+ );
+
+ let mut container = face_pile
+ .contained()
+ .with_style(theme.workspace.titlebar.leader_selection);
+
+ if let Some(replica_id) = replica_id {
+ if followed_by_self {
+ let color = theme.editor.replica_selection_style(replica_id).selection;
+ container = container.with_background_color(color);
+ }
+ }
+
+ container.boxed()
}))
- .constrained()
- .with_width(theme.workspace.titlebar.avatar_width)
- .contained()
- .with_margin_left(theme.workspace.titlebar.avatar_margin)
+ .with_children((|| {
+ let replica_id = replica_id?;
+ let color = theme.editor.replica_selection_style(replica_id).cursor;
+ Some(
+ AvatarRibbon::new(color)
+ .constrained()
+ .with_width(theme.workspace.titlebar.avatar_ribbon.width)
+ .with_height(theme.workspace.titlebar.avatar_ribbon.height)
+ .aligned()
+ .bottom()
+ .boxed(),
+ )
+ })())
.boxed();
- if let Some((peer_id, peer_github_login, location)) = peer {
+ if let Some(location) = location {
if let Some(replica_id) = replica_id {
MouseEventHandler::<ToggleFollow>::new(replica_id.into(), cx, move |_, _| content)
.with_cursor_style(CursorStyle::PointingHand)
@@ -461,10 +789,10 @@ impl CollabTitlebarItem {
})
.with_tooltip::<ToggleFollow, _>(
peer_id.as_u64() as usize,
- if is_followed {
- format!("Unfollow {}", peer_github_login)
+ if is_being_followed {
+ format!("Unfollow {}", user.github_login)
} else {
- format!("Follow {}", peer_github_login)
+ format!("Follow {}", user.github_login)
},
Some(Box::new(FollowNextCollaborator)),
theme.tooltip.clone(),
@@ -485,7 +813,7 @@ impl CollabTitlebarItem {
})
.with_tooltip::<JoinProject, _>(
peer_id.as_u64() as usize,
- format!("Follow {} into external project", peer_github_login),
+ format!("Follow {} into external project", user.github_login),
Some(Box::new(FollowNextCollaborator)),
theme.tooltip.clone(),
cx,
@@ -499,6 +827,24 @@ impl CollabTitlebarItem {
}
}
+ fn render_face(
+ avatar: Arc<ImageData>,
+ avatar_style: AvatarStyle,
+ background_color: Color,
+ ) -> ElementBox {
+ Image::new(avatar)
+ .with_style(avatar_style.image)
+ .aligned()
+ .contained()
+ .with_background_color(background_color)
+ .with_corner_radius(avatar_style.outer_corner_radius)
+ .constrained()
+ .with_width(avatar_style.outer_width)
+ .with_height(avatar_style.outer_width)
+ .aligned()
+ .boxed()
+ }
+
fn render_connection_status(
&self,
workspace: &ViewHandle<Workspace>,
@@ -1,8 +1,10 @@
mod collab_titlebar_item;
+mod collaborator_list_popover;
mod contact_finder;
mod contact_list;
mod contact_notification;
mod contacts_popover;
+mod face_pile;
mod incoming_call_notification;
mod notifications;
mod project_shared_notification;
@@ -10,7 +12,7 @@ mod sharing_status_indicator;
use anyhow::anyhow;
use call::ActiveCall;
-pub use collab_titlebar_item::{CollabTitlebarItem, ToggleCollaborationMenu};
+pub use collab_titlebar_item::{CollabTitlebarItem, ToggleContactsMenu};
use gpui::{actions, MutableAppContext, Task};
use std::sync::Arc;
use workspace::{AppState, JoinProject, ToggleFollow, Workspace};
@@ -116,7 +118,7 @@ fn join_project(action: &JoinProject, app_state: Arc<AppState>, cx: &mut Mutable
});
if let Some(follow_peer_id) = follow_peer_id {
- if !workspace.is_following(follow_peer_id) {
+ if !workspace.is_being_followed(follow_peer_id) {
workspace
.toggle_follow(&ToggleFollow(follow_peer_id), cx)
.map(|follow| follow.detach_and_log_err(cx));
@@ -0,0 +1,165 @@
+use call::ActiveCall;
+use client::UserStore;
+use gpui::Action;
+use gpui::{
+ actions, elements::*, Entity, ModelHandle, MouseButton, RenderContext, View, ViewContext,
+};
+use settings::Settings;
+
+use crate::collab_titlebar_item::ToggleCollaboratorList;
+
+pub(crate) enum Event {
+ Dismissed,
+}
+
+enum Collaborator {
+ SelfUser { username: String },
+ RemoteUser { username: String },
+}
+
+actions!(collaborator_list_popover, [NoOp]);
+
+pub(crate) struct CollaboratorListPopover {
+ list_state: ListState,
+}
+
+impl Entity for CollaboratorListPopover {
+ type Event = Event;
+}
+
+impl View for CollaboratorListPopover {
+ fn ui_name() -> &'static str {
+ "CollaboratorListPopover"
+ }
+
+ fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
+ let theme = cx.global::<Settings>().theme.clone();
+
+ MouseEventHandler::<Self>::new(0, cx, |_, _| {
+ List::new(self.list_state.clone())
+ .contained()
+ .with_style(theme.contacts_popover.container) //TODO: Change the name of this theme key
+ .constrained()
+ .with_width(theme.contacts_popover.width)
+ .with_height(theme.contacts_popover.height)
+ .boxed()
+ })
+ .on_down_out(MouseButton::Left, move |_, cx| {
+ cx.dispatch_action(ToggleCollaboratorList);
+ })
+ .boxed()
+ }
+
+ fn focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
+ cx.emit(Event::Dismissed);
+ }
+}
+
+impl CollaboratorListPopover {
+ pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
+ let active_call = ActiveCall::global(cx);
+
+ let mut collaborators = user_store
+ .read(cx)
+ .current_user()
+ .map(|u| Collaborator::SelfUser {
+ username: u.github_login.clone(),
+ })
+ .into_iter()
+ .collect::<Vec<_>>();
+
+ //TODO: What should the canonical sort here look like, consult contacts list implementation
+ if let Some(room) = active_call.read(cx).room() {
+ for participant in room.read(cx).remote_participants() {
+ collaborators.push(Collaborator::RemoteUser {
+ username: participant.1.user.github_login.clone(),
+ });
+ }
+ }
+
+ Self {
+ list_state: ListState::new(
+ collaborators.len(),
+ Orientation::Top,
+ 0.,
+ cx,
+ move |_, index, cx| match &collaborators[index] {
+ Collaborator::SelfUser { username } => render_collaborator_list_entry(
+ index,
+ username,
+ None::<NoOp>,
+ None,
+ Svg::new("icons/chevron_right_12.svg"),
+ NoOp,
+ "Leave call".to_owned(),
+ cx,
+ ),
+
+ Collaborator::RemoteUser { username } => render_collaborator_list_entry(
+ index,
+ username,
+ Some(NoOp),
+ Some(format!("Follow {username}")),
+ Svg::new("icons/x_mark_12.svg"),
+ NoOp,
+ format!("Remove {username} from call"),
+ cx,
+ ),
+ },
+ ),
+ }
+ }
+}
+
+fn render_collaborator_list_entry<UA: Action + Clone, IA: Action + Clone>(
+ index: usize,
+ username: &str,
+ username_action: Option<UA>,
+ username_tooltip: Option<String>,
+ icon: Svg,
+ icon_action: IA,
+ icon_tooltip: String,
+ cx: &mut RenderContext<CollaboratorListPopover>,
+) -> ElementBox {
+ enum Username {}
+ enum UsernameTooltip {}
+ enum Icon {}
+ enum IconTooltip {}
+
+ let theme = &cx.global::<Settings>().theme;
+ let username_theme = theme.contact_list.contact_username.text.clone();
+ let tooltip_theme = theme.tooltip.clone();
+
+ let username = MouseEventHandler::<Username>::new(index, cx, |_, _| {
+ Label::new(username.to_owned(), username_theme.clone()).boxed()
+ })
+ .on_click(MouseButton::Left, move |_, cx| {
+ if let Some(username_action) = username_action.clone() {
+ cx.dispatch_action(username_action);
+ }
+ });
+
+ Flex::row()
+ .with_child(if let Some(username_tooltip) = username_tooltip {
+ username
+ .with_tooltip::<UsernameTooltip, _>(
+ index,
+ username_tooltip,
+ None,
+ tooltip_theme.clone(),
+ cx,
+ )
+ .boxed()
+ } else {
+ username.boxed()
+ })
+ .with_child(
+ MouseEventHandler::<Icon>::new(index, cx, |_, _| icon.boxed())
+ .on_click(MouseButton::Left, move |_, cx| {
+ cx.dispatch_action(icon_action.clone())
+ })
+ .with_tooltip::<IconTooltip, _>(index, icon_tooltip, None, tooltip_theme, cx)
+ .boxed(),
+ )
+ .boxed()
+}
@@ -1,3 +1,4 @@
+use super::collab_titlebar_item::LeaveCall;
use crate::contacts_popover;
use call::ActiveCall;
use client::{proto::PeerId, Contact, User, UserStore};
@@ -18,22 +19,20 @@ use serde::Deserialize;
use settings::Settings;
use std::{mem, sync::Arc};
use theme::IconButton;
-use util::ResultExt;
use workspace::{JoinProject, OpenSharedScreen};
impl_actions!(contact_list, [RemoveContact, RespondToContactRequest]);
-impl_internal_actions!(contact_list, [ToggleExpanded, Call, LeaveCall]);
+impl_internal_actions!(contact_list, [ToggleExpanded, Call]);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ContactList::remove_contact);
cx.add_action(ContactList::respond_to_contact_request);
- cx.add_action(ContactList::clear_filter);
+ cx.add_action(ContactList::cancel);
cx.add_action(ContactList::select_next);
cx.add_action(ContactList::select_prev);
cx.add_action(ContactList::confirm);
cx.add_action(ContactList::toggle_expanded);
cx.add_action(ContactList::call);
- cx.add_action(ContactList::leave_call);
}
#[derive(Clone, PartialEq)]
@@ -45,9 +44,6 @@ struct Call {
initial_project: Option<ModelHandle<Project>>,
}
-#[derive(Copy, Clone, PartialEq)]
-struct LeaveCall;
-
#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
enum Section {
ActiveCall,
@@ -326,7 +322,7 @@ impl ContactList {
.detach();
}
- fn clear_filter(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
+ fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
let did_clear = self.filter_editor.update(cx, |editor, cx| {
if editor.buffer().read(cx).len(cx) > 0 {
editor.set_text("", cx);
@@ -335,6 +331,7 @@ impl ContactList {
false
}
});
+
if !did_clear {
cx.emit(Event::Dismissed);
}
@@ -980,6 +977,7 @@ impl ContactList {
cx: &mut RenderContext<Self>,
) -> ElementBox {
enum Header {}
+ enum LeaveCallContactList {}
let header_style = theme
.header_row
@@ -992,9 +990,9 @@ impl ContactList {
};
let leave_call = if section == Section::ActiveCall {
Some(
- MouseEventHandler::<LeaveCall>::new(0, cx, |state, _| {
+ MouseEventHandler::<LeaveCallContactList>::new(0, cx, |state, _| {
let style = theme.leave_call.style_for(state, false);
- Label::new("Leave Session", style.text.clone())
+ Label::new("Leave Call", style.text.clone())
.contained()
.with_style(style.container)
.boxed()
@@ -1283,12 +1281,6 @@ impl ContactList {
})
.detach_and_log_err(cx);
}
-
- fn leave_call(&mut self, _: &LeaveCall, cx: &mut ViewContext<Self>) {
- ActiveCall::global(cx)
- .update(cx, |call, cx| call.hang_up(cx))
- .log_err();
- }
}
impl Entity for ContactList {
@@ -1334,7 +1326,7 @@ impl View for ContactList {
})
.with_tooltip::<AddContact, _>(
0,
- "Add contact".into(),
+ "Search for new contact".into(),
None,
theme.tooltip.clone(),
cx,
@@ -1,4 +1,4 @@
-use crate::{contact_finder::ContactFinder, contact_list::ContactList, ToggleCollaborationMenu};
+use crate::{contact_finder::ContactFinder, contact_list::ContactList, ToggleContactsMenu};
use client::UserStore;
use gpui::{
actions, elements::*, ClipboardItem, CursorStyle, Entity, ModelHandle, MouseButton,
@@ -155,7 +155,7 @@ impl View for ContactsPopover {
.boxed()
})
.on_down_out(MouseButton::Left, move |_, cx| {
- cx.dispatch_action(ToggleCollaborationMenu);
+ cx.dispatch_action(ToggleContactsMenu);
})
.boxed()
}
@@ -0,0 +1,101 @@
+use std::ops::Range;
+
+use gpui::{
+ geometry::{
+ rect::RectF,
+ vector::{vec2f, Vector2F},
+ },
+ json::ToJson,
+ serde_json::{self, json},
+ Axis, DebugContext, Element, ElementBox, MeasurementContext, PaintContext,
+};
+
+pub(crate) struct FacePile {
+ overlap: f32,
+ faces: Vec<ElementBox>,
+}
+
+impl FacePile {
+ pub fn new(overlap: f32) -> FacePile {
+ FacePile {
+ overlap,
+ faces: Vec::new(),
+ }
+ }
+}
+
+impl Element for FacePile {
+ type LayoutState = ();
+ type PaintState = ();
+
+ fn layout(
+ &mut self,
+ constraint: gpui::SizeConstraint,
+ cx: &mut gpui::LayoutContext,
+ ) -> (Vector2F, Self::LayoutState) {
+ debug_assert!(constraint.max_along(Axis::Horizontal) == f32::INFINITY);
+
+ let mut width = 0.;
+ for face in &mut self.faces {
+ width += face.layout(constraint, cx).x();
+ }
+ width -= self.overlap * self.faces.len().saturating_sub(1) as f32;
+
+ (Vector2F::new(width, constraint.max.y()), ())
+ }
+
+ fn paint(
+ &mut self,
+ bounds: RectF,
+ visible_bounds: RectF,
+ _layout: &mut Self::LayoutState,
+ cx: &mut PaintContext,
+ ) -> 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();
+ cx.paint_layer(None, |cx| {
+ face.paint(vec2f(origin_x, origin_y), visible_bounds, cx);
+ });
+ origin_x += self.overlap;
+ }
+
+ ()
+ }
+
+ fn rect_for_text_range(
+ &self,
+ _: Range<usize>,
+ _: RectF,
+ _: RectF,
+ _: &Self::LayoutState,
+ _: &Self::PaintState,
+ _: &MeasurementContext,
+ ) -> Option<RectF> {
+ None
+ }
+
+ fn debug(
+ &self,
+ bounds: RectF,
+ _: &Self::LayoutState,
+ _: &Self::PaintState,
+ _: &DebugContext,
+ ) -> serde_json::Value {
+ json!({
+ "type": "FacePile",
+ "bounds": bounds.to_json()
+ })
+ }
+}
+
+impl Extend<ElementBox> for FacePile {
+ fn extend<T: IntoIterator<Item = ElementBox>>(&mut self, children: T) {
+ self.faces.extend(children);
+ }
+}
@@ -363,6 +363,7 @@ impl<T: Element> AnyElement for Lifecycle<T> {
value
}
}
+
_ => panic!("invalid element lifecycle state"),
}
}
@@ -308,7 +308,9 @@ impl Element for Flex {
}
}
}
+
child.paint(child_origin, visible_bounds, cx);
+
match self.axis {
Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0),
Axis::Vertical => child_origin += vec2f(0.0, child.size().y()),
@@ -16,7 +16,7 @@ message Envelope {
Error error = 6;
Ping ping = 7;
Test test = 8;
-
+
CreateRoom create_room = 9;
CreateRoomResponse create_room_response = 10;
JoinRoom join_room = 11;
@@ -206,7 +206,8 @@ message Room {
uint64 id = 1;
repeated Participant participants = 2;
repeated PendingParticipant pending_participants = 3;
- string live_kit_room = 4;
+ repeated Follower followers = 4;
+ string live_kit_room = 5;
}
message Participant {
@@ -227,6 +228,11 @@ message ParticipantProject {
repeated string worktree_root_names = 2;
}
+message Follower {
+ PeerId leader_id = 1;
+ PeerId follower_id = 2;
+}
+
message ParticipantLocation {
oneof variant {
SharedProject shared_project = 1;
@@ -6,4 +6,4 @@ pub use conn::Connection;
pub use peer::*;
mod macros;
-pub const PROTOCOL_VERSION: u32 = 46;
+pub const PROTOCOL_VERSION: u32 = 47;
@@ -74,12 +74,15 @@ pub struct Titlebar {
pub container: ContainerStyle,
pub height: f32,
pub title: TextStyle,
- pub avatar_width: f32,
- pub avatar_margin: f32,
+ pub item_spacing: f32,
+ pub face_pile_spacing: f32,
pub avatar_ribbon: AvatarRibbon,
+ pub follower_avatar_overlap: f32,
+ pub leader_selection: ContainerStyle,
pub offline_icon: OfflineIcon,
- pub avatar: ImageStyle,
- pub inactive_avatar: ImageStyle,
+ pub avatar: AvatarStyle,
+ pub inactive_avatar: AvatarStyle,
+ pub follower_avatar: AvatarStyle,
pub sign_in_prompt: Interactive<ContainedText>,
pub outdated_warning: ContainedText,
pub share_button: Interactive<ContainedText>,
@@ -88,6 +91,14 @@ pub struct Titlebar {
pub toggle_contacts_badge: ContainerStyle,
}
+#[derive(Clone, Deserialize, Default)]
+pub struct AvatarStyle {
+ #[serde(flatten)]
+ pub image: ImageStyle,
+ pub outer_width: f32,
+ pub outer_corner_radius: f32,
+}
+
#[derive(Deserialize, Default)]
pub struct ContactsPopover {
#[serde(flatten)]
@@ -381,7 +392,7 @@ pub struct InviteLink {
pub icon: Icon,
}
-#[derive(Deserialize, Default)]
+#[derive(Deserialize, Clone, Copy, Default)]
pub struct Icon {
#[serde(flatten)]
pub container: ContainerStyle,
@@ -837,7 +837,7 @@ impl Workspace {
&self.project
}
- pub fn client(&self) -> &Arc<Client> {
+ pub fn client(&self) -> &Client {
&self.client
}
@@ -1832,24 +1832,15 @@ impl Workspace {
None
}
- pub fn is_following(&self, peer_id: PeerId) -> bool {
+ pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
self.follower_states_by_leader.contains_key(&peer_id)
}
- pub fn is_followed(&self, peer_id: PeerId) -> bool {
+ pub fn is_followed_by(&self, peer_id: PeerId) -> bool {
self.leader_state.followers.contains(&peer_id)
}
fn render_titlebar(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
- let project = &self.project.read(cx);
- let mut worktree_root_names = String::new();
- for (i, name) in project.worktree_root_names(cx).enumerate() {
- if i > 0 {
- worktree_root_names.push_str(", ");
- }
- worktree_root_names.push_str(name);
- }
-
// TODO: There should be a better system in place for this
// (https://github.com/zed-industries/zed/issues/1290)
let is_fullscreen = cx.window_is_fullscreen(cx.window_id());
@@ -1866,16 +1857,10 @@ impl Workspace {
MouseEventHandler::<TitleBar>::new(0, cx, |_, cx| {
Container::new(
Stack::new()
- .with_child(
- Label::new(worktree_root_names, theme.workspace.titlebar.title.clone())
- .aligned()
- .left()
- .boxed(),
- )
.with_children(
self.titlebar_item
.as_ref()
- .map(|item| ChildView::new(item, cx).aligned().right().boxed()),
+ .map(|item| ChildView::new(item, cx).boxed()),
)
.boxed(),
)
@@ -6,7 +6,7 @@ use anyhow::{anyhow, Context, Result};
use assets::Assets;
use breadcrumbs::Breadcrumbs;
pub use client;
-use collab_ui::{CollabTitlebarItem, ToggleCollaborationMenu};
+use collab_ui::{CollabTitlebarItem, ToggleContactsMenu};
use collections::VecDeque;
pub use editor;
use editor::{Editor, MultiBuffer};
@@ -99,9 +99,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
},
);
cx.add_action(
- |workspace: &mut Workspace,
- _: &ToggleCollaborationMenu,
- cx: &mut ViewContext<Workspace>| {
+ |workspace: &mut Workspace, _: &ToggleContactsMenu, cx: &mut ViewContext<Workspace>| {
if let Some(item) = workspace
.titlebar_item()
.and_then(|item| item.downcast::<CollabTitlebarItem>())
@@ -12,7 +12,7 @@ import tabBar from "./tabBar";
export default function workspace(colorScheme: ColorScheme) {
const layer = colorScheme.lowest;
- const titlebarPadding = 6;
+ const itemSpacing = 8;
const titlebarButton = {
cornerRadius: 6,
padding: {
@@ -29,8 +29,21 @@ export default function workspace(colorScheme: ColorScheme) {
background: background(layer, "variant", "hovered"),
border: border(layer, "variant", "hovered"),
},
+ clicked: {
+ ...text(layer, "sans", "variant", "pressed", { size: "xs" }),
+ background: background(layer, "variant", "pressed"),
+ border: border(layer, "variant", "pressed"),
+ },
+ active: {
+ ...text(layer, "sans", "variant", "active", { size: "xs" }),
+ background: background(layer, "variant", "active"),
+ border: border(layer, "variant", "active"),
+ },
};
const avatarWidth = 18;
+ const avatarOuterWidth = avatarWidth + 4;
+ const followerAvatarWidth = 14;
+ const followerAvatarOuterWidth = followerAvatarWidth + 4;
return {
background: background(layer),
@@ -70,14 +83,14 @@ export default function workspace(colorScheme: ColorScheme) {
},
statusBar: statusBar(colorScheme),
titlebar: {
- avatarWidth,
- avatarMargin: 8,
+ itemSpacing,
+ facePileSpacing: 2,
height: 33, // 32px + 1px for overlaid border
background: background(layer),
border: border(layer, { bottom: true, overlay: true }),
padding: {
left: 80,
- right: titlebarPadding,
+ right: itemSpacing,
},
// Project
@@ -85,20 +98,38 @@ export default function workspace(colorScheme: ColorScheme) {
// Collaborators
avatar: {
+ width: avatarWidth,
+ outerWidth: avatarOuterWidth,
cornerRadius: avatarWidth / 2,
- border: {
- color: "#00000088",
- width: 1,
- },
+ outerCornerRadius: avatarOuterWidth / 2,
},
inactiveAvatar: {
+ width: avatarWidth,
+ outerWidth: avatarOuterWidth,
cornerRadius: avatarWidth / 2,
- border: {
- color: "#00000088",
- width: 1,
- },
+ outerCornerRadius: avatarOuterWidth / 2,
grayscale: true,
},
+ followerAvatar: {
+ width: followerAvatarWidth,
+ outerWidth: followerAvatarOuterWidth,
+ cornerRadius: followerAvatarWidth / 2,
+ outerCornerRadius: followerAvatarOuterWidth / 2,
+ },
+ followerAvatarOverlap: 8,
+ leaderSelection: {
+ margin: {
+ top: 4,
+ bottom: 4,
+ },
+ padding: {
+ left: 2,
+ right: 2,
+ top: 4,
+ bottom: 4,
+ },
+ cornerRadius: 6,
+ },
avatarRibbon: {
height: 3,
width: 12,
@@ -108,7 +139,7 @@ export default function workspace(colorScheme: ColorScheme) {
// Sign in buttom
// FlatButton, Variant
signInPrompt: {
- ...titlebarButton
+ ...titlebarButton,
},
// Offline Indicator
@@ -116,7 +147,7 @@ export default function workspace(colorScheme: ColorScheme) {
color: foreground(layer, "variant"),
width: 16,
margin: {
- left: titlebarPadding,
+ left: itemSpacing,
},
padding: {
right: 4,
@@ -129,7 +160,7 @@ export default function workspace(colorScheme: ColorScheme) {
background: withOpacity(background(layer, "warning"), 0.3),
border: border(layer, "warning"),
margin: {
- left: titlebarPadding,
+ left: itemSpacing,
},
padding: {
left: 8,
@@ -148,7 +179,7 @@ export default function workspace(colorScheme: ColorScheme) {
},
},
toggleContactsButton: {
- margin: { left: 6 },
+ margin: { left: itemSpacing },
cornerRadius: 6,
color: foreground(layer, "variant"),
iconWidth: 8,
@@ -157,6 +188,10 @@ export default function workspace(colorScheme: ColorScheme) {
background: background(layer, "variant", "active"),
color: foreground(layer, "variant", "active"),
},
+ clicked: {
+ background: background(layer, "variant", "pressed"),
+ color: foreground(layer, "variant", "pressed"),
+ },
hover: {
background: background(layer, "variant", "hovered"),
color: foreground(layer, "variant", "hovered"),
@@ -170,8 +205,8 @@ export default function workspace(colorScheme: ColorScheme) {
background: foreground(layer, "accent"),
},
shareButton: {
- ...titlebarButton
- }
+ ...titlebarButton,
+ },
},
toolbar: {
@@ -227,9 +262,6 @@ export default function workspace(colorScheme: ColorScheme) {
shadow: colorScheme.modalShadow,
},
},
- dropTargetOverlayColor: withOpacity(
- foreground(layer, "variant"),
- 0.5
- ),
+ dropTargetOverlayColor: withOpacity(foreground(layer, "variant"), 0.5),
};
}