Merge pull request #1700 from zed-industries/room

Introduce call-based collaboration
This commit is contained in:
Antonio Scandurra 2022-10-11 17:40:44 +01:00 committed by GitHub
commit a656047c15
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
74 changed files with 7164 additions and 5222 deletions

94
Cargo.lock generated
View file

@ -684,6 +684,20 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c"
[[package]]
name = "call"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"collections",
"futures",
"gpui",
"postage",
"project",
"util",
]
[[package]]
name = "cap-fs-ext"
version = "0.24.4"
@ -1023,6 +1037,7 @@ dependencies = [
"axum",
"axum-extra",
"base64",
"call",
"clap 3.2.8",
"client",
"collections",
@ -1067,6 +1082,31 @@ dependencies = [
"workspace",
]
[[package]]
name = "collab_ui"
version = "0.1.0"
dependencies = [
"anyhow",
"call",
"client",
"clock",
"collections",
"editor",
"futures",
"fuzzy",
"gpui",
"log",
"menu",
"picker",
"postage",
"project",
"serde",
"settings",
"theme",
"util",
"workspace",
]
[[package]]
name = "collections"
version = "0.1.0"
@ -1108,54 +1148,6 @@ dependencies = [
"cache-padded",
]
[[package]]
name = "contacts_panel"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"collections",
"editor",
"futures",
"fuzzy",
"gpui",
"language",
"log",
"menu",
"picker",
"postage",
"project",
"serde",
"settings",
"theme",
"util",
"workspace",
]
[[package]]
name = "contacts_status_item"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"collections",
"editor",
"futures",
"fuzzy",
"gpui",
"language",
"log",
"menu",
"picker",
"postage",
"project",
"serde",
"settings",
"theme",
"util",
"workspace",
]
[[package]]
name = "context_menu"
version = "0.1.0"
@ -7176,8 +7168,8 @@ name = "workspace"
version = "0.1.0"
dependencies = [
"anyhow",
"call",
"client",
"clock",
"collections",
"context_menu",
"drag_and_drop",
@ -7247,15 +7239,15 @@ dependencies = [
"auto_update",
"backtrace",
"breadcrumbs",
"call",
"chat_panel",
"chrono",
"cli",
"client",
"clock",
"collab_ui",
"collections",
"command_palette",
"contacts_panel",
"contacts_status_item",
"context_menu",
"ctor",
"diagnostics",

View file

@ -1,4 +0,0 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 11C5 14.3137 7.68629 17 11 17C14.3137 17 17 14.3137 17 11C17 7.68629 14.3137 5 11 5C7.68629 5 5 7.68629 5 11ZM11 3C6.58172 3 3 6.58172 3 11C3 15.4183 6.58172 19 11 19C15.4183 19 19 15.4183 19 11C19 6.58172 15.4183 3 11 3Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.09092 8.09088H14.6364L10.5511 12.4545H12.4546L13.9091 13.9091H7.36365L11.7273 9.54543H9.54547L8.09092 8.09088Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 571 B

View file

@ -395,7 +395,6 @@
"context": "Workspace",
"bindings": {
"shift-escape": "dock::FocusDock",
"cmd-shift-c": "contacts_panel::ToggleFocus",
"cmd-shift-b": "workspace::ToggleRightSidebar"
}
},

35
crates/call/Cargo.toml Normal file
View file

@ -0,0 +1,35 @@
[package]
name = "call"
version = "0.1.0"
edition = "2021"
[lib]
path = "src/call.rs"
doctest = false
[features]
test-support = [
"client/test-support",
"collections/test-support",
"gpui/test-support",
"project/test-support",
"util/test-support"
]
[dependencies]
client = { path = "../client" }
collections = { path = "../collections" }
gpui = { path = "../gpui" }
project = { path = "../project" }
util = { path = "../util" }
anyhow = "1.0.38"
futures = "0.3"
postage = { version = "0.4.1", features = ["futures-traits"] }
[dev-dependencies]
client = { path = "../client", features = ["test-support"] }
collections = { path = "../collections", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }

261
crates/call/src/call.rs Normal file
View file

@ -0,0 +1,261 @@
mod participant;
pub mod room;
use anyhow::{anyhow, Result};
use client::{proto, Client, TypedEnvelope, User, UserStore};
use gpui::{
AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext,
Subscription, Task,
};
pub use participant::ParticipantLocation;
use postage::watch;
use project::Project;
pub use room::Room;
use std::sync::Arc;
pub fn init(client: Arc<Client>, user_store: ModelHandle<UserStore>, cx: &mut MutableAppContext) {
let active_call = cx.add_model(|cx| ActiveCall::new(client, user_store, cx));
cx.set_global(active_call);
}
#[derive(Clone)]
pub struct IncomingCall {
pub room_id: u64,
pub caller: Arc<User>,
pub participants: Vec<Arc<User>>,
pub initial_project: Option<proto::ParticipantProject>,
}
pub struct ActiveCall {
room: Option<(ModelHandle<Room>, Vec<Subscription>)>,
incoming_call: (
watch::Sender<Option<IncomingCall>>,
watch::Receiver<Option<IncomingCall>>,
),
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
_subscriptions: Vec<client::Subscription>,
}
impl Entity for ActiveCall {
type Event = room::Event;
}
impl ActiveCall {
fn new(
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
cx: &mut ModelContext<Self>,
) -> Self {
Self {
room: None,
incoming_call: watch::channel(),
_subscriptions: vec![
client.add_request_handler(cx.handle(), Self::handle_incoming_call),
client.add_message_handler(cx.handle(), Self::handle_call_canceled),
],
client,
user_store,
}
}
async fn handle_incoming_call(
this: ModelHandle<Self>,
envelope: TypedEnvelope<proto::IncomingCall>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<proto::Ack> {
let user_store = this.read_with(&cx, |this, _| this.user_store.clone());
let call = IncomingCall {
room_id: envelope.payload.room_id,
participants: user_store
.update(&mut cx, |user_store, cx| {
user_store.get_users(envelope.payload.participant_user_ids, cx)
})
.await?,
caller: user_store
.update(&mut cx, |user_store, cx| {
user_store.get_user(envelope.payload.caller_user_id, cx)
})
.await?,
initial_project: envelope.payload.initial_project,
};
this.update(&mut cx, |this, _| {
*this.incoming_call.0.borrow_mut() = Some(call);
});
Ok(proto::Ack {})
}
async fn handle_call_canceled(
this: ModelHandle<Self>,
_: TypedEnvelope<proto::CallCanceled>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, _| {
*this.incoming_call.0.borrow_mut() = None;
});
Ok(())
}
pub fn global(cx: &AppContext) -> ModelHandle<Self> {
cx.global::<ModelHandle<Self>>().clone()
}
pub fn invite(
&mut self,
recipient_user_id: u64,
initial_project: Option<ModelHandle<Project>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let client = self.client.clone();
let user_store = self.user_store.clone();
cx.spawn(|this, mut cx| async move {
if let Some(room) = this.read_with(&cx, |this, _| this.room().cloned()) {
let initial_project_id = if let Some(initial_project) = initial_project {
Some(
room.update(&mut cx, |room, cx| room.share_project(initial_project, cx))
.await?,
)
} else {
None
};
room.update(&mut cx, |room, cx| {
room.call(recipient_user_id, initial_project_id, cx)
})
.await?;
} else {
let room = cx
.update(|cx| {
Room::create(recipient_user_id, initial_project, client, user_store, cx)
})
.await?;
this.update(&mut cx, |this, cx| this.set_room(Some(room), cx));
};
Ok(())
})
}
pub fn cancel_invite(
&mut self,
recipient_user_id: u64,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let room_id = if let Some(room) = self.room() {
room.read(cx).id()
} else {
return Task::ready(Err(anyhow!("no active call")));
};
let client = self.client.clone();
cx.foreground().spawn(async move {
client
.request(proto::CancelCall {
room_id,
recipient_user_id,
})
.await?;
anyhow::Ok(())
})
}
pub fn incoming(&self) -> watch::Receiver<Option<IncomingCall>> {
self.incoming_call.1.clone()
}
pub fn accept_incoming(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
if self.room.is_some() {
return Task::ready(Err(anyhow!("cannot join while on another call")));
}
let call = if let Some(call) = self.incoming_call.1.borrow().clone() {
call
} else {
return Task::ready(Err(anyhow!("no incoming call")));
};
let join = Room::join(&call, self.client.clone(), self.user_store.clone(), cx);
cx.spawn(|this, mut cx| async move {
let room = join.await?;
this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx));
Ok(())
})
}
pub fn decline_incoming(&mut self) -> Result<()> {
let call = self
.incoming_call
.0
.borrow_mut()
.take()
.ok_or_else(|| anyhow!("no incoming call"))?;
self.client.send(proto::DeclineCall {
room_id: call.room_id,
})?;
Ok(())
}
pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
if let Some((room, _)) = self.room.take() {
room.update(cx, |room, cx| room.leave(cx))?;
cx.notify();
}
Ok(())
}
pub fn share_project(
&mut self,
project: ModelHandle<Project>,
cx: &mut ModelContext<Self>,
) -> Task<Result<u64>> {
if let Some((room, _)) = self.room.as_ref() {
room.update(cx, |room, cx| room.share_project(project, cx))
} else {
Task::ready(Err(anyhow!("no active call")))
}
}
pub fn set_location(
&mut self,
project: Option<&ModelHandle<Project>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if let Some((room, _)) = self.room.as_ref() {
room.update(cx, |room, cx| room.set_location(project, cx))
} else {
Task::ready(Err(anyhow!("no active call")))
}
}
fn set_room(&mut self, room: Option<ModelHandle<Room>>, cx: &mut ModelContext<Self>) {
if room.as_ref() != self.room.as_ref().map(|room| &room.0) {
if let Some(room) = room {
if room.read(cx).status().is_offline() {
self.room = None;
} else {
let subscriptions = vec![
cx.observe(&room, |this, room, cx| {
if room.read(cx).status().is_offline() {
this.set_room(None, cx);
}
cx.notify();
}),
cx.subscribe(&room, |_, _, event, cx| cx.emit(event.clone())),
];
self.room = Some((room, subscriptions));
}
} else {
self.room = None;
}
cx.notify();
}
}
pub fn room(&self) -> Option<&ModelHandle<Room>> {
self.room.as_ref().map(|(room, _)| room)
}
}

View file

@ -0,0 +1,42 @@
use anyhow::{anyhow, Result};
use client::{proto, User};
use gpui::WeakModelHandle;
use project::Project;
use std::sync::Arc;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ParticipantLocation {
SharedProject { project_id: u64 },
UnsharedProject,
External,
}
impl ParticipantLocation {
pub fn from_proto(location: Option<proto::ParticipantLocation>) -> Result<Self> {
match location.and_then(|l| l.variant) {
Some(proto::participant_location::Variant::SharedProject(project)) => {
Ok(Self::SharedProject {
project_id: project.id,
})
}
Some(proto::participant_location::Variant::UnsharedProject(_)) => {
Ok(Self::UnsharedProject)
}
Some(proto::participant_location::Variant::External(_)) => Ok(Self::External),
None => Err(anyhow!("participant location was not provided")),
}
}
}
#[derive(Clone, Default)]
pub struct LocalParticipant {
pub projects: Vec<proto::ParticipantProject>,
pub active_project: Option<WeakModelHandle<Project>>,
}
#[derive(Clone, Debug)]
pub struct RemoteParticipant {
pub user: Arc<User>,
pub projects: Vec<proto::ParticipantProject>,
pub location: ParticipantLocation,
}

474
crates/call/src/room.rs Normal file
View file

@ -0,0 +1,474 @@
use crate::{
participant::{LocalParticipant, ParticipantLocation, RemoteParticipant},
IncomingCall,
};
use anyhow::{anyhow, Result};
use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore};
use collections::{BTreeMap, HashSet};
use futures::StreamExt;
use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task};
use project::Project;
use std::sync::Arc;
use util::ResultExt;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Event {
RemoteProjectShared {
owner: Arc<User>,
project_id: u64,
worktree_root_names: Vec<String>,
},
RemoteProjectUnshared {
project_id: u64,
},
Left,
}
pub struct Room {
id: u64,
status: RoomStatus,
local_participant: LocalParticipant,
remote_participants: BTreeMap<PeerId, RemoteParticipant>,
pending_participants: Vec<Arc<User>>,
participant_user_ids: HashSet<u64>,
pending_call_count: usize,
leave_when_empty: bool,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
subscriptions: Vec<client::Subscription>,
pending_room_update: Option<Task<()>>,
}
impl Entity for Room {
type Event = Event;
fn release(&mut self, _: &mut MutableAppContext) {
self.client.send(proto::LeaveRoom { id: self.id }).log_err();
}
}
impl Room {
fn new(
id: u64,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
cx: &mut ModelContext<Self>,
) -> Self {
let mut client_status = client.status();
cx.spawn_weak(|this, mut cx| async move {
let is_connected = client_status
.next()
.await
.map_or(false, |s| s.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() {
if let Some(this) = this.upgrade(&cx) {
let _ = this.update(&mut cx, |this, cx| this.leave(cx));
}
}
})
.detach();
Self {
id,
status: RoomStatus::Online,
participant_user_ids: Default::default(),
local_participant: Default::default(),
remote_participants: Default::default(),
pending_participants: Default::default(),
pending_call_count: 0,
subscriptions: vec![client.add_message_handler(cx.handle(), Self::handle_room_updated)],
leave_when_empty: false,
pending_room_update: None,
client,
user_store,
}
}
pub(crate) fn create(
recipient_user_id: u64,
initial_project: Option<ModelHandle<Project>>,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
cx: &mut MutableAppContext,
) -> Task<Result<ModelHandle<Self>>> {
cx.spawn(|mut cx| async move {
let response = client.request(proto::CreateRoom {}).await?;
let room = cx.add_model(|cx| Self::new(response.id, client, user_store, cx));
let initial_project_id = if let Some(initial_project) = initial_project {
let initial_project_id = room
.update(&mut cx, |room, cx| {
room.share_project(initial_project.clone(), cx)
})
.await?;
Some(initial_project_id)
} else {
None
};
match room
.update(&mut cx, |room, cx| {
room.leave_when_empty = true;
room.call(recipient_user_id, initial_project_id, cx)
})
.await
{
Ok(()) => Ok(room),
Err(error) => Err(anyhow!("room creation failed: {:?}", error)),
}
})
}
pub(crate) fn join(
call: &IncomingCall,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
cx: &mut MutableAppContext,
) -> Task<Result<ModelHandle<Self>>> {
let room_id = call.room_id;
cx.spawn(|mut cx| async move {
let response = client.request(proto::JoinRoom { id: room_id }).await?;
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
let room = cx.add_model(|cx| Self::new(room_id, client, user_store, cx));
room.update(&mut cx, |room, cx| {
room.leave_when_empty = true;
room.apply_room_update(room_proto, cx)?;
anyhow::Ok(())
})?;
Ok(room)
})
}
fn should_leave(&self) -> bool {
self.leave_when_empty
&& self.pending_room_update.is_none()
&& self.pending_participants.is_empty()
&& self.remote_participants.is_empty()
&& self.pending_call_count == 0
}
pub(crate) fn leave(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
if self.status.is_offline() {
return Err(anyhow!("room is offline"));
}
cx.notify();
cx.emit(Event::Left);
self.status = RoomStatus::Offline;
self.remote_participants.clear();
self.pending_participants.clear();
self.participant_user_ids.clear();
self.subscriptions.clear();
self.client.send(proto::LeaveRoom { id: self.id })?;
Ok(())
}
pub fn id(&self) -> u64 {
self.id
}
pub fn status(&self) -> RoomStatus {
self.status
}
pub fn local_participant(&self) -> &LocalParticipant {
&self.local_participant
}
pub fn remote_participants(&self) -> &BTreeMap<PeerId, RemoteParticipant> {
&self.remote_participants
}
pub fn pending_participants(&self) -> &[Arc<User>] {
&self.pending_participants
}
pub fn contains_participant(&self, user_id: u64) -> bool {
self.participant_user_ids.contains(&user_id)
}
async fn handle_room_updated(
this: ModelHandle<Self>,
envelope: TypedEnvelope<proto::RoomUpdated>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
let room = envelope
.payload
.room
.ok_or_else(|| anyhow!("invalid room"))?;
this.update(&mut cx, |this, cx| this.apply_room_update(room, cx))
}
fn apply_room_update(
&mut self,
mut room: proto::Room,
cx: &mut ModelContext<Self>,
) -> Result<()> {
// Filter ourselves out from the room's participants.
let local_participant_ix = room
.participants
.iter()
.position(|participant| Some(participant.user_id) == self.client.user_id());
let local_participant = local_participant_ix.map(|ix| room.participants.swap_remove(ix));
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| {
(
user_store.get_users(remote_participant_user_ids, cx),
user_store.get_users(room.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);
this.update(&mut cx, |this, cx| {
this.participant_user_ids.clear();
if let Some(participant) = local_participant {
this.local_participant.projects = participant.projects;
} else {
this.local_participant.projects.clear();
}
if let Some(participants) = remote_participants.log_err() {
for (participant, user) in room.participants.into_iter().zip(participants) {
let peer_id = PeerId(participant.peer_id);
this.participant_user_ids.insert(participant.user_id);
let old_projects = this
.remote_participants
.get(&peer_id)
.into_iter()
.flat_map(|existing| &existing.projects)
.map(|project| project.id)
.collect::<HashSet<_>>();
let new_projects = participant
.projects
.iter()
.map(|project| project.id)
.collect::<HashSet<_>>();
for project in &participant.projects {
if !old_projects.contains(&project.id) {
cx.emit(Event::RemoteProjectShared {
owner: user.clone(),
project_id: project.id,
worktree_root_names: project.worktree_root_names.clone(),
});
}
}
for unshared_project_id in old_projects.difference(&new_projects) {
cx.emit(Event::RemoteProjectUnshared {
project_id: *unshared_project_id,
});
}
this.remote_participants.insert(
peer_id,
RemoteParticipant {
user: user.clone(),
projects: participant.projects,
location: ParticipantLocation::from_proto(participant.location)
.unwrap_or(ParticipantLocation::External),
},
);
}
this.remote_participants.retain(|_, participant| {
if this.participant_user_ids.contains(&participant.user.id) {
true
} else {
for project in &participant.projects {
cx.emit(Event::RemoteProjectUnshared {
project_id: project.id,
});
}
false
}
});
}
if let Some(pending_participants) = pending_participants.log_err() {
this.pending_participants = pending_participants;
for participant in &this.pending_participants {
this.participant_user_ids.insert(participant.id);
}
}
this.pending_room_update.take();
if this.should_leave() {
let _ = this.leave(cx);
}
this.check_invariants();
cx.notify();
});
}));
cx.notify();
Ok(())
}
fn check_invariants(&self) {
#[cfg(any(test, feature = "test-support"))]
{
for participant in self.remote_participants.values() {
assert!(self.participant_user_ids.contains(&participant.user.id));
}
for participant in &self.pending_participants {
assert!(self.participant_user_ids.contains(&participant.id));
}
assert_eq!(
self.participant_user_ids.len(),
self.remote_participants.len() + self.pending_participants.len()
);
}
}
pub(crate) fn call(
&mut self,
recipient_user_id: u64,
initial_project_id: Option<u64>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if self.status.is_offline() {
return Task::ready(Err(anyhow!("room is offline")));
}
cx.notify();
let client = self.client.clone();
let room_id = self.id;
self.pending_call_count += 1;
cx.spawn(|this, mut cx| async move {
let result = client
.request(proto::Call {
room_id,
recipient_user_id,
initial_project_id,
})
.await;
this.update(&mut cx, |this, cx| {
this.pending_call_count -= 1;
if this.should_leave() {
this.leave(cx)?;
}
result
})?;
Ok(())
})
}
pub(crate) fn share_project(
&mut self,
project: ModelHandle<Project>,
cx: &mut ModelContext<Self>,
) -> Task<Result<u64>> {
if project.read(cx).is_remote() {
return Task::ready(Err(anyhow!("can't share remote project")));
} else if let Some(project_id) = project.read(cx).remote_id() {
return Task::ready(Ok(project_id));
}
let request = self.client.request(proto::ShareProject {
room_id: self.id(),
worktrees: project
.read(cx)
.worktrees(cx)
.map(|worktree| {
let worktree = worktree.read(cx);
proto::WorktreeMetadata {
id: worktree.id().to_proto(),
root_name: worktree.root_name().into(),
visible: worktree.is_visible(),
}
})
.collect(),
});
cx.spawn(|this, mut cx| async move {
let response = request.await?;
project
.update(&mut cx, |project, cx| {
project.shared(response.project_id, cx)
})
.await?;
// If the user's location is in this project, it changes from UnsharedProject to SharedProject.
this.update(&mut cx, |this, cx| {
let active_project = this.local_participant.active_project.as_ref();
if active_project.map_or(false, |location| *location == project) {
this.set_location(Some(&project), cx)
} else {
Task::ready(Ok(()))
}
})
.await?;
Ok(response.project_id)
})
}
pub fn set_location(
&mut self,
project: Option<&ModelHandle<Project>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if self.status.is_offline() {
return Task::ready(Err(anyhow!("room is offline")));
}
let client = self.client.clone();
let room_id = self.id;
let location = if let Some(project) = project {
self.local_participant.active_project = Some(project.downgrade());
if let Some(project_id) = project.read(cx).remote_id() {
proto::participant_location::Variant::SharedProject(
proto::participant_location::SharedProject { id: project_id },
)
} else {
proto::participant_location::Variant::UnsharedProject(
proto::participant_location::UnsharedProject {},
)
}
} else {
self.local_participant.active_project = None;
proto::participant_location::Variant::External(proto::participant_location::External {})
};
cx.notify();
cx.foreground().spawn(async move {
client
.request(proto::UpdateParticipantLocation {
room_id,
location: Some(proto::ParticipantLocation {
variant: Some(location),
}),
})
.await?;
Ok(())
})
}
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum RoomStatus {
Online,
Offline,
}
impl RoomStatus {
pub fn is_offline(&self) -> bool {
matches!(self, RoomStatus::Offline)
}
}

View file

@ -530,7 +530,7 @@ impl ChannelMessage {
) -> Result<Self> {
let sender = user_store
.update(cx, |user_store, cx| {
user_store.fetch_user(message.sender_id, cx)
user_store.get_user(message.sender_id, cx)
})
.await?;
Ok(ChannelMessage {

View file

@ -434,6 +434,29 @@ impl Client {
}
}
pub fn add_request_handler<M, E, H, F>(
self: &Arc<Self>,
model: ModelHandle<E>,
handler: H,
) -> Subscription
where
M: RequestMessage,
E: Entity,
H: 'static
+ Send
+ Sync
+ Fn(ModelHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
F: 'static + Future<Output = Result<M::Response>>,
{
self.add_message_handler(model, move |handle, envelope, this, cx| {
Self::respond_to_request(
envelope.receipt(),
handler(handle, envelope, this.clone(), cx),
this,
)
})
}
pub fn add_view_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
where
M: EntityMessage,

View file

@ -1,14 +1,14 @@
use super::{http::HttpClient, proto, Client, Status, TypedEnvelope};
use anyhow::{anyhow, Context, Result};
use collections::{hash_map::Entry, BTreeSet, HashMap, HashSet};
use collections::{hash_map::Entry, HashMap, HashSet};
use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt};
use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task};
use postage::{prelude::Stream, sink::Sink, watch};
use postage::{sink::Sink, watch};
use rpc::proto::{RequestMessage, UsersResponse};
use std::sync::{Arc, Weak};
use util::TryFutureExt as _;
#[derive(Debug)]
#[derive(Default, Debug)]
pub struct User {
pub id: u64,
pub github_login: String,
@ -39,14 +39,7 @@ impl Eq for User {}
pub struct Contact {
pub user: Arc<User>,
pub online: bool,
pub projects: Vec<ProjectMetadata>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ProjectMetadata {
pub id: u64,
pub visible_worktree_root_names: Vec<String>,
pub guests: BTreeSet<Arc<User>>,
pub busy: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -138,12 +131,12 @@ impl UserStore {
}),
_maintain_current_user: cx.spawn_weak(|this, mut cx| async move {
let mut status = client.status();
while let Some(status) = status.recv().await {
while let Some(status) = status.next().await {
match status {
Status::Connected { .. } => {
if let Some((this, user_id)) = this.upgrade(&cx).zip(client.user_id()) {
let fetch_user = this
.update(&mut cx, |this, cx| this.fetch_user(user_id, cx))
.update(&mut cx, |this, cx| this.get_user(user_id, cx))
.log_err();
let fetch_metrics_id =
client.request(proto::GetPrivateUserInfo {}).log_err();
@ -244,7 +237,6 @@ impl UserStore {
let mut user_ids = HashSet::default();
for contact in &message.contacts {
user_ids.insert(contact.user_id);
user_ids.extend(contact.projects.iter().flat_map(|w| &w.guests).copied());
}
user_ids.extend(message.incoming_requests.iter().map(|req| req.requester_id));
user_ids.extend(message.outgoing_requests.iter());
@ -268,9 +260,7 @@ impl UserStore {
for request in message.incoming_requests {
incoming_requests.push({
let user = this
.update(&mut cx, |this, cx| {
this.fetch_user(request.requester_id, cx)
})
.update(&mut cx, |this, cx| this.get_user(request.requester_id, cx))
.await?;
(user, request.should_notify)
});
@ -279,7 +269,7 @@ impl UserStore {
let mut outgoing_requests = Vec::new();
for requested_user_id in message.outgoing_requests {
outgoing_requests.push(
this.update(&mut cx, |this, cx| this.fetch_user(requested_user_id, cx))
this.update(&mut cx, |this, cx| this.get_user(requested_user_id, cx))
.await?,
);
}
@ -504,7 +494,7 @@ impl UserStore {
.unbounded_send(UpdateContacts::Clear(tx))
.unwrap();
async move {
rx.recv().await;
rx.next().await;
}
}
@ -514,25 +504,43 @@ impl UserStore {
.unbounded_send(UpdateContacts::Wait(tx))
.unwrap();
async move {
rx.recv().await;
rx.next().await;
}
}
pub fn get_users(
&mut self,
mut user_ids: Vec<u64>,
user_ids: Vec<u64>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
user_ids.retain(|id| !self.users.contains_key(id));
if user_ids.is_empty() {
Task::ready(Ok(()))
} else {
let load = self.load_users(proto::GetUsers { user_ids }, cx);
cx.foreground().spawn(async move {
load.await?;
Ok(())
) -> Task<Result<Vec<Arc<User>>>> {
let mut user_ids_to_fetch = user_ids.clone();
user_ids_to_fetch.retain(|id| !self.users.contains_key(id));
cx.spawn(|this, mut cx| async move {
if !user_ids_to_fetch.is_empty() {
this.update(&mut cx, |this, cx| {
this.load_users(
proto::GetUsers {
user_ids: user_ids_to_fetch,
},
cx,
)
})
.await?;
}
this.read_with(&cx, |this, _| {
user_ids
.iter()
.map(|user_id| {
this.users
.get(user_id)
.cloned()
.ok_or_else(|| anyhow!("user {} not found", user_id))
})
.collect()
})
}
})
}
pub fn fuzzy_search_users(
@ -543,7 +551,7 @@ impl UserStore {
self.load_users(proto::FuzzySearchUsers { query }, cx)
}
pub fn fetch_user(
pub fn get_user(
&mut self,
user_id: u64,
cx: &mut ModelContext<Self>,
@ -623,39 +631,15 @@ impl Contact {
) -> Result<Self> {
let user = user_store
.update(cx, |user_store, cx| {
user_store.fetch_user(contact.user_id, cx)
user_store.get_user(contact.user_id, cx)
})
.await?;
let mut projects = Vec::new();
for project in contact.projects {
let mut guests = BTreeSet::new();
for participant_id in project.guests {
guests.insert(
user_store
.update(cx, |user_store, cx| {
user_store.fetch_user(participant_id, cx)
})
.await?,
);
}
projects.push(ProjectMetadata {
id: project.id,
visible_worktree_root_names: project.visible_worktree_root_names.clone(),
guests,
});
}
Ok(Self {
user,
online: contact.online,
projects,
busy: contact.busy,
})
}
pub fn non_empty_projects(&self) -> impl Iterator<Item = &ProjectMetadata> {
self.projects
.iter()
.filter(|project| !project.visible_worktree_root_names.is_empty())
}
}
async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result<Arc<ImageData>> {

View file

@ -56,13 +56,14 @@ features = ["runtime-tokio-rustls", "postgres", "time", "uuid"]
[dev-dependencies]
collections = { path = "../collections", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
rpc = { path = "../rpc", features = ["test-support"] }
call = { path = "../call", features = ["test-support"] }
client = { path = "../client", features = ["test-support"] }
editor = { path = "../editor", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
lsp = { path = "../lsp", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
rpc = { path = "../rpc", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
theme = { path = "../theme" }
workspace = { path = "../workspace", features = ["test-support"] }

View file

@ -84,7 +84,23 @@ async fn main() {
},
)
.await
.expect("failed to insert user"),
.expect("failed to insert user")
.user_id,
);
} else if admin {
zed_user_ids.push(
db.create_user(
&format!("{}@zed.dev", github_user.login),
admin,
db::NewUserParams {
github_login: github_user.login,
github_user_id: github_user.id,
invite_count: 5,
},
)
.await
.expect("failed to insert user")
.user_id,
);
}
}

View file

@ -1098,10 +1098,7 @@ impl Db for PostgresDb {
.bind(user_id)
.fetch(&self.pool);
let mut contacts = vec![Contact::Accepted {
user_id,
should_notify: false,
}];
let mut contacts = Vec::new();
while let Some(row) = rows.next().await {
let (user_id_a, user_id_b, a_to_b, accepted, should_notify) = row?;
@ -2080,10 +2077,7 @@ mod test {
async fn get_contacts(&self, id: UserId) -> Result<Vec<Contact>> {
self.background.simulate_random_delay().await;
let mut contacts = vec![Contact::Accepted {
user_id: id,
should_notify: false,
}];
let mut contacts = Vec::new();
for contact in self.contacts.lock().iter() {
if contact.requester_id == id {

View file

@ -666,13 +666,7 @@ async fn test_add_contacts() {
let user_3 = user_ids[2];
// User starts with no contacts
assert_eq!(
db.get_contacts(user_1).await.unwrap(),
vec![Contact::Accepted {
user_id: user_1,
should_notify: false
}],
);
assert_eq!(db.get_contacts(user_1).await.unwrap(), &[]);
// User requests a contact. Both users see the pending request.
db.send_contact_request(user_1, user_2).await.unwrap();
@ -680,26 +674,14 @@ async fn test_add_contacts() {
assert!(!db.has_contact(user_2, user_1).await.unwrap());
assert_eq!(
db.get_contacts(user_1).await.unwrap(),
&[
Contact::Accepted {
user_id: user_1,
should_notify: false
},
Contact::Outgoing { user_id: user_2 }
],
&[Contact::Outgoing { user_id: user_2 }],
);
assert_eq!(
db.get_contacts(user_2).await.unwrap(),
&[
Contact::Incoming {
user_id: user_1,
should_notify: true
},
Contact::Accepted {
user_id: user_2,
should_notify: false
},
]
&[Contact::Incoming {
user_id: user_1,
should_notify: true
}]
);
// User 2 dismisses the contact request notification without accepting or rejecting.
@ -712,16 +694,10 @@ async fn test_add_contacts() {
.unwrap();
assert_eq!(
db.get_contacts(user_2).await.unwrap(),
&[
Contact::Incoming {
user_id: user_1,
should_notify: false
},
Contact::Accepted {
user_id: user_2,
should_notify: false
},
]
&[Contact::Incoming {
user_id: user_1,
should_notify: false
}]
);
// User can't accept their own contact request
@ -735,31 +711,19 @@ async fn test_add_contacts() {
.unwrap();
assert_eq!(
db.get_contacts(user_1).await.unwrap(),
&[
Contact::Accepted {
user_id: user_1,
should_notify: false
},
Contact::Accepted {
user_id: user_2,
should_notify: true
}
],
&[Contact::Accepted {
user_id: user_2,
should_notify: true
}],
);
assert!(db.has_contact(user_1, user_2).await.unwrap());
assert!(db.has_contact(user_2, user_1).await.unwrap());
assert_eq!(
db.get_contacts(user_2).await.unwrap(),
&[
Contact::Accepted {
user_id: user_1,
should_notify: false,
},
Contact::Accepted {
user_id: user_2,
should_notify: false,
},
]
&[Contact::Accepted {
user_id: user_1,
should_notify: false,
}]
);
// Users cannot re-request existing contacts.
@ -772,16 +736,10 @@ async fn test_add_contacts() {
.unwrap_err();
assert_eq!(
db.get_contacts(user_1).await.unwrap(),
&[
Contact::Accepted {
user_id: user_1,
should_notify: false
},
Contact::Accepted {
user_id: user_2,
should_notify: true,
},
]
&[Contact::Accepted {
user_id: user_2,
should_notify: true,
}]
);
// Users can dismiss notifications of other users accepting their requests.
@ -790,16 +748,10 @@ async fn test_add_contacts() {
.unwrap();
assert_eq!(
db.get_contacts(user_1).await.unwrap(),
&[
Contact::Accepted {
user_id: user_1,
should_notify: false
},
Contact::Accepted {
user_id: user_2,
should_notify: false,
},
]
&[Contact::Accepted {
user_id: user_2,
should_notify: false,
}]
);
// Users send each other concurrent contact requests and
@ -809,10 +761,6 @@ async fn test_add_contacts() {
assert_eq!(
db.get_contacts(user_1).await.unwrap(),
&[
Contact::Accepted {
user_id: user_1,
should_notify: false
},
Contact::Accepted {
user_id: user_2,
should_notify: false,
@ -820,21 +768,15 @@ async fn test_add_contacts() {
Contact::Accepted {
user_id: user_3,
should_notify: false
},
}
]
);
assert_eq!(
db.get_contacts(user_3).await.unwrap(),
&[
Contact::Accepted {
user_id: user_1,
should_notify: false
},
Contact::Accepted {
user_id: user_3,
should_notify: false
}
],
&[Contact::Accepted {
user_id: user_1,
should_notify: false
}],
);
// User declines a contact request. Both users see that it is gone.
@ -846,29 +788,17 @@ async fn test_add_contacts() {
assert!(!db.has_contact(user_3, user_2).await.unwrap());
assert_eq!(
db.get_contacts(user_2).await.unwrap(),
&[
Contact::Accepted {
user_id: user_1,
should_notify: false
},
Contact::Accepted {
user_id: user_2,
should_notify: false
}
]
&[Contact::Accepted {
user_id: user_1,
should_notify: false
}]
);
assert_eq!(
db.get_contacts(user_3).await.unwrap(),
&[
Contact::Accepted {
user_id: user_1,
should_notify: false
},
Contact::Accepted {
user_id: user_3,
should_notify: false
}
],
&[Contact::Accepted {
user_id: user_1,
should_notify: false
}],
);
}
}
@ -930,29 +860,17 @@ async fn test_invite_codes() {
assert_eq!(db.get_user_metrics_id(user2).await.unwrap(), metrics_id);
assert_eq!(
db.get_contacts(user1).await.unwrap(),
[
Contact::Accepted {
user_id: user1,
should_notify: false
},
Contact::Accepted {
user_id: user2,
should_notify: true
}
]
[Contact::Accepted {
user_id: user2,
should_notify: true
}]
);
assert_eq!(
db.get_contacts(user2).await.unwrap(),
[
Contact::Accepted {
user_id: user1,
should_notify: false
},
Contact::Accepted {
user_id: user2,
should_notify: false
}
]
[Contact::Accepted {
user_id: user1,
should_notify: false
}]
);
assert_eq!(
db.get_invite_code_for_user(user2).await.unwrap().unwrap().1,
@ -987,10 +905,6 @@ async fn test_invite_codes() {
assert_eq!(
db.get_contacts(user1).await.unwrap(),
[
Contact::Accepted {
user_id: user1,
should_notify: false
},
Contact::Accepted {
user_id: user2,
should_notify: true
@ -1003,16 +917,10 @@ async fn test_invite_codes() {
);
assert_eq!(
db.get_contacts(user3).await.unwrap(),
[
Contact::Accepted {
user_id: user1,
should_notify: false
},
Contact::Accepted {
user_id: user3,
should_notify: false
},
]
[Contact::Accepted {
user_id: user1,
should_notify: false
}]
);
assert_eq!(
db.get_invite_code_for_user(user3).await.unwrap().unwrap().1,
@ -1053,10 +961,6 @@ async fn test_invite_codes() {
assert_eq!(
db.get_contacts(user1).await.unwrap(),
[
Contact::Accepted {
user_id: user1,
should_notify: false
},
Contact::Accepted {
user_id: user2,
should_notify: true
@ -1073,16 +977,10 @@ async fn test_invite_codes() {
);
assert_eq!(
db.get_contacts(user4).await.unwrap(),
[
Contact::Accepted {
user_id: user1,
should_notify: false
},
Contact::Accepted {
user_id: user4,
should_notify: false
},
]
[Contact::Accepted {
user_id: user1,
should_notify: false
}]
);
assert_eq!(
db.get_invite_code_for_user(user4).await.unwrap().unwrap().1,

File diff suppressed because it is too large Load diff

View file

@ -22,7 +22,7 @@ use axum::{
routing::get,
Extension, Router, TypedHeader,
};
use collections::HashMap;
use collections::{HashMap, HashSet};
use futures::{
channel::mpsc,
future::{self, BoxFuture},
@ -88,11 +88,6 @@ impl<R: RequestMessage> Response<R> {
self.server.peer.respond(self.receipt, payload)?;
Ok(())
}
fn into_receipt(self) -> Receipt<R> {
self.responded.store(true, SeqCst);
self.receipt
}
}
pub struct Server {
@ -151,11 +146,17 @@ impl Server {
server
.add_request_handler(Server::ping)
.add_request_handler(Server::register_project)
.add_request_handler(Server::unregister_project)
.add_request_handler(Server::create_room)
.add_request_handler(Server::join_room)
.add_message_handler(Server::leave_room)
.add_request_handler(Server::call)
.add_request_handler(Server::cancel_call)
.add_message_handler(Server::decline_call)
.add_request_handler(Server::update_participant_location)
.add_request_handler(Server::share_project)
.add_message_handler(Server::unshare_project)
.add_request_handler(Server::join_project)
.add_message_handler(Server::leave_project)
.add_message_handler(Server::respond_to_join_project_request)
.add_message_handler(Server::update_project)
.add_message_handler(Server::register_project_activity)
.add_request_handler(Server::update_worktree)
@ -385,7 +386,11 @@ impl Server {
{
let mut store = this.store().await;
store.add_connection(connection_id, user_id, user.admin);
let incoming_call = store.add_connection(connection_id, user_id, user.admin);
if let Some(incoming_call) = incoming_call {
this.peer.send(connection_id, incoming_call)?;
}
this.peer.send(connection_id, store.build_initial_contacts_update(contacts))?;
if let Some((code, count)) = invite_code {
@ -468,69 +473,58 @@ impl Server {
async fn sign_out(self: &mut Arc<Self>, connection_id: ConnectionId) -> Result<()> {
self.peer.disconnect(connection_id);
let mut projects_to_unregister = Vec::new();
let removed_user_id;
let mut projects_to_unshare = Vec::new();
let mut contacts_to_update = HashSet::default();
{
let mut store = self.store().await;
let removed_connection = store.remove_connection(connection_id)?;
for (project_id, project) in removed_connection.hosted_projects {
projects_to_unregister.push(project_id);
for project in removed_connection.hosted_projects {
projects_to_unshare.push(project.id);
broadcast(connection_id, project.guests.keys().copied(), |conn_id| {
self.peer.send(
conn_id,
proto::UnregisterProject {
project_id: project_id.to_proto(),
proto::UnshareProject {
project_id: project.id.to_proto(),
},
)
});
for (_, receipts) in project.join_requests {
for receipt in receipts {
self.peer.respond(
receipt,
proto::JoinProjectResponse {
variant: Some(proto::join_project_response::Variant::Decline(
proto::join_project_response::Decline {
reason: proto::join_project_response::decline::Reason::WentOffline as i32
},
)),
},
)?;
}
}
}
for project_id in removed_connection.guest_project_ids {
if let Some(project) = store.project(project_id).trace_err() {
broadcast(connection_id, project.connection_ids(), |conn_id| {
self.peer.send(
conn_id,
proto::RemoveProjectCollaborator {
project_id: project_id.to_proto(),
peer_id: connection_id.0,
},
)
});
if project.guests.is_empty() {
self.peer
.send(
project.host_connection_id,
proto::ProjectUnshared {
project_id: project_id.to_proto(),
},
)
.trace_err();
}
}
for project in removed_connection.guest_projects {
broadcast(connection_id, project.connection_ids, |conn_id| {
self.peer.send(
conn_id,
proto::RemoveProjectCollaborator {
project_id: project.id.to_proto(),
peer_id: connection_id.0,
},
)
});
}
removed_user_id = removed_connection.user_id;
for connection_id in removed_connection.canceled_call_connection_ids {
self.peer
.send(connection_id, proto::CallCanceled {})
.trace_err();
contacts_to_update.extend(store.user_id_for_connection(connection_id).ok());
}
if let Some(room) = removed_connection
.room_id
.and_then(|room_id| store.room(room_id))
{
self.room_updated(room);
}
contacts_to_update.insert(removed_connection.user_id);
};
self.update_user_contacts(removed_user_id).await.trace_err();
for user_id in contacts_to_update {
self.update_user_contacts(user_id).await.trace_err();
}
for project_id in projects_to_unregister {
for project_id in projects_to_unshare {
self.app_state
.db
.unregister_project(project_id)
@ -598,76 +592,286 @@ impl Server {
Ok(())
}
async fn register_project(
async fn create_room(
self: Arc<Server>,
request: TypedEnvelope<proto::RegisterProject>,
response: Response<proto::RegisterProject>,
request: TypedEnvelope<proto::CreateRoom>,
response: Response<proto::CreateRoom>,
) -> Result<()> {
let user_id;
let room_id;
{
let mut store = self.store().await;
user_id = store.user_id_for_connection(request.sender_id)?;
room_id = store.create_room(request.sender_id)?;
}
response.send(proto::CreateRoomResponse { id: room_id })?;
self.update_user_contacts(user_id).await?;
Ok(())
}
async fn join_room(
self: Arc<Server>,
request: TypedEnvelope<proto::JoinRoom>,
response: Response<proto::JoinRoom>,
) -> Result<()> {
let user_id;
{
let mut store = self.store().await;
user_id = store.user_id_for_connection(request.sender_id)?;
let (room, recipient_connection_ids) =
store.join_room(request.payload.id, request.sender_id)?;
for recipient_id in recipient_connection_ids {
self.peer
.send(recipient_id, proto::CallCanceled {})
.trace_err();
}
response.send(proto::JoinRoomResponse {
room: Some(room.clone()),
})?;
self.room_updated(room);
}
self.update_user_contacts(user_id).await?;
Ok(())
}
async fn leave_room(self: Arc<Server>, message: TypedEnvelope<proto::LeaveRoom>) -> Result<()> {
let mut contacts_to_update = HashSet::default();
{
let mut store = self.store().await;
let user_id = store.user_id_for_connection(message.sender_id)?;
let left_room = store.leave_room(message.payload.id, message.sender_id)?;
contacts_to_update.insert(user_id);
for project in left_room.unshared_projects {
for connection_id in project.connection_ids() {
self.peer.send(
connection_id,
proto::UnshareProject {
project_id: project.id.to_proto(),
},
)?;
}
}
for project in left_room.left_projects {
if project.remove_collaborator {
for connection_id in project.connection_ids {
self.peer.send(
connection_id,
proto::RemoveProjectCollaborator {
project_id: project.id.to_proto(),
peer_id: message.sender_id.0,
},
)?;
}
self.peer.send(
message.sender_id,
proto::UnshareProject {
project_id: project.id.to_proto(),
},
)?;
}
}
if let Some(room) = left_room.room {
self.room_updated(room);
}
for connection_id in left_room.canceled_call_connection_ids {
self.peer
.send(connection_id, proto::CallCanceled {})
.trace_err();
contacts_to_update.extend(store.user_id_for_connection(connection_id).ok());
}
}
for user_id in contacts_to_update {
self.update_user_contacts(user_id).await?;
}
Ok(())
}
async fn call(
self: Arc<Server>,
request: TypedEnvelope<proto::Call>,
response: Response<proto::Call>,
) -> Result<()> {
let caller_user_id = self
.store()
.await
.user_id_for_connection(request.sender_id)?;
let recipient_user_id = UserId::from_proto(request.payload.recipient_user_id);
let initial_project_id = request
.payload
.initial_project_id
.map(ProjectId::from_proto);
if !self
.app_state
.db
.has_contact(caller_user_id, recipient_user_id)
.await?
{
return Err(anyhow!("cannot call a user who isn't a contact"))?;
}
let room_id = request.payload.room_id;
let mut calls = {
let mut store = self.store().await;
let (room, recipient_connection_ids, incoming_call) = store.call(
room_id,
recipient_user_id,
initial_project_id,
request.sender_id,
)?;
self.room_updated(room);
recipient_connection_ids
.into_iter()
.map(|recipient_connection_id| {
self.peer
.request(recipient_connection_id, incoming_call.clone())
})
.collect::<FuturesUnordered<_>>()
};
self.update_user_contacts(recipient_user_id).await?;
while let Some(call_response) = calls.next().await {
match call_response.as_ref() {
Ok(_) => {
response.send(proto::Ack {})?;
return Ok(());
}
Err(_) => {
call_response.trace_err();
}
}
}
{
let mut store = self.store().await;
let room = store.call_failed(room_id, recipient_user_id)?;
self.room_updated(&room);
}
self.update_user_contacts(recipient_user_id).await?;
Err(anyhow!("failed to ring call recipient"))?
}
async fn cancel_call(
self: Arc<Server>,
request: TypedEnvelope<proto::CancelCall>,
response: Response<proto::CancelCall>,
) -> Result<()> {
let recipient_user_id = UserId::from_proto(request.payload.recipient_user_id);
{
let mut store = self.store().await;
let (room, recipient_connection_ids) = store.cancel_call(
request.payload.room_id,
recipient_user_id,
request.sender_id,
)?;
for recipient_id in recipient_connection_ids {
self.peer
.send(recipient_id, proto::CallCanceled {})
.trace_err();
}
self.room_updated(room);
response.send(proto::Ack {})?;
}
self.update_user_contacts(recipient_user_id).await?;
Ok(())
}
async fn decline_call(
self: Arc<Server>,
message: TypedEnvelope<proto::DeclineCall>,
) -> Result<()> {
let recipient_user_id;
{
let mut store = self.store().await;
recipient_user_id = store.user_id_for_connection(message.sender_id)?;
let (room, recipient_connection_ids) =
store.decline_call(message.payload.room_id, message.sender_id)?;
for recipient_id in recipient_connection_ids {
self.peer
.send(recipient_id, proto::CallCanceled {})
.trace_err();
}
self.room_updated(room);
}
self.update_user_contacts(recipient_user_id).await?;
Ok(())
}
async fn update_participant_location(
self: Arc<Server>,
request: TypedEnvelope<proto::UpdateParticipantLocation>,
response: Response<proto::UpdateParticipantLocation>,
) -> Result<()> {
let room_id = request.payload.room_id;
let location = request
.payload
.location
.ok_or_else(|| anyhow!("invalid location"))?;
let mut store = self.store().await;
let room = store.update_participant_location(room_id, location, request.sender_id)?;
self.room_updated(room);
response.send(proto::Ack {})?;
Ok(())
}
fn room_updated(&self, room: &proto::Room) {
for participant in &room.participants {
self.peer
.send(
ConnectionId(participant.peer_id),
proto::RoomUpdated {
room: Some(room.clone()),
},
)
.trace_err();
}
}
async fn share_project(
self: Arc<Server>,
request: TypedEnvelope<proto::ShareProject>,
response: Response<proto::ShareProject>,
) -> Result<()> {
let user_id = self
.store()
.await
.user_id_for_connection(request.sender_id)?;
let project_id = self.app_state.db.register_project(user_id).await?;
self.store().await.register_project(
request.sender_id,
let mut store = self.store().await;
let room = store.share_project(
request.payload.room_id,
project_id,
request.payload.online,
request.payload.worktrees,
request.sender_id,
)?;
response.send(proto::RegisterProjectResponse {
response.send(proto::ShareProjectResponse {
project_id: project_id.to_proto(),
})?;
self.room_updated(room);
Ok(())
}
async fn unregister_project(
async fn unshare_project(
self: Arc<Server>,
request: TypedEnvelope<proto::UnregisterProject>,
response: Response<proto::UnregisterProject>,
message: TypedEnvelope<proto::UnshareProject>,
) -> Result<()> {
let project_id = ProjectId::from_proto(request.payload.project_id);
let (user_id, project) = {
let mut state = self.store().await;
let project = state.unregister_project(project_id, request.sender_id)?;
(state.user_id_for_connection(request.sender_id)?, project)
};
self.app_state.db.unregister_project(project_id).await?;
let project_id = ProjectId::from_proto(message.payload.project_id);
let mut store = self.store().await;
let (room, project) = store.unshare_project(project_id, message.sender_id)?;
broadcast(
request.sender_id,
project.guests.keys().copied(),
|conn_id| {
self.peer.send(
conn_id,
proto::UnregisterProject {
project_id: project_id.to_proto(),
},
)
},
message.sender_id,
project.guest_connection_ids(),
|conn_id| self.peer.send(conn_id, message.payload.clone()),
);
for (_, receipts) in project.join_requests {
for receipt in receipts {
self.peer.respond(
receipt,
proto::JoinProjectResponse {
variant: Some(proto::join_project_response::Variant::Decline(
proto::join_project_response::Decline {
reason: proto::join_project_response::decline::Reason::Closed
as i32,
},
)),
},
)?;
}
}
// Send out the `UpdateContacts` message before responding to the unregister
// request. This way, when the project's host can keep track of the project's
// remote id until after they've received the `UpdateContacts` message for
// themself.
self.update_user_contacts(user_id).await?;
response.send(proto::Ack {})?;
self.room_updated(room);
Ok(())
}
@ -721,176 +925,94 @@ impl Server {
};
tracing::info!(%project_id, %host_user_id, %host_connection_id, "join project");
let has_contact = self
.app_state
.db
.has_contact(guest_user_id, host_user_id)
.await?;
if !has_contact {
return Err(anyhow!("no such project"))?;
let mut store = self.store().await;
let (project, replica_id) = store.join_project(request.sender_id, project_id)?;
let peer_count = project.guests.len();
let mut collaborators = Vec::with_capacity(peer_count);
collaborators.push(proto::Collaborator {
peer_id: project.host_connection_id.0,
replica_id: 0,
user_id: project.host.user_id.to_proto(),
});
let worktrees = project
.worktrees
.iter()
.map(|(id, worktree)| proto::WorktreeMetadata {
id: *id,
root_name: worktree.root_name.clone(),
visible: worktree.visible,
})
.collect::<Vec<_>>();
// Add all guests other than the requesting user's own connections as collaborators
for (guest_conn_id, guest) in &project.guests {
if request.sender_id != *guest_conn_id {
collaborators.push(proto::Collaborator {
peer_id: guest_conn_id.0,
replica_id: guest.replica_id as u32,
user_id: guest.user_id.to_proto(),
});
}
}
self.store().await.request_join_project(
guest_user_id,
project_id,
response.into_receipt(),
)?;
self.peer.send(
host_connection_id,
proto::RequestJoinProject {
project_id: project_id.to_proto(),
requester_id: guest_user_id.to_proto(),
},
)?;
Ok(())
}
async fn respond_to_join_project_request(
self: Arc<Server>,
request: TypedEnvelope<proto::RespondToJoinProjectRequest>,
) -> Result<()> {
let host_user_id;
{
let mut state = self.store().await;
let project_id = ProjectId::from_proto(request.payload.project_id);
let project = state.project(project_id)?;
if project.host_connection_id != request.sender_id {
Err(anyhow!("no such connection"))?;
}
host_user_id = project.host.user_id;
let guest_user_id = UserId::from_proto(request.payload.requester_id);
if !request.payload.allow {
let receipts = state
.deny_join_project_request(request.sender_id, guest_user_id, project_id)
.ok_or_else(|| anyhow!("no such request"))?;
for receipt in receipts {
self.peer.respond(
receipt,
proto::JoinProjectResponse {
variant: Some(proto::join_project_response::Variant::Decline(
proto::join_project_response::Decline {
reason: proto::join_project_response::decline::Reason::Declined
as i32,
},
)),
},
)?;
}
return Ok(());
}
let (receipts_with_replica_ids, project) = state
.accept_join_project_request(request.sender_id, guest_user_id, project_id)
.ok_or_else(|| anyhow!("no such request"))?;
let peer_count = project.guests.len();
let mut collaborators = Vec::with_capacity(peer_count);
collaborators.push(proto::Collaborator {
peer_id: project.host_connection_id.0,
replica_id: 0,
user_id: project.host.user_id.to_proto(),
});
let worktrees = project
.worktrees
.iter()
.map(|(id, worktree)| proto::WorktreeMetadata {
id: *id,
root_name: worktree.root_name.clone(),
visible: worktree.visible,
})
.collect::<Vec<_>>();
// Add all guests other than the requesting user's own connections as collaborators
for (guest_conn_id, guest) in &project.guests {
if receipts_with_replica_ids
.iter()
.all(|(receipt, _)| receipt.sender_id != *guest_conn_id)
{
collaborators.push(proto::Collaborator {
peer_id: guest_conn_id.0,
replica_id: guest.replica_id as u32,
user_id: guest.user_id.to_proto(),
});
}
}
for conn_id in project.connection_ids() {
for (receipt, replica_id) in &receipts_with_replica_ids {
if conn_id != receipt.sender_id {
self.peer.send(
conn_id,
proto::AddProjectCollaborator {
project_id: project_id.to_proto(),
collaborator: Some(proto::Collaborator {
peer_id: receipt.sender_id.0,
replica_id: *replica_id as u32,
user_id: guest_user_id.to_proto(),
}),
},
)?;
}
}
}
// First, we send the metadata associated with each worktree.
for (receipt, replica_id) in &receipts_with_replica_ids {
self.peer.respond(
*receipt,
proto::JoinProjectResponse {
variant: Some(proto::join_project_response::Variant::Accept(
proto::join_project_response::Accept {
worktrees: worktrees.clone(),
replica_id: *replica_id as u32,
collaborators: collaborators.clone(),
language_servers: project.language_servers.clone(),
},
)),
for conn_id in project.connection_ids() {
if conn_id != request.sender_id {
self.peer.send(
conn_id,
proto::AddProjectCollaborator {
project_id: project_id.to_proto(),
collaborator: Some(proto::Collaborator {
peer_id: request.sender_id.0,
replica_id: replica_id as u32,
user_id: guest_user_id.to_proto(),
}),
},
)?;
}
}
for (worktree_id, worktree) in &project.worktrees {
#[cfg(any(test, feature = "test-support"))]
const MAX_CHUNK_SIZE: usize = 2;
#[cfg(not(any(test, feature = "test-support")))]
const MAX_CHUNK_SIZE: usize = 256;
// First, we send the metadata associated with each worktree.
response.send(proto::JoinProjectResponse {
worktrees: worktrees.clone(),
replica_id: replica_id as u32,
collaborators: collaborators.clone(),
language_servers: project.language_servers.clone(),
})?;
// Stream this worktree's entries.
let message = proto::UpdateWorktree {
project_id: project_id.to_proto(),
worktree_id: *worktree_id,
root_name: worktree.root_name.clone(),
updated_entries: worktree.entries.values().cloned().collect(),
removed_entries: Default::default(),
scan_id: worktree.scan_id,
is_last_update: worktree.is_complete,
};
for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) {
for (receipt, _) in &receipts_with_replica_ids {
self.peer.send(receipt.sender_id, update.clone())?;
}
}
for (worktree_id, worktree) in &project.worktrees {
#[cfg(any(test, feature = "test-support"))]
const MAX_CHUNK_SIZE: usize = 2;
#[cfg(not(any(test, feature = "test-support")))]
const MAX_CHUNK_SIZE: usize = 256;
// Stream this worktree's diagnostics.
for summary in worktree.diagnostic_summaries.values() {
for (receipt, _) in &receipts_with_replica_ids {
self.peer.send(
receipt.sender_id,
proto::UpdateDiagnosticSummary {
project_id: project_id.to_proto(),
worktree_id: *worktree_id,
summary: Some(summary.clone()),
},
)?;
}
}
// Stream this worktree's entries.
let message = proto::UpdateWorktree {
project_id: project_id.to_proto(),
worktree_id: *worktree_id,
root_name: worktree.root_name.clone(),
updated_entries: worktree.entries.values().cloned().collect(),
removed_entries: Default::default(),
scan_id: worktree.scan_id,
is_last_update: worktree.is_complete,
};
for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) {
self.peer.send(request.sender_id, update.clone())?;
}
// Stream this worktree's diagnostics.
for summary in worktree.diagnostic_summaries.values() {
self.peer.send(
request.sender_id,
proto::UpdateDiagnosticSummary {
project_id: project_id.to_proto(),
worktree_id: *worktree_id,
summary: Some(summary.clone()),
},
)?;
}
}
self.update_user_contacts(host_user_id).await?;
Ok(())
}
@ -903,7 +1025,7 @@ impl Server {
let project;
{
let mut store = self.store().await;
project = store.leave_project(sender_id, project_id)?;
project = store.leave_project(project_id, sender_id)?;
tracing::info!(
%project_id,
host_user_id = %project.host_user_id,
@ -922,27 +1044,8 @@ impl Server {
)
});
}
if let Some(requester_id) = project.cancel_request {
self.peer.send(
project.host_connection_id,
proto::JoinProjectRequestCancelled {
project_id: project_id.to_proto(),
requester_id: requester_id.to_proto(),
},
)?;
}
if project.unshare {
self.peer.send(
project.host_connection_id,
proto::ProjectUnshared {
project_id: project_id.to_proto(),
},
)?;
}
}
self.update_user_contacts(project.host_user_id).await?;
Ok(())
}
@ -951,61 +1054,20 @@ impl Server {
request: TypedEnvelope<proto::UpdateProject>,
) -> Result<()> {
let project_id = ProjectId::from_proto(request.payload.project_id);
let user_id;
{
let mut state = self.store().await;
user_id = state.user_id_for_connection(request.sender_id)?;
let guest_connection_ids = state
.read_project(project_id, request.sender_id)?
.guest_connection_ids();
let unshared_project = state.update_project(
project_id,
&request.payload.worktrees,
request.payload.online,
request.sender_id,
)?;
if let Some(unshared_project) = unshared_project {
broadcast(
request.sender_id,
unshared_project.guests.keys().copied(),
|conn_id| {
self.peer.send(
conn_id,
proto::UnregisterProject {
project_id: project_id.to_proto(),
},
)
},
);
for (_, receipts) in unshared_project.pending_join_requests {
for receipt in receipts {
self.peer.respond(
receipt,
proto::JoinProjectResponse {
variant: Some(proto::join_project_response::Variant::Decline(
proto::join_project_response::Decline {
reason:
proto::join_project_response::decline::Reason::Closed
as i32,
},
)),
},
)?;
}
}
} else {
broadcast(request.sender_id, guest_connection_ids, |connection_id| {
self.peer.forward_send(
request.sender_id,
connection_id,
request.payload.clone(),
)
});
}
let room =
state.update_project(project_id, &request.payload.worktrees, request.sender_id)?;
broadcast(request.sender_id, guest_connection_ids, |connection_id| {
self.peer
.forward_send(request.sender_id, connection_id, request.payload.clone())
});
self.room_updated(room);
};
self.update_user_contacts(user_id).await?;
Ok(())
}
@ -1027,32 +1089,21 @@ impl Server {
) -> Result<()> {
let project_id = ProjectId::from_proto(request.payload.project_id);
let worktree_id = request.payload.worktree_id;
let (connection_ids, metadata_changed) = {
let mut store = self.store().await;
let (connection_ids, metadata_changed) = store.update_worktree(
request.sender_id,
project_id,
worktree_id,
&request.payload.root_name,
&request.payload.removed_entries,
&request.payload.updated_entries,
request.payload.scan_id,
request.payload.is_last_update,
)?;
(connection_ids, metadata_changed)
};
let connection_ids = self.store().await.update_worktree(
request.sender_id,
project_id,
worktree_id,
&request.payload.root_name,
&request.payload.removed_entries,
&request.payload.updated_entries,
request.payload.scan_id,
request.payload.is_last_update,
)?;
broadcast(request.sender_id, connection_ids, |connection_id| {
self.peer
.forward_send(request.sender_id, connection_id, request.payload.clone())
});
if metadata_changed {
let user_id = self
.store()
.await
.user_id_for_connection(request.sender_id)?;
self.update_user_contacts(user_id).await?;
}
response.send(proto::Ack {})?;
Ok(())
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,53 @@
[package]
name = "collab_ui"
version = "0.1.0"
edition = "2021"
[lib]
path = "src/collab_ui.rs"
doctest = false
[features]
test-support = [
"call/test-support",
"client/test-support",
"collections/test-support",
"editor/test-support",
"gpui/test-support",
"project/test-support",
"settings/test-support",
"util/test-support",
"workspace/test-support",
]
[dependencies]
call = { path = "../call" }
client = { path = "../client" }
clock = { path = "../clock" }
collections = { path = "../collections" }
editor = { path = "../editor" }
fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" }
menu = { path = "../menu" }
picker = { path = "../picker" }
project = { path = "../project" }
settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" }
workspace = { path = "../workspace" }
anyhow = "1.0"
futures = "0.3"
log = "0.4"
postage = { version = "0.4.1", features = ["futures-traits"] }
serde = { version = "1.0", features = ["derive", "rc"] }
[dev-dependencies]
call = { path = "../call", features = ["test-support"] }
client = { path = "../client", features = ["test-support"] }
collections = { path = "../collections", features = ["test-support"] }
editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }

View file

@ -0,0 +1,566 @@
use crate::{contact_notification::ContactNotification, contacts_popover};
use call::{ActiveCall, ParticipantLocation};
use client::{Authenticate, ContactEventKind, PeerId, User, UserStore};
use clock::ReplicaId;
use contacts_popover::ContactsPopover;
use gpui::{
actions,
color::Color,
elements::*,
geometry::{rect::RectF, vector::vec2f, PathBuilder},
json::{self, ToJson},
Border, CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext,
Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
};
use settings::Settings;
use std::ops::Range;
use theme::Theme;
use workspace::{FollowNextCollaborator, JoinProject, ToggleFollow, Workspace};
actions!(
contacts_titlebar_item,
[ToggleContactsPopover, ShareProject]
);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(CollabTitlebarItem::toggle_contacts_popover);
cx.add_action(CollabTitlebarItem::share_project);
}
pub struct CollabTitlebarItem {
workspace: WeakViewHandle<Workspace>,
user_store: ModelHandle<UserStore>,
contacts_popover: Option<ViewHandle<ContactsPopover>>,
_subscriptions: Vec<Subscription>,
}
impl Entity for CollabTitlebarItem {
type Event = ();
}
impl View for CollabTitlebarItem {
fn ui_name() -> &'static str {
"CollabTitlebarItem"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let workspace = if let Some(workspace) = self.workspace.upgrade(cx) {
workspace
} else {
return Empty::new().boxed();
};
let theme = cx.global::<Settings>().theme.clone();
let project = workspace.read(cx).project().read(cx);
let mut container = Flex::row();
if workspace.read(cx).client().status().borrow().is_connected() {
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));
}
}
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()
}
}
impl CollabTitlebarItem {
pub fn new(
workspace: &ViewHandle<Workspace>,
user_store: &ModelHandle<UserStore>,
cx: &mut ViewContext<Self>,
) -> Self {
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_window_activation(|this, active, cx| {
this.window_activation_changed(active, cx)
}));
subscriptions.push(cx.observe(user_store, |_, _, cx| cx.notify()));
subscriptions.push(
cx.subscribe(user_store, move |this, user_store, event, cx| {
if let Some(workspace) = this.workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
if let client::Event::Contact { user, kind } = event {
if let ContactEventKind::Requested | ContactEventKind::Accepted = kind {
workspace.show_notification(user.id as usize, cx, |cx| {
cx.add_view(|cx| {
ContactNotification::new(
user.clone(),
*kind,
user_store,
cx,
)
})
})
}
}
});
}
}),
);
Self {
workspace: workspace.downgrade(),
user_store: user_store.clone(),
contacts_popover: None,
_subscriptions: subscriptions,
}
}
fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
let workspace = self.workspace.upgrade(cx);
let room = ActiveCall::global(cx).read(cx).room().cloned();
if let Some((workspace, room)) = workspace.zip(room) {
let workspace = workspace.read(cx);
let project = if active {
Some(workspace.project().clone())
} else {
None
};
room.update(cx, |room, cx| {
room.set_location(project.as_ref(), cx)
.detach_and_log_err(cx);
});
}
}
fn share_project(&mut self, _: &ShareProject, 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.share_project(project, cx))
.detach_and_log_err(cx);
}
}
fn toggle_contacts_popover(&mut self, _: &ToggleContactsPopover, cx: &mut ViewContext<Self>) {
match self.contacts_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));
cx.focus(&view);
cx.subscribe(&view, |this, _, event, cx| {
match event {
contacts_popover::Event::Dismissed => {
this.contacts_popover = None;
}
}
cx.notify();
})
.detach();
self.contacts_popover = Some(view);
}
}
}
cx.notify();
}
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)
.incoming_contact_requests()
.is_empty()
{
None
} else {
Some(
Empty::new()
.collapsed()
.contained()
.with_style(titlebar.toggle_contacts_badge)
.contained()
.with_margin_left(titlebar.toggle_contacts_button.default.icon_width)
.with_margin_top(titlebar.toggle_contacts_button.default.icon_width)
.aligned()
.boxed(),
)
};
Stack::new()
.with_child(
MouseEventHandler::<ToggleContactsPopover>::new(0, cx, |state, _| {
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()
.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(ToggleContactsPopover);
})
.aligned()
.boxed(),
)
.with_children(badge)
.with_children(self.contacts_popover.as_ref().map(|popover| {
Overlay::new(
ChildView::new(popover)
.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)
.boxed()
}))
.boxed()
}
fn render_share_button(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
enum Share {}
let titlebar = &theme.workspace.titlebar;
MouseEventHandler::<Share>::new(0, cx, |state, _| {
let style = titlebar.share_button.style_for(state, false);
Label::new("Share".into(), style.text.clone())
.contained()
.with_style(style.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, cx| cx.dispatch_action(ShareProject))
.with_tooltip::<Share, _>(
0,
"Share project with call participants".into(),
None,
theme.tooltip.clone(),
cx,
)
.aligned()
.boxed()
}
fn render_collaborators(
&self,
workspace: &ViewHandle<Workspace>,
theme: &Theme,
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()
.iter()
.map(|(peer_id, collaborator)| (*peer_id, collaborator.clone()))
.collect::<Vec<_>>();
participants
.sort_by_key(|(peer_id, _)| Some(project.collaborators().get(peer_id)?.replica_id));
participants
.into_iter()
.filter_map(|(peer_id, participant)| {
let project = workspace.read(cx).project().read(cx);
let replica_id = project
.collaborators()
.get(&peer_id)
.map(|collaborator| collaborator.replica_id);
let user = participant.user.clone();
Some(self.render_avatar(
&user,
replica_id,
Some((peer_id, &user.github_login, participant.location)),
workspace,
theme,
cx,
))
})
.collect()
} else {
Default::default()
}
}
fn render_current_user(
&self,
workspace: &ViewHandle<Workspace>,
theme: &Theme,
cx: &mut RenderContext<Self>,
) -> Option<ElementBox> {
let user = workspace.read(cx).user_store().read(cx).current_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".to_string(), 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(),
)
}
}
fn render_avatar(
&self,
user: &User,
replica_id: Option<ReplicaId>,
peer: Option<(PeerId, &str, 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 mut avatar_style;
if let Some((_, _, location)) = peer.as_ref() {
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;
} else {
avatar_style = theme.workspace.titlebar.inactive_avatar;
}
} else {
avatar_style = theme.workspace.titlebar.inactive_avatar;
}
} else {
avatar_style = theme.workspace.titlebar.avatar;
}
let mut replica_color = None;
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);
}
}
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()
}))
.constrained()
.with_width(theme.workspace.titlebar.avatar_width)
.contained()
.with_margin_left(theme.workspace.titlebar.avatar_margin)
.boxed();
if let Some((peer_id, peer_github_login, location)) = peer {
if let Some(replica_id) = replica_id {
MouseEventHandler::<ToggleFollow>::new(replica_id.into(), cx, move |_, _| content)
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(ToggleFollow(peer_id))
})
.with_tooltip::<ToggleFollow, _>(
peer_id.0 as usize,
if is_followed {
format!("Unfollow {}", peer_github_login)
} else {
format!("Follow {}", peer_github_login)
},
Some(Box::new(FollowNextCollaborator)),
theme.tooltip.clone(),
cx,
)
.boxed()
} else if let ParticipantLocation::SharedProject { project_id } = location {
let user_id = user.id;
MouseEventHandler::<JoinProject>::new(peer_id.0 as usize, cx, move |_, _| content)
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(JoinProject {
project_id,
follow_user_id: user_id,
})
})
.with_tooltip::<JoinProject, _>(
peer_id.0 as usize,
format!("Follow {} into external project", peer_github_login),
Some(Box::new(FollowNextCollaborator)),
theme.tooltip.clone(),
cx,
)
.boxed()
} else {
content
}
} else {
content
}
}
fn render_connection_status(
&self,
workspace: &ViewHandle<Workspace>,
cx: &mut RenderContext<Self>,
) -> Option<ElementBox> {
let theme = &cx.global::<Settings>().theme;
match &*workspace.read(cx).client().status().borrow() {
client::Status::ConnectionError
| client::Status::ConnectionLost
| client::Status::Reauthenticating { .. }
| client::Status::Reconnecting { .. }
| client::Status::ReconnectionError { .. } => Some(
Container::new(
Align::new(
ConstrainedBox::new(
Svg::new("icons/cloud_slash_12.svg")
.with_color(theme.workspace.titlebar.offline_icon.color)
.boxed(),
)
.with_width(theme.workspace.titlebar.offline_icon.width)
.boxed(),
)
.boxed(),
)
.with_style(theme.workspace.titlebar.offline_icon.container)
.boxed(),
),
client::Status::UpgradeRequired => Some(
Label::new(
"Please update Zed to collaborate".to_string(),
theme.workspace.titlebar.outdated_warning.text.clone(),
)
.contained()
.with_style(theme.workspace.titlebar.outdated_warning.container)
.aligned()
.boxed(),
),
_ => None,
}
}
}
pub struct AvatarRibbon {
color: Color,
}
impl AvatarRibbon {
pub fn new(color: Color) -> AvatarRibbon {
AvatarRibbon { color }
}
}
impl Element for AvatarRibbon {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: gpui::SizeConstraint,
_: &mut gpui::LayoutContext,
) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
(constraint.max, ())
}
fn paint(
&mut self,
bounds: gpui::geometry::rect::RectF,
_: gpui::geometry::rect::RectF,
_: &mut Self::LayoutState,
cx: &mut gpui::PaintContext,
) -> Self::PaintState {
let mut path = PathBuilder::new();
path.reset(bounds.lower_left());
path.curve_to(
bounds.origin() + vec2f(bounds.height(), 0.),
bounds.origin(),
);
path.line_to(bounds.upper_right() - vec2f(bounds.height(), 0.));
path.curve_to(bounds.lower_right(), bounds.upper_right());
path.line_to(bounds.lower_left());
cx.scene.push_path(path.build(self.color, None));
}
fn dispatch_event(
&mut self,
_: &gpui::Event,
_: RectF,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
_: &mut gpui::EventContext,
) -> bool {
false
}
fn rect_for_text_range(
&self,
_: Range<usize>,
_: RectF,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &gpui::MeasurementContext,
) -> Option<RectF> {
None
}
fn debug(
&self,
bounds: gpui::geometry::rect::RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &gpui::DebugContext,
) -> gpui::json::Value {
json::json!({
"type": "AvatarRibbon",
"bounds": bounds.to_json(),
"color": self.color.to_json(),
})
}
}

View file

@ -0,0 +1,97 @@
mod collab_titlebar_item;
mod contact_finder;
mod contact_list;
mod contact_notification;
mod contacts_popover;
mod incoming_call_notification;
mod notifications;
mod project_shared_notification;
use call::ActiveCall;
pub use collab_titlebar_item::CollabTitlebarItem;
use gpui::MutableAppContext;
use project::Project;
use std::sync::Arc;
use workspace::{AppState, JoinProject, ToggleFollow, Workspace};
pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
collab_titlebar_item::init(cx);
contact_notification::init(cx);
contact_list::init(cx);
contact_finder::init(cx);
contacts_popover::init(cx);
incoming_call_notification::init(cx);
project_shared_notification::init(cx);
cx.add_global_action(move |action: &JoinProject, cx| {
let project_id = action.project_id;
let follow_user_id = action.follow_user_id;
let app_state = app_state.clone();
cx.spawn(|mut cx| async move {
let existing_workspace = cx.update(|cx| {
cx.window_ids()
.filter_map(|window_id| cx.root_view::<Workspace>(window_id))
.find(|workspace| {
workspace.read(cx).project().read(cx).remote_id() == Some(project_id)
})
});
let workspace = if let Some(existing_workspace) = existing_workspace {
existing_workspace
} else {
let project = Project::remote(
project_id,
app_state.client.clone(),
app_state.user_store.clone(),
app_state.project_store.clone(),
app_state.languages.clone(),
app_state.fs.clone(),
cx.clone(),
)
.await?;
let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
let mut workspace = Workspace::new(project, app_state.default_item_factory, cx);
(app_state.initialize_workspace)(&mut workspace, &app_state, cx);
workspace
});
workspace
};
cx.activate_window(workspace.window_id());
cx.platform().activate(true);
workspace.update(&mut cx, |workspace, cx| {
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
let follow_peer_id = room
.read(cx)
.remote_participants()
.iter()
.find(|(_, participant)| participant.user.id == follow_user_id)
.map(|(peer_id, _)| *peer_id)
.or_else(|| {
// If we couldn't follow the given user, follow the host instead.
let collaborator = workspace
.project()
.read(cx)
.collaborators()
.values()
.find(|collaborator| collaborator.replica_id == 0)?;
Some(collaborator.peer_id)
});
if let Some(follow_peer_id) = follow_peer_id {
if !workspace.is_following(follow_peer_id) {
workspace
.toggle_follow(&ToggleFollow(follow_peer_id), cx)
.map(|follow| follow.detach_and_log_err(cx));
}
}
}
});
anyhow::Ok(())
})
.detach_and_log_err(cx);
});
}

View file

@ -1,21 +1,15 @@
use client::{ContactRequestStatus, User, UserStore};
use gpui::{
actions, elements::*, AnyViewHandle, Entity, ModelHandle, MouseState, MutableAppContext,
RenderContext, Task, View, ViewContext, ViewHandle,
elements::*, AnyViewHandle, Entity, ModelHandle, MouseState, MutableAppContext, RenderContext,
Task, View, ViewContext, ViewHandle,
};
use picker::{Picker, PickerDelegate};
use settings::Settings;
use std::sync::Arc;
use util::TryFutureExt;
use workspace::Workspace;
use crate::render_icon_button;
actions!(contact_finder, [Toggle]);
pub fn init(cx: &mut MutableAppContext) {
Picker::<ContactFinder>::init(cx);
cx.add_action(ContactFinder::toggle);
}
pub struct ContactFinder {
@ -117,18 +111,21 @@ impl PickerDelegate for ContactFinder {
let icon_path = match request_status {
ContactRequestStatus::None | ContactRequestStatus::RequestReceived => {
"icons/check_8.svg"
}
ContactRequestStatus::RequestSent | ContactRequestStatus::RequestAccepted => {
"icons/x_mark_8.svg"
Some("icons/check_8.svg")
}
ContactRequestStatus::RequestSent => Some("icons/x_mark_8.svg"),
ContactRequestStatus::RequestAccepted => None,
};
let button_style = if self.user_store.read(cx).is_contact_request_pending(user) {
&theme.contact_finder.disabled_contact_button
} else {
&theme.contact_finder.contact_button
};
let style = theme.picker.item.style_for(mouse_state, selected);
let style = theme
.contact_finder
.picker
.item
.style_for(mouse_state, selected);
Flex::row()
.with_children(user.avatar.clone().map(|avatar| {
Image::new(avatar)
@ -145,12 +142,21 @@ impl PickerDelegate for ContactFinder {
.left()
.boxed(),
)
.with_child(
render_icon_button(button_style, icon_path)
.with_children(icon_path.map(|icon_path| {
Svg::new(icon_path)
.with_color(button_style.color)
.constrained()
.with_width(button_style.icon_width)
.aligned()
.contained()
.with_style(button_style.container)
.constrained()
.with_width(button_style.button_width)
.with_height(button_style.button_width)
.aligned()
.flex_float()
.boxed(),
)
.boxed()
}))
.contained()
.with_style(style.container)
.constrained()
@ -160,34 +166,16 @@ impl PickerDelegate for ContactFinder {
}
impl ContactFinder {
fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
workspace.toggle_modal(cx, |workspace, cx| {
let finder = cx.add_view(|cx| Self::new(workspace.user_store().clone(), cx));
cx.subscribe(&finder, Self::on_event).detach();
finder
});
}
pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
let this = cx.weak_handle();
Self {
picker: cx.add_view(|cx| Picker::new(this, cx)),
picker: cx.add_view(|cx| {
Picker::new(this, cx)
.with_theme(|cx| &cx.global::<Settings>().theme.contact_finder.picker)
}),
potential_contacts: Arc::from([]),
user_store,
selected_index: 0,
}
}
fn on_event(
workspace: &mut Workspace,
_: ViewHandle<Self>,
event: &Event,
cx: &mut ViewContext<Workspace>,
) {
match event {
Event::Dismissed => {
workspace.dismiss_modal(cx);
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -49,10 +49,7 @@ impl View for ContactNotification {
self.user.clone(),
"wants to add you as a contact",
Some("They won't know if you decline."),
RespondToContactRequest {
user_id: self.user.id,
accept: false,
},
Dismiss(self.user.id),
vec![
(
"Decline",

View file

@ -0,0 +1,162 @@
use crate::{contact_finder::ContactFinder, contact_list::ContactList};
use client::UserStore;
use gpui::{
actions, elements::*, ClipboardItem, CursorStyle, Entity, ModelHandle, MouseButton,
MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
};
use project::Project;
use settings::Settings;
actions!(contacts_popover, [ToggleContactFinder]);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ContactsPopover::toggle_contact_finder);
}
pub enum Event {
Dismissed,
}
enum Child {
ContactList(ViewHandle<ContactList>),
ContactFinder(ViewHandle<ContactFinder>),
}
pub struct ContactsPopover {
child: Child,
project: ModelHandle<Project>,
user_store: ModelHandle<UserStore>,
_subscription: Option<gpui::Subscription>,
}
impl ContactsPopover {
pub fn new(
project: ModelHandle<Project>,
user_store: ModelHandle<UserStore>,
cx: &mut ViewContext<Self>,
) -> Self {
let mut this = Self {
child: Child::ContactList(
cx.add_view(|cx| ContactList::new(project.clone(), user_store.clone(), cx)),
),
project,
user_store,
_subscription: None,
};
this.show_contact_list(cx);
this
}
fn toggle_contact_finder(&mut self, _: &ToggleContactFinder, cx: &mut ViewContext<Self>) {
match &self.child {
Child::ContactList(_) => self.show_contact_finder(cx),
Child::ContactFinder(_) => self.show_contact_list(cx),
}
}
fn show_contact_finder(&mut self, cx: &mut ViewContext<ContactsPopover>) {
let child = cx.add_view(|cx| ContactFinder::new(self.user_store.clone(), cx));
cx.focus(&child);
self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
crate::contact_finder::Event::Dismissed => cx.emit(Event::Dismissed),
}));
self.child = Child::ContactFinder(child);
cx.notify();
}
fn show_contact_list(&mut self, cx: &mut ViewContext<ContactsPopover>) {
let child =
cx.add_view(|cx| ContactList::new(self.project.clone(), self.user_store.clone(), cx));
cx.focus(&child);
self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
crate::contact_list::Event::Dismissed => cx.emit(Event::Dismissed),
}));
self.child = Child::ContactList(child);
cx.notify();
}
}
impl Entity for ContactsPopover {
type Event = Event;
}
impl View for ContactsPopover {
fn ui_name() -> &'static str {
"ContactsPopover"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = cx.global::<Settings>().theme.clone();
let child = match &self.child {
Child::ContactList(child) => ChildView::new(child),
Child::ContactFinder(child) => ChildView::new(child),
};
Flex::column()
.with_child(child.flex(1., true).boxed())
.with_children(
self.user_store
.read(cx)
.invite_info()
.cloned()
.and_then(|info| {
enum InviteLink {}
if info.count > 0 {
Some(
MouseEventHandler::<InviteLink>::new(0, cx, |state, cx| {
let style = theme
.contacts_popover
.invite_row
.style_for(state, false)
.clone();
let copied = cx.read_from_clipboard().map_or(false, |item| {
item.text().as_str() == info.url.as_ref()
});
Label::new(
format!(
"{} invite link ({} left)",
if copied { "Copied" } else { "Copy" },
info.count
),
style.label.clone(),
)
.aligned()
.left()
.constrained()
.with_height(theme.contacts_popover.invite_row_height)
.contained()
.with_style(style.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.write_to_clipboard(ClipboardItem::new(info.url.to_string()));
cx.notify();
})
.boxed(),
)
} else {
None
}
}),
)
.contained()
.with_style(theme.contacts_popover.container)
.constrained()
.with_width(theme.contacts_popover.width)
.with_height(theme.contacts_popover.height)
.boxed()
}
fn on_focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
if cx.is_self_focused() {
match &self.child {
Child::ContactList(child) => cx.focus(child),
Child::ContactFinder(child) => cx.focus(child),
}
}
}
}

View file

@ -0,0 +1,232 @@
use call::{ActiveCall, IncomingCall};
use client::proto;
use futures::StreamExt;
use gpui::{
elements::*,
geometry::{rect::RectF, vector::vec2f},
impl_internal_actions, CursorStyle, Entity, MouseButton, MutableAppContext, RenderContext,
View, ViewContext, WindowBounds, WindowKind, WindowOptions,
};
use settings::Settings;
use util::ResultExt;
use workspace::JoinProject;
impl_internal_actions!(incoming_call_notification, [RespondToCall]);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(IncomingCallNotification::respond_to_call);
let mut incoming_call = ActiveCall::global(cx).read(cx).incoming();
cx.spawn(|mut cx| async move {
let mut notification_window = None;
while let Some(incoming_call) = incoming_call.next().await {
if let Some(window_id) = notification_window.take() {
cx.remove_window(window_id);
}
if let Some(incoming_call) = incoming_call {
const PADDING: f32 = 16.;
let screen_size = cx.platform().screen_size();
let window_size = cx.read(|cx| {
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
vec2f(theme.window_width, theme.window_height)
});
let (window_id, _) = cx.add_window(
WindowOptions {
bounds: WindowBounds::Fixed(RectF::new(
vec2f(screen_size.x() - window_size.x() - PADDING, PADDING),
window_size,
)),
titlebar: None,
center: false,
kind: WindowKind::PopUp,
is_movable: false,
},
|_| IncomingCallNotification::new(incoming_call),
);
notification_window = Some(window_id);
}
}
})
.detach();
}
#[derive(Clone, PartialEq)]
struct RespondToCall {
accept: bool,
}
pub struct IncomingCallNotification {
call: IncomingCall,
}
impl IncomingCallNotification {
pub fn new(call: IncomingCall) -> Self {
Self { call }
}
fn respond_to_call(&mut self, action: &RespondToCall, cx: &mut ViewContext<Self>) {
let active_call = ActiveCall::global(cx);
if action.accept {
let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx));
let caller_user_id = self.call.caller.id;
let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id);
cx.spawn_weak(|_, mut cx| async move {
join.await?;
if let Some(project_id) = initial_project_id {
cx.update(|cx| {
cx.dispatch_global_action(JoinProject {
project_id,
follow_user_id: caller_user_id,
})
});
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
} else {
active_call.update(cx, |active_call, _| {
active_call.decline_incoming().log_err();
});
}
}
fn render_caller(&self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
let default_project = proto::ParticipantProject::default();
let initial_project = self
.call
.initial_project
.as_ref()
.unwrap_or(&default_project);
Flex::row()
.with_children(self.call.caller.avatar.clone().map(|avatar| {
Image::new(avatar)
.with_style(theme.caller_avatar)
.aligned()
.boxed()
}))
.with_child(
Flex::column()
.with_child(
Label::new(
self.call.caller.github_login.clone(),
theme.caller_username.text.clone(),
)
.contained()
.with_style(theme.caller_username.container)
.boxed(),
)
.with_child(
Label::new(
format!(
"is sharing a project in Zed{}",
if initial_project.worktree_root_names.is_empty() {
""
} else {
":"
}
),
theme.caller_message.text.clone(),
)
.contained()
.with_style(theme.caller_message.container)
.boxed(),
)
.with_children(if initial_project.worktree_root_names.is_empty() {
None
} else {
Some(
Label::new(
initial_project.worktree_root_names.join(", "),
theme.worktree_roots.text.clone(),
)
.contained()
.with_style(theme.worktree_roots.container)
.boxed(),
)
})
.contained()
.with_style(theme.caller_metadata)
.aligned()
.boxed(),
)
.contained()
.with_style(theme.caller_container)
.flex(1., true)
.boxed()
}
fn render_buttons(&self, cx: &mut RenderContext<Self>) -> ElementBox {
enum Accept {}
enum Decline {}
Flex::column()
.with_child(
MouseEventHandler::<Accept>::new(0, cx, |_, cx| {
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
Label::new("Accept".to_string(), theme.accept_button.text.clone())
.aligned()
.contained()
.with_style(theme.accept_button.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, cx| {
cx.dispatch_action(RespondToCall { accept: true });
})
.flex(1., true)
.boxed(),
)
.with_child(
MouseEventHandler::<Decline>::new(0, cx, |_, cx| {
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
Label::new("Decline".to_string(), theme.decline_button.text.clone())
.aligned()
.contained()
.with_style(theme.decline_button.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, cx| {
cx.dispatch_action(RespondToCall { accept: false });
})
.flex(1., true)
.boxed(),
)
.constrained()
.with_width(
cx.global::<Settings>()
.theme
.incoming_call_notification
.button_width,
)
.boxed()
}
}
impl Entity for IncomingCallNotification {
type Event = ();
}
impl View for IncomingCallNotification {
fn ui_name() -> &'static str {
"IncomingCallNotification"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
let background = cx
.global::<Settings>()
.theme
.incoming_call_notification
.background;
Flex::row()
.with_child(self.render_caller(cx))
.with_child(self.render_buttons(cx))
.contained()
.with_background_color(background)
.expanded()
.boxed()
}
}

View file

@ -1,9 +1,7 @@
use crate::render_icon_button;
use client::User;
use gpui::{
elements::{Flex, Image, Label, MouseEventHandler, Padding, ParentElement, Text},
platform::CursorStyle,
Action, Element, ElementBox, MouseButton, RenderContext, View,
elements::*, platform::CursorStyle, Action, Element, ElementBox, MouseButton, RenderContext,
View,
};
use settings::Settings;
use std::sync::Arc;
@ -53,11 +51,18 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
)
.with_child(
MouseEventHandler::<Dismiss>::new(user.id as usize, cx, |state, _| {
render_icon_button(
theme.dismiss_button.style_for(state, false),
"icons/x_mark_thin_8.svg",
)
.boxed()
let style = theme.dismiss_button.style_for(state, false);
Svg::new("icons/x_mark_thin_8.svg")
.with_color(style.color)
.constrained()
.with_width(style.icon_width)
.aligned()
.contained()
.with_style(style.container)
.constrained()
.with_width(style.button_width)
.with_height(style.button_width)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.with_padding(Padding::uniform(5.))

View file

@ -0,0 +1,232 @@
use call::{room, ActiveCall};
use client::User;
use collections::HashMap;
use gpui::{
actions,
elements::*,
geometry::{rect::RectF, vector::vec2f},
CursorStyle, Entity, MouseButton, MutableAppContext, RenderContext, View, ViewContext,
WindowBounds, WindowKind, WindowOptions,
};
use settings::Settings;
use std::sync::Arc;
use workspace::JoinProject;
actions!(project_shared_notification, [DismissProject]);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ProjectSharedNotification::join);
cx.add_action(ProjectSharedNotification::dismiss);
let active_call = ActiveCall::global(cx);
let mut notification_windows = HashMap::default();
cx.subscribe(&active_call, move |_, event, cx| match event {
room::Event::RemoteProjectShared {
owner,
project_id,
worktree_root_names,
} => {
const PADDING: f32 = 16.;
let screen_size = cx.platform().screen_size();
let theme = &cx.global::<Settings>().theme.project_shared_notification;
let window_size = vec2f(theme.window_width, theme.window_height);
let (window_id, _) = cx.add_window(
WindowOptions {
bounds: WindowBounds::Fixed(RectF::new(
vec2f(screen_size.x() - window_size.x() - PADDING, PADDING),
window_size,
)),
titlebar: None,
center: false,
kind: WindowKind::PopUp,
is_movable: false,
},
|_| {
ProjectSharedNotification::new(
owner.clone(),
*project_id,
worktree_root_names.clone(),
)
},
);
notification_windows.insert(*project_id, window_id);
}
room::Event::RemoteProjectUnshared { project_id } => {
if let Some(window_id) = notification_windows.remove(&project_id) {
cx.remove_window(window_id);
}
}
room::Event::Left => {
for (_, window_id) in notification_windows.drain() {
cx.remove_window(window_id);
}
}
})
.detach();
}
pub struct ProjectSharedNotification {
project_id: u64,
worktree_root_names: Vec<String>,
owner: Arc<User>,
}
impl ProjectSharedNotification {
fn new(owner: Arc<User>, project_id: u64, worktree_root_names: Vec<String>) -> Self {
Self {
project_id,
worktree_root_names,
owner,
}
}
fn join(&mut self, _: &JoinProject, cx: &mut ViewContext<Self>) {
let window_id = cx.window_id();
cx.remove_window(window_id);
cx.propagate_action();
}
fn dismiss(&mut self, _: &DismissProject, cx: &mut ViewContext<Self>) {
let window_id = cx.window_id();
cx.remove_window(window_id);
}
fn render_owner(&self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = &cx.global::<Settings>().theme.project_shared_notification;
Flex::row()
.with_children(self.owner.avatar.clone().map(|avatar| {
Image::new(avatar)
.with_style(theme.owner_avatar)
.aligned()
.boxed()
}))
.with_child(
Flex::column()
.with_child(
Label::new(
self.owner.github_login.clone(),
theme.owner_username.text.clone(),
)
.contained()
.with_style(theme.owner_username.container)
.boxed(),
)
.with_child(
Label::new(
format!(
"is sharing a project in Zed{}",
if self.worktree_root_names.is_empty() {
""
} else {
":"
}
),
theme.message.text.clone(),
)
.contained()
.with_style(theme.message.container)
.boxed(),
)
.with_children(if self.worktree_root_names.is_empty() {
None
} else {
Some(
Label::new(
self.worktree_root_names.join(", "),
theme.worktree_roots.text.clone(),
)
.contained()
.with_style(theme.worktree_roots.container)
.boxed(),
)
})
.contained()
.with_style(theme.owner_metadata)
.aligned()
.boxed(),
)
.contained()
.with_style(theme.owner_container)
.flex(1., true)
.boxed()
}
fn render_buttons(&self, cx: &mut RenderContext<Self>) -> ElementBox {
enum Open {}
enum Dismiss {}
let project_id = self.project_id;
let owner_user_id = self.owner.id;
Flex::column()
.with_child(
MouseEventHandler::<Open>::new(0, cx, |_, cx| {
let theme = &cx.global::<Settings>().theme.project_shared_notification;
Label::new("Open".to_string(), theme.open_button.text.clone())
.aligned()
.contained()
.with_style(theme.open_button.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(JoinProject {
project_id,
follow_user_id: owner_user_id,
});
})
.flex(1., true)
.boxed(),
)
.with_child(
MouseEventHandler::<Dismiss>::new(0, cx, |_, cx| {
let theme = &cx.global::<Settings>().theme.project_shared_notification;
Label::new("Dismiss".to_string(), theme.dismiss_button.text.clone())
.aligned()
.contained()
.with_style(theme.dismiss_button.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, cx| {
cx.dispatch_action(DismissProject);
})
.flex(1., true)
.boxed(),
)
.constrained()
.with_width(
cx.global::<Settings>()
.theme
.project_shared_notification
.button_width,
)
.boxed()
}
}
impl Entity for ProjectSharedNotification {
type Event = ();
}
impl View for ProjectSharedNotification {
fn ui_name() -> &'static str {
"ProjectSharedNotification"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
let background = cx
.global::<Settings>()
.theme
.project_shared_notification
.background;
Flex::row()
.with_child(self.render_owner(cx))
.with_child(self.render_buttons(cx))
.contained()
.with_background_color(background)
.expanded()
.boxed()
}
}

View file

@ -1,32 +0,0 @@
[package]
name = "contacts_panel"
version = "0.1.0"
edition = "2021"
[lib]
path = "src/contacts_panel.rs"
doctest = false
[dependencies]
client = { path = "../client" }
collections = { path = "../collections" }
editor = { path = "../editor" }
fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" }
menu = { path = "../menu" }
picker = { path = "../picker" }
project = { path = "../project" }
settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" }
workspace = { path = "../workspace" }
anyhow = "1.0"
futures = "0.3"
log = "0.4"
postage = { version = "0.4.1", features = ["futures-traits"] }
serde = { version = "1.0", features = ["derive", "rc"] }
[dev-dependencies]
language = { path = "../language", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }

File diff suppressed because it is too large Load diff

View file

@ -1,80 +0,0 @@
use client::User;
use gpui::{
actions, ElementBox, Entity, ModelHandle, MutableAppContext, RenderContext, View, ViewContext,
};
use project::Project;
use std::sync::Arc;
use workspace::Notification;
use crate::notifications::render_user_notification;
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(JoinProjectNotification::decline);
cx.add_action(JoinProjectNotification::accept);
}
pub enum Event {
Dismiss,
}
actions!(contacts_panel, [Accept, Decline]);
pub struct JoinProjectNotification {
project: ModelHandle<Project>,
user: Arc<User>,
}
impl JoinProjectNotification {
pub fn new(project: ModelHandle<Project>, user: Arc<User>, cx: &mut ViewContext<Self>) -> Self {
cx.subscribe(&project, |this, _, event, cx| {
if let project::Event::ContactCancelledJoinRequest(user) = event {
if *user == this.user {
cx.emit(Event::Dismiss);
}
}
})
.detach();
Self { project, user }
}
fn decline(&mut self, _: &Decline, cx: &mut ViewContext<Self>) {
self.project.update(cx, |project, cx| {
project.respond_to_join_request(self.user.id, false, cx)
});
cx.emit(Event::Dismiss)
}
fn accept(&mut self, _: &Accept, cx: &mut ViewContext<Self>) {
self.project.update(cx, |project, cx| {
project.respond_to_join_request(self.user.id, true, cx)
});
cx.emit(Event::Dismiss)
}
}
impl Entity for JoinProjectNotification {
type Event = Event;
}
impl View for JoinProjectNotification {
fn ui_name() -> &'static str {
"JoinProjectNotification"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
render_user_notification(
self.user.clone(),
"wants to join your project",
None,
Decline,
vec![("Decline", Box::new(Decline)), ("Accept", Box::new(Accept))],
cx,
)
}
}
impl Notification for JoinProjectNotification {
fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool {
matches!(event, Event::Dismiss)
}
}

View file

@ -1,32 +0,0 @@
[package]
name = "contacts_status_item"
version = "0.1.0"
edition = "2021"
[lib]
path = "src/contacts_status_item.rs"
doctest = false
[dependencies]
client = { path = "../client" }
collections = { path = "../collections" }
editor = { path = "../editor" }
fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" }
menu = { path = "../menu" }
picker = { path = "../picker" }
project = { path = "../project" }
settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" }
workspace = { path = "../workspace" }
anyhow = "1.0"
futures = "0.3"
log = "0.4"
postage = { version = "0.4.1", features = ["futures-traits"] }
serde = { version = "1.0", features = ["derive", "rc"] }
[dev-dependencies]
language = { path = "../language", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }

View file

@ -1,94 +0,0 @@
use editor::Editor;
use gpui::{elements::*, Entity, RenderContext, View, ViewContext, ViewHandle};
use settings::Settings;
pub enum Event {
Deactivated,
}
pub struct ContactsPopover {
filter_editor: ViewHandle<Editor>,
}
impl Entity for ContactsPopover {
type Event = Event;
}
impl View for ContactsPopover {
fn ui_name() -> &'static str {
"ContactsPopover"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = &cx.global::<Settings>().theme.contacts_popover;
Flex::row()
.with_child(
ChildView::new(self.filter_editor.clone())
.contained()
.with_style(
cx.global::<Settings>()
.theme
.contacts_panel
.user_query_editor
.container,
)
.flex(1., true)
.boxed(),
)
// .with_child(
// MouseEventHandler::<AddContact>::new(0, cx, |_, _| {
// Svg::new("icons/user_plus_16.svg")
// .with_color(theme.add_contact_button.color)
// .constrained()
// .with_height(16.)
// .contained()
// .with_style(theme.add_contact_button.container)
// .aligned()
// .boxed()
// })
// .with_cursor_style(CursorStyle::PointingHand)
// .on_click(MouseButton::Left, |_, cx| {
// cx.dispatch_action(contact_finder::Toggle)
// })
// .boxed(),
// )
.constrained()
.with_height(
cx.global::<Settings>()
.theme
.contacts_panel
.user_query_editor_height,
)
.aligned()
.top()
.contained()
.with_background_color(theme.background)
.with_uniform_padding(4.)
.boxed()
}
}
impl ContactsPopover {
pub fn new(cx: &mut ViewContext<Self>) -> Self {
cx.observe_window_activation(Self::window_activation_changed)
.detach();
let filter_editor = cx.add_view(|cx| {
let mut editor = Editor::single_line(
Some(|theme| theme.contacts_panel.user_query_editor.clone()),
cx,
);
editor.set_placeholder_text("Filter contacts", cx);
editor
});
Self { filter_editor }
}
fn window_activation_changed(&mut self, is_active: bool, cx: &mut ViewContext<Self>) {
if !is_active {
cx.emit(Event::Deactivated);
}
}
}

View file

@ -1,94 +0,0 @@
mod contacts_popover;
use contacts_popover::ContactsPopover;
use gpui::{
actions,
color::Color,
elements::*,
geometry::{rect::RectF, vector::vec2f},
Appearance, Entity, MouseButton, MutableAppContext, RenderContext, View, ViewContext,
ViewHandle, WindowKind,
};
actions!(contacts_status_item, [ToggleContactsPopover]);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ContactsStatusItem::toggle_contacts_popover);
}
pub struct ContactsStatusItem {
popover: Option<ViewHandle<ContactsPopover>>,
}
impl Entity for ContactsStatusItem {
type Event = ();
}
impl View for ContactsStatusItem {
fn ui_name() -> &'static str {
"ContactsStatusItem"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let color = match cx.appearance {
Appearance::Light | Appearance::VibrantLight => Color::black(),
Appearance::Dark | Appearance::VibrantDark => Color::white(),
};
MouseEventHandler::<Self>::new(0, cx, |_, _| {
Svg::new("icons/zed_22.svg")
.with_color(color)
.aligned()
.boxed()
})
.on_click(MouseButton::Left, |_, cx| {
cx.dispatch_action(ToggleContactsPopover);
})
.boxed()
}
}
impl ContactsStatusItem {
pub fn new() -> Self {
Self { popover: None }
}
fn toggle_contacts_popover(&mut self, _: &ToggleContactsPopover, cx: &mut ViewContext<Self>) {
match self.popover.take() {
Some(popover) => {
cx.remove_window(popover.window_id());
}
None => {
let window_bounds = cx.window_bounds();
let size = vec2f(360., 460.);
let origin = window_bounds.lower_left()
+ vec2f(window_bounds.width() / 2. - size.x() / 2., 0.);
let (_, popover) = cx.add_window(
gpui::WindowOptions {
bounds: gpui::WindowBounds::Fixed(RectF::new(origin, size)),
titlebar: None,
center: false,
kind: WindowKind::PopUp,
is_movable: false,
},
|cx| ContactsPopover::new(cx),
);
cx.subscribe(&popover, Self::on_popover_event).detach();
self.popover = Some(popover);
}
}
}
fn on_popover_event(
&mut self,
popover: ViewHandle<ContactsPopover>,
event: &contacts_popover::Event,
cx: &mut ViewContext<Self>,
) {
match event {
contacts_popover::Event::Deactivated => {
self.popover.take();
cx.remove_window(popover.window_id());
}
}
}
}

View file

@ -1731,7 +1731,8 @@ impl Element for EditorElement {
layout: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
cx.scene.push_layer(Some(bounds));
let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
cx.scene.push_layer(Some(visible_bounds));
let gutter_bounds = RectF::new(bounds.origin(), layout.gutter_size);
let text_bounds = RectF::new(

View file

@ -786,6 +786,24 @@ impl AsyncAppContext {
self.update(|cx| cx.add_window(window_options, build_root_view))
}
pub fn remove_window(&mut self, window_id: usize) {
self.update(|cx| cx.remove_window(window_id))
}
pub fn activate_window(&mut self, window_id: usize) {
self.update(|cx| cx.activate_window(window_id))
}
pub fn prompt(
&mut self,
window_id: usize,
level: PromptLevel,
msg: &str,
answers: &[&str],
) -> oneshot::Receiver<usize> {
self.update(|cx| cx.prompt(window_id, level, msg, answers))
}
pub fn platform(&self) -> Arc<dyn Platform> {
self.0.borrow().platform()
}
@ -1519,6 +1537,17 @@ impl MutableAppContext {
}
}
pub fn observe_default_global<G, F>(&mut self, observe: F) -> Subscription
where
G: Any + Default,
F: 'static + FnMut(&mut MutableAppContext),
{
if !self.has_global::<G>() {
self.set_global(G::default());
}
self.observe_global::<G, F>(observe)
}
pub fn observe_release<E, H, F>(&mut self, handle: &H, callback: F) -> Subscription
where
E: Entity,
@ -1887,6 +1916,10 @@ impl MutableAppContext {
})
}
pub fn clear_globals(&mut self) {
self.cx.globals.clear();
}
pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T>
where
T: Entity,
@ -1967,6 +2000,10 @@ impl MutableAppContext {
})
}
pub fn remove_status_bar_item(&mut self, id: usize) {
self.remove_window(id);
}
fn register_platform_window(
&mut self,
window_id: usize,
@ -4650,6 +4687,12 @@ impl<T> PartialEq for WeakModelHandle<T> {
impl<T> Eq for WeakModelHandle<T> {}
impl<T: Entity> PartialEq<ModelHandle<T>> for WeakModelHandle<T> {
fn eq(&self, other: &ModelHandle<T>) -> bool {
self.model_id == other.model_id
}
}
impl<T> Clone for WeakModelHandle<T> {
fn clone(&self) -> Self {
Self {

View file

@ -271,9 +271,6 @@ impl<T: Element> AnyElement for Lifecycle<T> {
mut layout,
} => {
let bounds = RectF::new(origin, size);
let visible_bounds = visible_bounds
.intersection(bounds)
.unwrap_or_else(|| RectF::new(bounds.origin(), Vector2F::default()));
let paint = element.paint(bounds, visible_bounds, &mut layout, cx);
Lifecycle::PostPaint {
element,
@ -292,9 +289,6 @@ impl<T: Element> AnyElement for Lifecycle<T> {
..
} => {
let bounds = RectF::new(origin, bounds.size());
let visible_bounds = visible_bounds
.intersection(bounds)
.unwrap_or_else(|| RectF::new(bounds.origin(), Vector2F::default()));
let paint = element.paint(bounds, visible_bounds, &mut layout, cx);
Lifecycle::PostPaint {
element,

View file

@ -241,11 +241,12 @@ impl Element for Flex {
remaining_space: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
let mut remaining_space = *remaining_space;
let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
let mut remaining_space = *remaining_space;
let overflowing = remaining_space < 0.;
if overflowing {
cx.scene.push_layer(Some(bounds));
cx.scene.push_layer(Some(visible_bounds));
}
if let Some(scroll_state) = &self.scroll_state {

View file

@ -27,6 +27,8 @@ pub struct ImageStyle {
pub height: Option<f32>,
#[serde(default)]
pub width: Option<f32>,
#[serde(default)]
pub grayscale: bool,
}
impl Image {
@ -74,6 +76,7 @@ impl Element for Image {
bounds,
border: self.style.border,
corner_radius: self.style.corner_radius,
grayscale: self.style.grayscale,
data: self.data.clone(),
});
}

View file

@ -261,7 +261,8 @@ impl Element for List {
scroll_top: &mut ListOffset,
cx: &mut PaintContext,
) {
cx.scene.push_layer(Some(bounds));
let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
cx.scene.push_layer(Some(visible_bounds));
cx.scene
.push_mouse_region(MouseRegion::new::<Self>(10, 0, bounds).on_scroll({

View file

@ -169,6 +169,7 @@ impl<Tag> Element for MouseEventHandler<Tag> {
_: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
let hit_bounds = self.hit_bounds(visible_bounds);
if let Some(style) = self.cursor_style {
cx.scene.push_cursor_region(CursorRegion {

View file

@ -217,7 +217,11 @@ impl Element for Overlay {
));
}
self.child.paint(bounds.origin(), bounds, cx);
self.child.paint(
bounds.origin(),
RectF::new(Vector2F::zero(), cx.window_size),
cx,
);
cx.scene.pop_stacking_context();
}

View file

@ -284,7 +284,9 @@ impl Element for UniformList {
layout: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
cx.scene.push_layer(Some(bounds));
let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
cx.scene.push_layer(Some(visible_bounds));
cx.scene.push_mouse_region(
MouseRegion::new::<Self>(self.view_id, 0, visible_bounds).on_scroll({

View file

@ -44,6 +44,8 @@ pub trait Platform: Send + Sync {
fn unhide_other_apps(&self);
fn quit(&self);
fn screen_size(&self) -> Vector2F;
fn open_window(
&self,
id: usize,

View file

@ -2,7 +2,9 @@ use super::{
event::key_to_native, status_item::StatusItem, BoolExt as _, Dispatcher, FontSystem, Window,
};
use crate::{
executor, keymap,
executor,
geometry::vector::{vec2f, Vector2F},
keymap,
platform::{self, CursorStyle},
Action, AppVersion, ClipboardItem, Event, Menu, MenuItem,
};
@ -12,7 +14,7 @@ use cocoa::{
appkit::{
NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular,
NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard,
NSPasteboardTypeString, NSSavePanel, NSWindow,
NSPasteboardTypeString, NSSavePanel, NSScreen, NSWindow,
},
base::{id, nil, selector, YES},
foundation::{
@ -486,6 +488,14 @@ impl platform::Platform for MacPlatform {
}
}
fn screen_size(&self) -> Vector2F {
unsafe {
let screen = NSScreen::mainScreen(nil);
let frame = NSScreen::frame(screen);
vec2f(frame.size.width as f32, frame.size.height as f32)
}
}
fn open_window(
&self,
id: usize,

View file

@ -747,6 +747,7 @@ impl Renderer {
border_left: border_width * (image.border.left as usize as f32),
border_color: image.border.color.to_uchar4(),
corner_radius,
grayscale: image.grayscale as u8,
});
}
@ -769,6 +770,7 @@ impl Renderer {
border_left: 0.,
border_color: Default::default(),
corner_radius: 0.,
grayscale: false as u8,
});
} else {
log::warn!("could not render glyph with id {}", image_glyph.id);

View file

@ -90,6 +90,7 @@ typedef struct {
float border_left;
vector_uchar4 border_color;
float corner_radius;
uint8_t grayscale;
} GPUIImage;
typedef enum {

View file

@ -44,6 +44,7 @@ struct QuadFragmentInput {
float border_left;
float4 border_color;
float corner_radius;
uchar grayscale; // only used in image shader
};
float4 quad_sdf(QuadFragmentInput input) {
@ -110,6 +111,7 @@ vertex QuadFragmentInput quad_vertex(
quad.border_left,
coloru_to_colorf(quad.border_color),
quad.corner_radius,
0,
};
}
@ -251,6 +253,7 @@ vertex QuadFragmentInput image_vertex(
image.border_left,
coloru_to_colorf(image.border_color),
image.corner_radius,
image.grayscale,
};
}
@ -260,6 +263,13 @@ fragment float4 image_fragment(
) {
constexpr sampler atlas_sampler(mag_filter::linear, min_filter::linear);
input.background_color = atlas.sample(atlas_sampler, input.atlas_position);
if (input.grayscale) {
float grayscale =
0.2126 * input.background_color.r +
0.7152 * input.background_color.g +
0.0722 * input.background_color.b;
input.background_color = float4(grayscale, grayscale, grayscale, input.background_color.a);
}
return quad_sdf(input);
}
@ -289,6 +299,7 @@ vertex QuadFragmentInput surface_vertex(
0.,
float4(0.),
0.,
0,
};
}

View file

@ -131,6 +131,10 @@ impl super::Platform for Platform {
fn quit(&self) {}
fn screen_size(&self) -> Vector2F {
vec2f(1024., 768.)
}
fn open_window(
&self,
_: usize,

View file

@ -172,6 +172,7 @@ pub struct Image {
pub bounds: RectF,
pub border: Border,
pub corner_radius: f32,
pub grayscale: bool,
pub data: Arc<ImageData>,
}

View file

@ -91,7 +91,7 @@ pub fn run_test(
cx.update(|cx| cx.remove_all_windows());
deterministic.run_until_parked();
cx.update(|_| {}); // flush effects
cx.update(|cx| cx.clear_globals());
leak_detector.lock().detect();
if is_last_iteration {

View file

@ -122,7 +122,7 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
cx_teardowns.extend(quote!(
#cx_varname.update(|cx| cx.remove_all_windows());
deterministic.run_until_parked();
#cx_varname.update(|_| {}); // flush effects
#cx_varname.update(|cx| cx.clear_globals());
));
inner_fn_args.extend(quote!(&mut #cx_varname,));
continue;

View file

@ -19,6 +19,7 @@ pub struct Picker<D: PickerDelegate> {
query_editor: ViewHandle<Editor>,
list_state: UniformListState,
max_size: Vector2F,
theme: Box<dyn FnMut(&AppContext) -> &theme::Picker>,
confirmed: bool,
}
@ -51,8 +52,8 @@ impl<D: PickerDelegate> View for Picker<D> {
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
let settings = cx.global::<Settings>();
let container_style = settings.theme.picker.container;
let theme = (self.theme)(cx);
let container_style = theme.container;
let delegate = self.delegate.clone();
let match_count = if let Some(delegate) = delegate.upgrade(cx.app) {
delegate.read(cx).match_count()
@ -64,17 +65,14 @@ impl<D: PickerDelegate> View for Picker<D> {
.with_child(
ChildView::new(&self.query_editor)
.contained()
.with_style(settings.theme.picker.input_editor.container)
.with_style(theme.input_editor.container)
.boxed(),
)
.with_child(
if match_count == 0 {
Label::new(
"No matches".into(),
settings.theme.picker.empty.label.clone(),
)
.contained()
.with_style(settings.theme.picker.empty.container)
Label::new("No matches".into(), theme.empty.label.clone())
.contained()
.with_style(theme.empty.container)
} else {
UniformList::new(
self.list_state.clone(),
@ -147,6 +145,7 @@ impl<D: PickerDelegate> Picker<D> {
list_state: Default::default(),
delegate,
max_size: vec2f(540., 420.),
theme: Box::new(|cx| &cx.global::<Settings>().theme.picker),
confirmed: false,
};
cx.defer(|this, cx| {
@ -163,6 +162,14 @@ impl<D: PickerDelegate> Picker<D> {
self
}
pub fn with_theme<F>(mut self, theme: F) -> Self
where
F: 'static + FnMut(&AppContext) -> &theme::Picker,
{
self.theme = Box::new(theme);
self
}
pub fn query(&self, cx: &AppContext) -> String {
self.query_editor.read(cx).text(cx)
}

View file

@ -8,7 +8,7 @@ pub mod worktree;
mod project_tests;
use anyhow::{anyhow, Context, Result};
use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore};
use client::{proto, Client, PeerId, TypedEnvelope, UserStore};
use clock::ReplicaId;
use collections::{hash_map, BTreeMap, HashMap, HashSet};
use futures::{future::Shared, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt};
@ -35,7 +35,6 @@ use lsp::{
};
use lsp_command::*;
use parking_lot::Mutex;
use postage::stream::Stream;
use postage::watch;
use rand::prelude::*;
use search::SearchQuery;
@ -74,7 +73,6 @@ pub trait Item: Entity {
}
pub struct ProjectStore {
db: Arc<Db>,
projects: Vec<WeakModelHandle<Project>>,
}
@ -127,7 +125,6 @@ pub struct Project {
buffer_snapshots: HashMap<u64, Vec<(i32, TextBufferSnapshot)>>,
buffers_being_formatted: HashSet<usize>,
nonce: u128,
initialized_persistent_state: bool,
_maintain_buffer_languages: Task<()>,
}
@ -156,13 +153,8 @@ enum WorktreeHandle {
enum ProjectClientState {
Local {
is_shared: bool,
remote_id_tx: watch::Sender<Option<u64>>,
remote_id_rx: watch::Receiver<Option<u64>>,
online_tx: watch::Sender<bool>,
online_rx: watch::Receiver<bool>,
_maintain_remote_id: Task<Option<()>>,
_maintain_online_status: Task<Option<()>>,
remote_id: Option<u64>,
_detect_unshare: Task<Option<()>>,
},
Remote {
sharing_has_stopped: bool,
@ -174,7 +166,6 @@ enum ProjectClientState {
#[derive(Clone, Debug)]
pub struct Collaborator {
pub user: Arc<User>,
pub peer_id: PeerId,
pub replica_id: ReplicaId,
}
@ -197,8 +188,6 @@ pub enum Event {
RemoteIdChanged(Option<u64>),
DisconnectedFromHost,
CollaboratorLeft(PeerId),
ContactRequestedJoin(Arc<User>),
ContactCancelledJoinRequest(Arc<User>),
}
pub enum LanguageServerState {
@ -383,17 +372,14 @@ impl FormatTrigger {
impl Project {
pub fn init(client: &Arc<Client>) {
client.add_model_message_handler(Self::handle_request_join_project);
client.add_model_message_handler(Self::handle_add_collaborator);
client.add_model_message_handler(Self::handle_buffer_reloaded);
client.add_model_message_handler(Self::handle_buffer_saved);
client.add_model_message_handler(Self::handle_start_language_server);
client.add_model_message_handler(Self::handle_update_language_server);
client.add_model_message_handler(Self::handle_remove_collaborator);
client.add_model_message_handler(Self::handle_join_project_request_cancelled);
client.add_model_message_handler(Self::handle_update_project);
client.add_model_message_handler(Self::handle_unregister_project);
client.add_model_message_handler(Self::handle_project_unshared);
client.add_model_message_handler(Self::handle_unshare_project);
client.add_model_message_handler(Self::handle_create_buffer_for_peer);
client.add_model_message_handler(Self::handle_update_buffer_file);
client.add_model_message_handler(Self::handle_update_buffer);
@ -426,7 +412,6 @@ impl Project {
}
pub fn local(
online: bool,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
project_store: ModelHandle<ProjectStore>,
@ -435,41 +420,19 @@ impl Project {
cx: &mut MutableAppContext,
) -> ModelHandle<Self> {
cx.add_model(|cx: &mut ModelContext<Self>| {
let (remote_id_tx, remote_id_rx) = watch::channel();
let _maintain_remote_id = cx.spawn_weak({
let mut status_rx = client.clone().status();
move |this, mut cx| async move {
while let Some(status) = status_rx.recv().await {
let this = this.upgrade(&cx)?;
if status.is_connected() {
this.update(&mut cx, |this, cx| this.register(cx))
.await
.log_err()?;
} else {
this.update(&mut cx, |this, cx| this.unregister(cx))
.await
.log_err();
let mut status = client.status();
let _detect_unshare = cx.spawn_weak(move |this, mut cx| {
async move {
let is_connected = status.next().await.map_or(false, |s| s.is_connected());
// Even if we're initially connected, any future change of the status means we momentarily disconnected.
if !is_connected || status.next().await.is_some() {
if let Some(this) = this.upgrade(&cx) {
let _ = this.update(&mut cx, |this, cx| this.unshare(cx));
}
}
None
}
});
let (online_tx, online_rx) = watch::channel_with(online);
let _maintain_online_status = cx.spawn_weak({
let mut online_rx = online_rx.clone();
move |this, mut cx| async move {
while let Some(online) = online_rx.recv().await {
let this = this.upgrade(&cx)?;
this.update(&mut cx, |this, cx| {
if !online {
this.unshared(cx);
}
this.metadata_changed(false, cx)
});
}
None
Ok(())
}
.log_err()
});
let handle = cx.weak_handle();
@ -485,13 +448,8 @@ impl Project {
loading_local_worktrees: Default::default(),
buffer_snapshots: Default::default(),
client_state: ProjectClientState::Local {
is_shared: false,
remote_id_tx,
remote_id_rx,
online_tx,
online_rx,
_maintain_remote_id,
_maintain_online_status,
remote_id: None,
_detect_unshare,
},
opened_buffer: watch::channel(),
client_subscriptions: Vec::new(),
@ -513,7 +471,6 @@ impl Project {
buffers_being_formatted: Default::default(),
next_language_server_id: 0,
nonce: StdRng::from_entropy().gen(),
initialized_persistent_state: false,
}
})
}
@ -535,24 +492,6 @@ impl Project {
})
.await?;
let response = match response.variant.ok_or_else(|| anyhow!("missing variant"))? {
proto::join_project_response::Variant::Accept(response) => response,
proto::join_project_response::Variant::Decline(decline) => {
match proto::join_project_response::decline::Reason::from_i32(decline.reason) {
Some(proto::join_project_response::decline::Reason::Declined) => {
Err(JoinProjectError::HostDeclined)?
}
Some(proto::join_project_response::decline::Reason::Closed) => {
Err(JoinProjectError::HostClosedProject)?
}
Some(proto::join_project_response::decline::Reason::WentOffline) => {
Err(JoinProjectError::HostWentOffline)?
}
None => Err(anyhow!("missing decline reason"))?,
}
}
};
let replica_id = response.replica_id as ReplicaId;
let mut worktrees = Vec::new();
@ -629,7 +568,6 @@ impl Project {
buffers_being_formatted: Default::default(),
buffer_snapshots: Default::default(),
nonce: StdRng::from_entropy().gen(),
initialized_persistent_state: false,
};
for worktree in worktrees {
this.add_worktree(&worktree, cx);
@ -647,7 +585,7 @@ impl Project {
.await?;
let mut collaborators = HashMap::default();
for message in response.collaborators {
let collaborator = Collaborator::from_proto(message, &user_store, &mut cx).await?;
let collaborator = Collaborator::from_proto(message);
collaborators.insert(collaborator.peer_id, collaborator);
}
@ -672,10 +610,9 @@ impl Project {
let http_client = client::test::FakeHttpClient::with_404_response();
let client = cx.update(|cx| client::Client::new(http_client.clone(), cx));
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
let project_store = cx.add_model(|_| ProjectStore::new(Db::open_fake()));
let project = cx.update(|cx| {
Project::local(true, client, user_store, project_store, languages, fs, cx)
});
let project_store = cx.add_model(|_| ProjectStore::new());
let project =
cx.update(|cx| Project::local(client, user_store, project_store, languages, fs, cx));
for path in root_paths {
let (tree, _) = project
.update(cx, |project, cx| {
@ -689,53 +626,6 @@ impl Project {
project
}
pub fn restore_state(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
if self.is_remote() {
return Task::ready(Ok(()));
}
let db = self.project_store.read(cx).db.clone();
let keys = self.db_keys_for_online_state(cx);
let online_by_default = cx.global::<Settings>().projects_online_by_default;
let read_online = cx.background().spawn(async move {
let values = db.read(keys)?;
anyhow::Ok(
values
.into_iter()
.all(|e| e.map_or(online_by_default, |e| e == [true as u8])),
)
});
cx.spawn(|this, mut cx| async move {
let online = read_online.await.log_err().unwrap_or(false);
this.update(&mut cx, |this, cx| {
this.initialized_persistent_state = true;
if let ProjectClientState::Local { online_tx, .. } = &mut this.client_state {
let mut online_tx = online_tx.borrow_mut();
if *online_tx != online {
*online_tx = online;
drop(online_tx);
this.metadata_changed(false, cx);
}
}
});
Ok(())
})
}
fn persist_state(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
if self.is_remote() || !self.initialized_persistent_state {
return Task::ready(Ok(()));
}
let db = self.project_store.read(cx).db.clone();
let keys = self.db_keys_for_online_state(cx);
let is_online = self.is_online();
cx.background().spawn(async move {
let value = &[is_online as u8];
db.write(keys.into_iter().map(|key| (key, value)))
})
}
fn on_settings_changed(&mut self, cx: &mut ModelContext<Self>) {
let settings = cx.global::<Settings>();
@ -864,136 +754,9 @@ impl Project {
&self.fs
}
pub fn set_online(&mut self, online: bool, _: &mut ModelContext<Self>) {
if let ProjectClientState::Local { online_tx, .. } = &mut self.client_state {
let mut online_tx = online_tx.borrow_mut();
if *online_tx != online {
*online_tx = online;
}
}
}
pub fn is_online(&self) -> bool {
match &self.client_state {
ProjectClientState::Local { online_rx, .. } => *online_rx.borrow(),
ProjectClientState::Remote { .. } => true,
}
}
fn unregister(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
self.unshared(cx);
if let ProjectClientState::Local { remote_id_rx, .. } = &mut self.client_state {
if let Some(remote_id) = *remote_id_rx.borrow() {
let request = self.client.request(proto::UnregisterProject {
project_id: remote_id,
});
return cx.spawn(|this, mut cx| async move {
let response = request.await;
// Unregistering the project causes the server to send out a
// contact update removing this project from the host's list
// of online projects. Wait until this contact update has been
// processed before clearing out this project's remote id, so
// that there is no moment where this project appears in the
// contact metadata and *also* has no remote id.
this.update(&mut cx, |this, cx| {
this.user_store()
.update(cx, |store, _| store.contact_updates_done())
})
.await;
this.update(&mut cx, |this, cx| {
if let ProjectClientState::Local { remote_id_tx, .. } =
&mut this.client_state
{
*remote_id_tx.borrow_mut() = None;
}
this.client_subscriptions.clear();
this.metadata_changed(false, cx);
});
response.map(drop)
});
}
}
Task::ready(Ok(()))
}
fn register(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
if let ProjectClientState::Local {
remote_id_rx,
online_rx,
..
} = &self.client_state
{
if remote_id_rx.borrow().is_some() {
return Task::ready(Ok(()));
}
let response = self.client.request(proto::RegisterProject {
online: *online_rx.borrow(),
});
cx.spawn(|this, mut cx| async move {
let remote_id = response.await?.project_id;
this.update(&mut cx, |this, cx| {
if let ProjectClientState::Local { remote_id_tx, .. } = &mut this.client_state {
*remote_id_tx.borrow_mut() = Some(remote_id);
}
this.metadata_changed(false, cx);
cx.emit(Event::RemoteIdChanged(Some(remote_id)));
this.client_subscriptions
.push(this.client.add_model_for_remote_entity(remote_id, cx));
Ok(())
})
})
} else {
Task::ready(Err(anyhow!("can't register a remote project")))
}
}
pub fn remote_id(&self) -> Option<u64> {
match &self.client_state {
ProjectClientState::Local { remote_id_rx, .. } => *remote_id_rx.borrow(),
ProjectClientState::Remote { remote_id, .. } => Some(*remote_id),
}
}
pub fn next_remote_id(&self) -> impl Future<Output = u64> {
let mut id = None;
let mut watch = None;
match &self.client_state {
ProjectClientState::Local { remote_id_rx, .. } => watch = Some(remote_id_rx.clone()),
ProjectClientState::Remote { remote_id, .. } => id = Some(*remote_id),
}
async move {
if let Some(id) = id {
return id;
}
let mut watch = watch.unwrap();
loop {
let id = *watch.borrow();
if let Some(id) = id {
return id;
}
watch.next().await;
}
}
}
pub fn shared_remote_id(&self) -> Option<u64> {
match &self.client_state {
ProjectClientState::Local {
remote_id_rx,
is_shared,
..
} => {
if *is_shared {
*remote_id_rx.borrow()
} else {
None
}
}
ProjectClientState::Local { remote_id, .. } => *remote_id,
ProjectClientState::Remote { remote_id, .. } => Some(*remote_id),
}
}
@ -1005,65 +768,50 @@ impl Project {
}
}
fn metadata_changed(&mut self, persist: bool, cx: &mut ModelContext<Self>) {
if let ProjectClientState::Local {
remote_id_rx,
online_rx,
..
} = &self.client_state
{
fn metadata_changed(&mut self, cx: &mut ModelContext<Self>) {
if let ProjectClientState::Local { remote_id, .. } = &self.client_state {
// Broadcast worktrees only if the project is online.
let worktrees = if *online_rx.borrow() {
self.worktrees
.iter()
.filter_map(|worktree| {
worktree
.upgrade(cx)
.map(|worktree| worktree.read(cx).as_local().unwrap().metadata_proto())
})
.collect()
} else {
Default::default()
};
if let Some(project_id) = *remote_id_rx.borrow() {
let online = *online_rx.borrow();
let worktrees = self
.worktrees
.iter()
.filter_map(|worktree| {
worktree
.upgrade(cx)
.map(|worktree| worktree.read(cx).as_local().unwrap().metadata_proto())
})
.collect();
if let Some(project_id) = *remote_id {
self.client
.send(proto::UpdateProject {
project_id,
worktrees,
online,
})
.log_err();
if online {
let worktrees = self.visible_worktrees(cx).collect::<Vec<_>>();
let scans_complete =
futures::future::join_all(worktrees.iter().filter_map(|worktree| {
Some(worktree.read(cx).as_local()?.scan_complete())
}));
let worktrees = self.visible_worktrees(cx).collect::<Vec<_>>();
let scans_complete =
futures::future::join_all(worktrees.iter().filter_map(|worktree| {
Some(worktree.read(cx).as_local()?.scan_complete())
}));
let worktrees = worktrees.into_iter().map(|handle| handle.downgrade());
cx.spawn_weak(move |_, cx| async move {
scans_complete.await;
cx.read(|cx| {
for worktree in worktrees {
if let Some(worktree) = worktree
.upgrade(cx)
.and_then(|worktree| worktree.read(cx).as_local())
{
worktree.send_extension_counts(project_id);
}
let worktrees = worktrees.into_iter().map(|handle| handle.downgrade());
cx.spawn_weak(move |_, cx| async move {
scans_complete.await;
cx.read(|cx| {
for worktree in worktrees {
if let Some(worktree) = worktree
.upgrade(cx)
.and_then(|worktree| worktree.read(cx).as_local())
{
worktree.send_extension_counts(project_id);
}
})
}
})
.detach();
}
})
.detach();
}
self.project_store.update(cx, |_, cx| cx.notify());
if persist {
self.persist_state(cx).detach_and_log_err(cx);
}
cx.notify();
}
}
@ -1101,23 +849,6 @@ impl Project {
.map(|tree| tree.read(cx).root_name())
}
fn db_keys_for_online_state(&self, cx: &AppContext) -> Vec<String> {
self.worktrees
.iter()
.filter_map(|worktree| {
let worktree = worktree.upgrade(cx)?.read(cx);
if worktree.is_visible() {
Some(format!(
"project-path-online:{}",
worktree.as_local().unwrap().abs_path().to_string_lossy()
))
} else {
None
}
})
.collect::<Vec<_>>()
}
pub fn worktree_for_id(
&self,
id: WorktreeId,
@ -1321,142 +1052,106 @@ impl Project {
}
}
fn share(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
if !self.is_online() {
return Task::ready(Err(anyhow!("can't share an offline project")));
}
let project_id;
if let ProjectClientState::Local {
remote_id_rx,
is_shared,
..
} = &mut self.client_state
{
if *is_shared {
return Task::ready(Ok(()));
}
*is_shared = true;
if let Some(id) = *remote_id_rx.borrow() {
project_id = id;
} else {
return Task::ready(Err(anyhow!("project hasn't been registered")));
}
} else {
return Task::ready(Err(anyhow!("can't share a remote project")));
};
for open_buffer in self.opened_buffers.values_mut() {
match open_buffer {
OpenBuffer::Strong(_) => {}
OpenBuffer::Weak(buffer) => {
if let Some(buffer) = buffer.upgrade(cx) {
*open_buffer = OpenBuffer::Strong(buffer);
}
}
OpenBuffer::Operations(_) => unreachable!(),
}
}
for worktree_handle in self.worktrees.iter_mut() {
match worktree_handle {
WorktreeHandle::Strong(_) => {}
WorktreeHandle::Weak(worktree) => {
if let Some(worktree) = worktree.upgrade(cx) {
*worktree_handle = WorktreeHandle::Strong(worktree);
}
}
}
}
let mut tasks = Vec::new();
for worktree in self.worktrees(cx).collect::<Vec<_>>() {
worktree.update(cx, |worktree, cx| {
let worktree = worktree.as_local_mut().unwrap();
tasks.push(worktree.share(project_id, cx));
});
}
for (server_id, status) in &self.language_server_statuses {
self.client
.send(proto::StartLanguageServer {
project_id,
server: Some(proto::LanguageServer {
id: *server_id as u64,
name: status.name.clone(),
}),
})
.log_err();
}
cx.spawn(|this, mut cx| async move {
for task in tasks {
task.await?;
}
this.update(&mut cx, |_, cx| cx.notify());
Ok(())
})
}
fn unshared(&mut self, cx: &mut ModelContext<Self>) {
if let ProjectClientState::Local { is_shared, .. } = &mut self.client_state {
if !*is_shared {
return;
pub fn shared(&mut self, project_id: u64, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
if let ProjectClientState::Local { remote_id, .. } = &mut self.client_state {
if remote_id.is_some() {
return Task::ready(Err(anyhow!("project was already shared")));
}
*is_shared = false;
self.collaborators.clear();
self.shared_buffers.clear();
for worktree_handle in self.worktrees.iter_mut() {
if let WorktreeHandle::Strong(worktree) = worktree_handle {
let is_visible = worktree.update(cx, |worktree, _| {
worktree.as_local_mut().unwrap().unshare();
worktree.is_visible()
});
if !is_visible {
*worktree_handle = WorktreeHandle::Weak(worktree.downgrade());
}
}
}
*remote_id = Some(project_id);
let mut worktree_share_tasks = Vec::new();
for open_buffer in self.opened_buffers.values_mut() {
if let OpenBuffer::Strong(buffer) = open_buffer {
*open_buffer = OpenBuffer::Weak(buffer.downgrade());
match open_buffer {
OpenBuffer::Strong(_) => {}
OpenBuffer::Weak(buffer) => {
if let Some(buffer) = buffer.upgrade(cx) {
*open_buffer = OpenBuffer::Strong(buffer);
}
}
OpenBuffer::Operations(_) => unreachable!(),
}
}
for worktree_handle in self.worktrees.iter_mut() {
match worktree_handle {
WorktreeHandle::Strong(_) => {}
WorktreeHandle::Weak(worktree) => {
if let Some(worktree) = worktree.upgrade(cx) {
*worktree_handle = WorktreeHandle::Strong(worktree);
}
}
}
}
for worktree in self.worktrees(cx).collect::<Vec<_>>() {
worktree.update(cx, |worktree, cx| {
let worktree = worktree.as_local_mut().unwrap();
worktree_share_tasks.push(worktree.share(project_id, cx));
});
}
for (server_id, status) in &self.language_server_statuses {
self.client
.send(proto::StartLanguageServer {
project_id,
server: Some(proto::LanguageServer {
id: *server_id as u64,
name: status.name.clone(),
}),
})
.log_err();
}
self.client_subscriptions
.push(self.client.add_model_for_remote_entity(project_id, cx));
self.metadata_changed(cx);
cx.emit(Event::RemoteIdChanged(Some(project_id)));
cx.notify();
cx.foreground().spawn(async move {
futures::future::try_join_all(worktree_share_tasks).await?;
Ok(())
})
} else {
log::error!("attempted to unshare a remote project");
Task::ready(Err(anyhow!("can't share a remote project")))
}
}
pub fn respond_to_join_request(
&mut self,
requester_id: u64,
allow: bool,
cx: &mut ModelContext<Self>,
) {
if let Some(project_id) = self.remote_id() {
let share = if self.is_online() && allow {
Some(self.share(cx))
} else {
None
};
let client = self.client.clone();
cx.foreground()
.spawn(async move {
client.send(proto::RespondToJoinProjectRequest {
requester_id,
project_id,
allow,
})?;
if let Some(share) = share {
share.await?;
pub fn unshare(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
if let ProjectClientState::Local { remote_id, .. } = &mut self.client_state {
if let Some(project_id) = remote_id.take() {
self.collaborators.clear();
self.shared_buffers.clear();
self.client_subscriptions.clear();
for worktree_handle in self.worktrees.iter_mut() {
if let WorktreeHandle::Strong(worktree) = worktree_handle {
let is_visible = worktree.update(cx, |worktree, _| {
worktree.as_local_mut().unwrap().unshare();
worktree.is_visible()
});
if !is_visible {
*worktree_handle = WorktreeHandle::Weak(worktree.downgrade());
}
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
for open_buffer in self.opened_buffers.values_mut() {
if let OpenBuffer::Strong(buffer) = open_buffer {
*open_buffer = OpenBuffer::Weak(buffer.downgrade());
}
}
self.metadata_changed(cx);
cx.notify();
self.client.send(proto::UnshareProject { project_id })?;
}
Ok(())
} else {
Err(anyhow!("attempted to unshare a remote project"))
}
}
@ -1930,7 +1625,7 @@ impl Project {
) -> Option<()> {
match event {
BufferEvent::Operation(operation) => {
if let Some(project_id) = self.shared_remote_id() {
if let Some(project_id) = self.remote_id() {
let request = self.client.request(proto::UpdateBuffer {
project_id,
buffer_id: buffer.read(cx).remote_id(),
@ -2335,7 +2030,7 @@ impl Project {
)
.ok();
if let Some(project_id) = this.shared_remote_id() {
if let Some(project_id) = this.remote_id() {
this.client
.send(proto::StartLanguageServer {
project_id,
@ -2742,7 +2437,7 @@ impl Project {
language_server_id: usize,
event: proto::update_language_server::Variant,
) {
if let Some(project_id) = self.shared_remote_id() {
if let Some(project_id) = self.remote_id() {
self.client
.send(proto::UpdateLanguageServer {
project_id,
@ -4472,7 +4167,7 @@ impl Project {
pub fn is_shared(&self) -> bool {
match &self.client_state {
ProjectClientState::Local { is_shared, .. } => *is_shared,
ProjectClientState::Local { remote_id, .. } => remote_id.is_some(),
ProjectClientState::Remote { .. } => false,
}
}
@ -4509,7 +4204,7 @@ impl Project {
let project_id = project.update(&mut cx, |project, cx| {
project.add_worktree(&worktree, cx);
project.shared_remote_id()
project.remote_id()
});
if let Some(project_id) = project_id {
@ -4550,7 +4245,7 @@ impl Project {
false
}
});
self.metadata_changed(true, cx);
self.metadata_changed(cx);
cx.notify();
}
@ -4578,7 +4273,7 @@ impl Project {
.push(WorktreeHandle::Weak(worktree.downgrade()));
}
self.metadata_changed(true, cx);
self.metadata_changed(cx);
cx.observe_release(worktree, |this, worktree, cx| {
this.remove_worktree(worktree.id(), cx);
cx.notify();
@ -4641,7 +4336,7 @@ impl Project {
renamed_buffers.push((cx.handle(), old_path));
}
if let Some(project_id) = self.shared_remote_id() {
if let Some(project_id) = self.remote_id() {
self.client
.send(proto::UpdateBufferFile {
project_id,
@ -4697,7 +4392,7 @@ impl Project {
Err(_) => return,
};
let shared_remote_id = self.shared_remote_id();
let remote_id = self.remote_id();
let client = self.client.clone();
cx.spawn(|_, mut cx| async move {
@ -4711,7 +4406,7 @@ impl Project {
buffer.remote_id()
});
if let Some(project_id) = shared_remote_id {
if let Some(project_id) = remote_id {
client
.send(proto::UpdateDiffBase {
project_id,
@ -4811,47 +4506,20 @@ impl Project {
// RPC message handlers
async fn handle_request_join_project(
async fn handle_unshare_project(
this: ModelHandle<Self>,
message: TypedEnvelope<proto::RequestJoinProject>,
_: TypedEnvelope<proto::UnshareProject>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
let user_id = message.payload.requester_id;
if this.read_with(&cx, |project, _| {
project.collaborators.values().any(|c| c.user.id == user_id)
}) {
this.update(&mut cx, |this, cx| {
this.respond_to_join_request(user_id, true, cx)
});
} else {
let user_store = this.read_with(&cx, |this, _| this.user_store.clone());
let user = user_store
.update(&mut cx, |store, cx| store.fetch_user(user_id, cx))
.await?;
this.update(&mut cx, |_, cx| cx.emit(Event::ContactRequestedJoin(user)));
}
Ok(())
}
async fn handle_unregister_project(
this: ModelHandle<Self>,
_: TypedEnvelope<proto::UnregisterProject>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| this.disconnected_from_host(cx));
Ok(())
}
async fn handle_project_unshared(
this: ModelHandle<Self>,
_: TypedEnvelope<proto::ProjectUnshared>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| this.unshared(cx));
Ok(())
this.update(&mut cx, |this, cx| {
if this.is_local() {
this.unshare(cx)?;
} else {
this.disconnected_from_host(cx);
}
Ok(())
})
}
async fn handle_add_collaborator(
@ -4860,14 +4528,13 @@ impl Project {
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
let user_store = this.read_with(&cx, |this, _| this.user_store.clone());
let collaborator = envelope
.payload
.collaborator
.take()
.ok_or_else(|| anyhow!("empty collaborator"))?;
let collaborator = Collaborator::from_proto(collaborator, &user_store, &mut cx).await?;
let collaborator = Collaborator::from_proto(collaborator);
this.update(&mut cx, |this, cx| {
this.collaborators
.insert(collaborator.peer_id, collaborator);
@ -4902,27 +4569,6 @@ impl Project {
})
}
async fn handle_join_project_request_cancelled(
this: ModelHandle<Self>,
envelope: TypedEnvelope<proto::JoinProjectRequestCancelled>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
let user = this
.update(&mut cx, |this, cx| {
this.user_store.update(cx, |user_store, cx| {
user_store.fetch_user(envelope.payload.requester_id, cx)
})
})
.await?;
this.update(&mut cx, |_, cx| {
cx.emit(Event::ContactCancelledJoinRequest(user));
});
Ok(())
}
async fn handle_update_project(
this: ModelHandle<Self>,
envelope: TypedEnvelope<proto::UpdateProject>,
@ -4954,7 +4600,7 @@ impl Project {
}
}
this.metadata_changed(true, cx);
this.metadata_changed(cx);
for (id, _) in old_worktrees_by_id {
cx.emit(Event::WorktreeRemoved(id));
}
@ -6182,9 +5828,8 @@ impl Project {
}
impl ProjectStore {
pub fn new(db: Arc<Db>) -> Self {
pub fn new() -> Self {
Self {
db,
projects: Default::default(),
}
}
@ -6313,10 +5958,10 @@ impl Entity for Project {
self.project_store.update(cx, ProjectStore::prune_projects);
match &self.client_state {
ProjectClientState::Local { remote_id_rx, .. } => {
if let Some(project_id) = *remote_id_rx.borrow() {
ProjectClientState::Local { remote_id, .. } => {
if let Some(project_id) = *remote_id {
self.client
.send(proto::UnregisterProject { project_id })
.send(proto::UnshareProject { project_id })
.log_err();
}
}
@ -6357,21 +6002,10 @@ impl Entity for Project {
}
impl Collaborator {
fn from_proto(
message: proto::Collaborator,
user_store: &ModelHandle<UserStore>,
cx: &mut AsyncAppContext,
) -> impl Future<Output = Result<Self>> {
let user = user_store.update(cx, |user_store, cx| {
user_store.fetch_user(message.user_id, cx)
});
async move {
Ok(Self {
peer_id: PeerId(message.peer_id),
user: user.await?,
replica_id: message.replica_id as ReplicaId,
})
fn from_proto(message: proto::Collaborator) -> Self {
Self {
peer_id: PeerId(message.peer_id),
replica_id: message.replica_id as ReplicaId,
}
}
}

View file

@ -10,107 +10,116 @@ message Envelope {
Error error = 5;
Ping ping = 6;
Test test = 7;
CreateRoom create_room = 8;
CreateRoomResponse create_room_response = 9;
JoinRoom join_room = 10;
JoinRoomResponse join_room_response = 11;
LeaveRoom leave_room = 12;
Call call = 13;
IncomingCall incoming_call = 14;
CallCanceled call_canceled = 15;
CancelCall cancel_call = 16;
DeclineCall decline_call = 17;
UpdateParticipantLocation update_participant_location = 18;
RoomUpdated room_updated = 19;
RegisterProject register_project = 8;
RegisterProjectResponse register_project_response = 9;
UnregisterProject unregister_project = 10;
RequestJoinProject request_join_project = 11;
RespondToJoinProjectRequest respond_to_join_project_request = 12;
JoinProjectRequestCancelled join_project_request_cancelled = 13;
JoinProject join_project = 14;
JoinProjectResponse join_project_response = 15;
LeaveProject leave_project = 16;
AddProjectCollaborator add_project_collaborator = 17;
RemoveProjectCollaborator remove_project_collaborator = 18;
ProjectUnshared project_unshared = 19;
ShareProject share_project = 20;
ShareProjectResponse share_project_response = 21;
UnshareProject unshare_project = 22;
JoinProject join_project = 23;
JoinProjectResponse join_project_response = 24;
LeaveProject leave_project = 25;
AddProjectCollaborator add_project_collaborator = 26;
RemoveProjectCollaborator remove_project_collaborator = 27;
GetDefinition get_definition = 20;
GetDefinitionResponse get_definition_response = 21;
GetTypeDefinition get_type_definition = 22;
GetTypeDefinitionResponse get_type_definition_response = 23;
GetReferences get_references = 24;
GetReferencesResponse get_references_response = 25;
GetDocumentHighlights get_document_highlights = 26;
GetDocumentHighlightsResponse get_document_highlights_response = 27;
GetProjectSymbols get_project_symbols = 28;
GetProjectSymbolsResponse get_project_symbols_response = 29;
OpenBufferForSymbol open_buffer_for_symbol = 30;
OpenBufferForSymbolResponse open_buffer_for_symbol_response = 31;
GetDefinition get_definition = 28;
GetDefinitionResponse get_definition_response = 29;
GetTypeDefinition get_type_definition = 30;
GetTypeDefinitionResponse get_type_definition_response = 31;
GetReferences get_references = 32;
GetReferencesResponse get_references_response = 33;
GetDocumentHighlights get_document_highlights = 34;
GetDocumentHighlightsResponse get_document_highlights_response = 35;
GetProjectSymbols get_project_symbols = 36;
GetProjectSymbolsResponse get_project_symbols_response = 37;
OpenBufferForSymbol open_buffer_for_symbol = 38;
OpenBufferForSymbolResponse open_buffer_for_symbol_response = 39;
UpdateProject update_project = 32;
RegisterProjectActivity register_project_activity = 33;
UpdateWorktree update_worktree = 34;
UpdateWorktreeExtensions update_worktree_extensions = 35;
UpdateProject update_project = 40;
RegisterProjectActivity register_project_activity = 41;
UpdateWorktree update_worktree = 42;
UpdateWorktreeExtensions update_worktree_extensions = 43;
CreateProjectEntry create_project_entry = 36;
RenameProjectEntry rename_project_entry = 37;
CopyProjectEntry copy_project_entry = 38;
DeleteProjectEntry delete_project_entry = 39;
ProjectEntryResponse project_entry_response = 40;
CreateProjectEntry create_project_entry = 44;
RenameProjectEntry rename_project_entry = 45;
CopyProjectEntry copy_project_entry = 46;
DeleteProjectEntry delete_project_entry = 47;
ProjectEntryResponse project_entry_response = 48;
UpdateDiagnosticSummary update_diagnostic_summary = 41;
StartLanguageServer start_language_server = 42;
UpdateLanguageServer update_language_server = 43;
UpdateDiagnosticSummary update_diagnostic_summary = 49;
StartLanguageServer start_language_server = 50;
UpdateLanguageServer update_language_server = 51;
OpenBufferById open_buffer_by_id = 44;
OpenBufferByPath open_buffer_by_path = 45;
OpenBufferResponse open_buffer_response = 46;
CreateBufferForPeer create_buffer_for_peer = 47;
UpdateBuffer update_buffer = 48;
UpdateBufferFile update_buffer_file = 49;
SaveBuffer save_buffer = 50;
BufferSaved buffer_saved = 51;
BufferReloaded buffer_reloaded = 52;
ReloadBuffers reload_buffers = 53;
ReloadBuffersResponse reload_buffers_response = 54;
FormatBuffers format_buffers = 55;
FormatBuffersResponse format_buffers_response = 56;
GetCompletions get_completions = 57;
GetCompletionsResponse get_completions_response = 58;
ApplyCompletionAdditionalEdits apply_completion_additional_edits = 59;
ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 60;
GetCodeActions get_code_actions = 61;
GetCodeActionsResponse get_code_actions_response = 62;
GetHover get_hover = 63;
GetHoverResponse get_hover_response = 64;
ApplyCodeAction apply_code_action = 65;
ApplyCodeActionResponse apply_code_action_response = 66;
PrepareRename prepare_rename = 67;
PrepareRenameResponse prepare_rename_response = 68;
PerformRename perform_rename = 69;
PerformRenameResponse perform_rename_response = 70;
SearchProject search_project = 71;
SearchProjectResponse search_project_response = 72;
OpenBufferById open_buffer_by_id = 52;
OpenBufferByPath open_buffer_by_path = 53;
OpenBufferResponse open_buffer_response = 54;
CreateBufferForPeer create_buffer_for_peer = 55;
UpdateBuffer update_buffer = 56;
UpdateBufferFile update_buffer_file = 57;
SaveBuffer save_buffer = 58;
BufferSaved buffer_saved = 59;
BufferReloaded buffer_reloaded = 60;
ReloadBuffers reload_buffers = 61;
ReloadBuffersResponse reload_buffers_response = 62;
FormatBuffers format_buffers = 63;
FormatBuffersResponse format_buffers_response = 64;
GetCompletions get_completions = 65;
GetCompletionsResponse get_completions_response = 66;
ApplyCompletionAdditionalEdits apply_completion_additional_edits = 67;
ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 68;
GetCodeActions get_code_actions = 69;
GetCodeActionsResponse get_code_actions_response = 70;
GetHover get_hover = 71;
GetHoverResponse get_hover_response = 72;
ApplyCodeAction apply_code_action = 73;
ApplyCodeActionResponse apply_code_action_response = 74;
PrepareRename prepare_rename = 75;
PrepareRenameResponse prepare_rename_response = 76;
PerformRename perform_rename = 77;
PerformRenameResponse perform_rename_response = 78;
SearchProject search_project = 79;
SearchProjectResponse search_project_response = 80;
GetChannels get_channels = 73;
GetChannelsResponse get_channels_response = 74;
JoinChannel join_channel = 75;
JoinChannelResponse join_channel_response = 76;
LeaveChannel leave_channel = 77;
SendChannelMessage send_channel_message = 78;
SendChannelMessageResponse send_channel_message_response = 79;
ChannelMessageSent channel_message_sent = 80;
GetChannelMessages get_channel_messages = 81;
GetChannelMessagesResponse get_channel_messages_response = 82;
GetChannels get_channels = 81;
GetChannelsResponse get_channels_response = 82;
JoinChannel join_channel = 83;
JoinChannelResponse join_channel_response = 84;
LeaveChannel leave_channel = 85;
SendChannelMessage send_channel_message = 86;
SendChannelMessageResponse send_channel_message_response = 87;
ChannelMessageSent channel_message_sent = 88;
GetChannelMessages get_channel_messages = 89;
GetChannelMessagesResponse get_channel_messages_response = 90;
UpdateContacts update_contacts = 83;
UpdateInviteInfo update_invite_info = 84;
ShowContacts show_contacts = 85;
UpdateContacts update_contacts = 91;
UpdateInviteInfo update_invite_info = 92;
ShowContacts show_contacts = 93;
GetUsers get_users = 86;
FuzzySearchUsers fuzzy_search_users = 87;
UsersResponse users_response = 88;
RequestContact request_contact = 89;
RespondToContactRequest respond_to_contact_request = 90;
RemoveContact remove_contact = 91;
GetUsers get_users = 94;
FuzzySearchUsers fuzzy_search_users = 95;
UsersResponse users_response = 96;
RequestContact request_contact = 97;
RespondToContactRequest respond_to_contact_request = 98;
RemoveContact remove_contact = 99;
Follow follow = 92;
FollowResponse follow_response = 93;
UpdateFollowers update_followers = 94;
Unfollow unfollow = 95;
GetPrivateUserInfo get_private_user_info = 96;
GetPrivateUserInfoResponse get_private_user_info_response = 97;
UpdateDiffBase update_diff_base = 98;
Follow follow = 100;
FollowResponse follow_response = 101;
UpdateFollowers update_followers = 102;
Unfollow unfollow = 103;
GetPrivateUserInfo get_private_user_info = 104;
GetPrivateUserInfoResponse get_private_user_info_response = 105;
UpdateDiffBase update_diff_base = 106;
}
}
@ -128,70 +137,121 @@ message Test {
uint64 id = 1;
}
message RegisterProject {
bool online = 1;
message CreateRoom {}
message CreateRoomResponse {
uint64 id = 1;
}
message RegisterProjectResponse {
message JoinRoom {
uint64 id = 1;
}
message JoinRoomResponse {
Room room = 1;
}
message LeaveRoom {
uint64 id = 1;
}
message Room {
repeated Participant participants = 1;
repeated uint64 pending_participant_user_ids = 2;
}
message Participant {
uint64 user_id = 1;
uint32 peer_id = 2;
repeated ParticipantProject projects = 3;
ParticipantLocation location = 4;
}
message ParticipantProject {
uint64 id = 1;
repeated string worktree_root_names = 2;
}
message ParticipantLocation {
oneof variant {
SharedProject shared_project = 1;
UnsharedProject unshared_project = 2;
External external = 3;
}
message SharedProject {
uint64 id = 1;
}
message UnsharedProject {}
message External {}
}
message Call {
uint64 room_id = 1;
uint64 recipient_user_id = 2;
optional uint64 initial_project_id = 3;
}
message IncomingCall {
uint64 room_id = 1;
uint64 caller_user_id = 2;
repeated uint64 participant_user_ids = 3;
optional ParticipantProject initial_project = 4;
}
message CallCanceled {}
message CancelCall {
uint64 room_id = 1;
uint64 recipient_user_id = 2;
}
message DeclineCall {
uint64 room_id = 1;
}
message UpdateParticipantLocation {
uint64 room_id = 1;
ParticipantLocation location = 2;
}
message RoomUpdated {
Room room = 1;
}
message ShareProject {
uint64 room_id = 1;
repeated WorktreeMetadata worktrees = 2;
}
message ShareProjectResponse {
uint64 project_id = 1;
}
message UnregisterProject {
message UnshareProject {
uint64 project_id = 1;
}
message UpdateProject {
uint64 project_id = 1;
repeated WorktreeMetadata worktrees = 2;
bool online = 3;
}
message RegisterProjectActivity {
uint64 project_id = 1;
}
message RequestJoinProject {
uint64 requester_id = 1;
uint64 project_id = 2;
}
message RespondToJoinProjectRequest {
uint64 requester_id = 1;
uint64 project_id = 2;
bool allow = 3;
}
message JoinProjectRequestCancelled {
uint64 requester_id = 1;
uint64 project_id = 2;
}
message JoinProject {
uint64 project_id = 1;
}
message JoinProjectResponse {
oneof variant {
Accept accept = 1;
Decline decline = 2;
}
message Accept {
uint32 replica_id = 1;
repeated WorktreeMetadata worktrees = 2;
repeated Collaborator collaborators = 3;
repeated LanguageServer language_servers = 4;
}
message Decline {
Reason reason = 1;
enum Reason {
Declined = 0;
Closed = 1;
WentOffline = 2;
}
}
uint32 replica_id = 1;
repeated WorktreeMetadata worktrees = 2;
repeated Collaborator collaborators = 3;
repeated LanguageServer language_servers = 4;
}
message LeaveProject {
@ -254,10 +314,6 @@ message RemoveProjectCollaborator {
uint32 peer_id = 2;
}
message ProjectUnshared {
uint64 project_id = 1;
}
message GetDefinition {
uint64 project_id = 1;
uint64 buffer_id = 2;
@ -986,17 +1042,11 @@ message ChannelMessage {
message Contact {
uint64 user_id = 1;
repeated ProjectMetadata projects = 2;
bool online = 3;
bool online = 2;
bool busy = 3;
bool should_notify = 4;
}
message ProjectMetadata {
uint64 id = 1;
repeated string visible_worktree_root_names = 3;
repeated uint64 guests = 4;
}
message WorktreeMetadata {
uint64 id = 1;
string root_name = 2;

View file

@ -33,7 +33,7 @@ impl fmt::Display for ConnectionId {
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct PeerId(pub u32);
impl fmt::Display for PeerId {
@ -394,7 +394,11 @@ impl Peer {
send?;
let (response, _barrier) = rx.await.map_err(|_| anyhow!("connection was closed"))?;
if let Some(proto::envelope::Payload::Error(error)) = &response.payload {
Err(anyhow!("RPC request failed - {}", error.message))
Err(anyhow!(
"RPC request {} failed - {}",
T::NAME,
error.message
))
} else {
T::Response::from_envelope(response)
.ok_or_else(|| anyhow!("received response of the wrong type"))

View file

@ -83,11 +83,16 @@ messages!(
(ApplyCompletionAdditionalEditsResponse, Background),
(BufferReloaded, Foreground),
(BufferSaved, Foreground),
(RemoveContact, Foreground),
(Call, Foreground),
(CallCanceled, Foreground),
(CancelCall, Foreground),
(ChannelMessageSent, Foreground),
(CopyProjectEntry, Foreground),
(CreateBufferForPeer, Foreground),
(CreateProjectEntry, Foreground),
(CreateRoom, Foreground),
(CreateRoomResponse, Foreground),
(DeclineCall, Foreground),
(DeleteProjectEntry, Foreground),
(Error, Foreground),
(Follow, Foreground),
@ -116,14 +121,17 @@ messages!(
(GetProjectSymbols, Background),
(GetProjectSymbolsResponse, Background),
(GetUsers, Foreground),
(IncomingCall, Foreground),
(UsersResponse, Foreground),
(JoinChannel, Foreground),
(JoinChannelResponse, Foreground),
(JoinProject, Foreground),
(JoinProjectResponse, Foreground),
(JoinProjectRequestCancelled, Foreground),
(JoinRoom, Foreground),
(JoinRoomResponse, Foreground),
(LeaveChannel, Foreground),
(LeaveProject, Foreground),
(LeaveRoom, Foreground),
(OpenBufferById, Background),
(OpenBufferByPath, Background),
(OpenBufferForSymbol, Background),
@ -134,29 +142,28 @@ messages!(
(PrepareRename, Background),
(PrepareRenameResponse, Background),
(ProjectEntryResponse, Foreground),
(ProjectUnshared, Foreground),
(RegisterProjectResponse, Foreground),
(RemoveContact, Foreground),
(Ping, Foreground),
(RegisterProject, Foreground),
(RegisterProjectActivity, Foreground),
(ReloadBuffers, Foreground),
(ReloadBuffersResponse, Foreground),
(RemoveProjectCollaborator, Foreground),
(RenameProjectEntry, Foreground),
(RequestContact, Foreground),
(RequestJoinProject, Foreground),
(RespondToContactRequest, Foreground),
(RespondToJoinProjectRequest, Foreground),
(RoomUpdated, Foreground),
(SaveBuffer, Foreground),
(SearchProject, Background),
(SearchProjectResponse, Background),
(SendChannelMessage, Foreground),
(SendChannelMessageResponse, Foreground),
(ShareProject, Foreground),
(ShareProjectResponse, Foreground),
(ShowContacts, Foreground),
(StartLanguageServer, Foreground),
(Test, Foreground),
(Unfollow, Foreground),
(UnregisterProject, Foreground),
(UnshareProject, Foreground),
(UpdateBuffer, Foreground),
(UpdateBufferFile, Foreground),
(UpdateContacts, Foreground),
@ -164,6 +171,7 @@ messages!(
(UpdateFollowers, Foreground),
(UpdateInviteInfo, Foreground),
(UpdateLanguageServer, Foreground),
(UpdateParticipantLocation, Foreground),
(UpdateProject, Foreground),
(UpdateWorktree, Foreground),
(UpdateWorktreeExtensions, Background),
@ -178,8 +186,12 @@ request_messages!(
ApplyCompletionAdditionalEdits,
ApplyCompletionAdditionalEditsResponse
),
(Call, Ack),
(CancelCall, Ack),
(CopyProjectEntry, ProjectEntryResponse),
(CreateProjectEntry, ProjectEntryResponse),
(CreateRoom, CreateRoomResponse),
(DeclineCall, Ack),
(DeleteProjectEntry, ProjectEntryResponse),
(Follow, FollowResponse),
(FormatBuffers, FormatBuffersResponse),
@ -198,13 +210,14 @@ request_messages!(
(GetUsers, UsersResponse),
(JoinChannel, JoinChannelResponse),
(JoinProject, JoinProjectResponse),
(JoinRoom, JoinRoomResponse),
(IncomingCall, Ack),
(OpenBufferById, OpenBufferResponse),
(OpenBufferByPath, OpenBufferResponse),
(OpenBufferForSymbol, OpenBufferForSymbolResponse),
(Ping, Ack),
(PerformRename, PerformRenameResponse),
(PrepareRename, PrepareRenameResponse),
(RegisterProject, RegisterProjectResponse),
(ReloadBuffers, ReloadBuffersResponse),
(RequestContact, Ack),
(RemoveContact, Ack),
@ -213,9 +226,10 @@ request_messages!(
(SaveBuffer, BufferSaved),
(SearchProject, SearchProjectResponse),
(SendChannelMessage, SendChannelMessageResponse),
(ShareProject, ShareProjectResponse),
(Test, Test),
(UnregisterProject, Ack),
(UpdateBuffer, Ack),
(UpdateParticipantLocation, Ack),
(UpdateWorktree, Ack),
);
@ -241,24 +255,21 @@ entity_messages!(
GetReferences,
GetProjectSymbols,
JoinProject,
JoinProjectRequestCancelled,
LeaveProject,
OpenBufferById,
OpenBufferByPath,
OpenBufferForSymbol,
PerformRename,
PrepareRename,
ProjectUnshared,
RegisterProjectActivity,
ReloadBuffers,
RemoveProjectCollaborator,
RenameProjectEntry,
RequestJoinProject,
SaveBuffer,
SearchProject,
StartLanguageServer,
Unfollow,
UnregisterProject,
UnshareProject,
UpdateBuffer,
UpdateBufferFile,
UpdateDiagnosticSummary,

View file

@ -6,4 +6,4 @@ pub use conn::Connection;
pub use peer::*;
mod macros;
pub const PROTOCOL_VERSION: u32 = 34;
pub const PROTOCOL_VERSION: u32 = 35;

View file

@ -726,6 +726,8 @@ impl Element for TerminalElement {
layout: &mut Self::LayoutState,
cx: &mut gpui::PaintContext,
) -> Self::PaintState {
let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
//Setup element stuff
let clip_bounds = Some(visible_bounds);

View file

@ -20,7 +20,7 @@ pub struct Theme {
pub context_menu: ContextMenu,
pub chat_panel: ChatPanel,
pub contacts_popover: ContactsPopover,
pub contacts_panel: ContactsPanel,
pub contact_list: ContactList,
pub contact_finder: ContactFinder,
pub project_panel: ProjectPanel,
pub command_palette: CommandPalette,
@ -31,6 +31,8 @@ pub struct Theme {
pub breadcrumbs: ContainedText,
pub contact_notification: ContactNotification,
pub update_notification: UpdateNotification,
pub project_shared_notification: ProjectSharedNotification,
pub incoming_call_notification: IncomingCallNotification,
pub tooltip: TooltipStyle,
pub terminal: TerminalStyle,
}
@ -58,6 +60,7 @@ pub struct Workspace {
pub notifications: Notifications,
pub joining_project_avatar: ImageStyle,
pub joining_project_message: ContainedText,
pub external_location_message: ContainedText,
pub dock: Dock,
}
@ -72,8 +75,67 @@ pub struct Titlebar {
pub avatar_ribbon: AvatarRibbon,
pub offline_icon: OfflineIcon,
pub avatar: ImageStyle,
pub inactive_avatar: ImageStyle,
pub sign_in_prompt: Interactive<ContainedText>,
pub outdated_warning: ContainedText,
pub share_button: Interactive<ContainedText>,
pub toggle_contacts_button: Interactive<IconButton>,
pub toggle_contacts_badge: ContainerStyle,
}
#[derive(Deserialize, Default)]
pub struct ContactsPopover {
#[serde(flatten)]
pub container: ContainerStyle,
pub height: f32,
pub width: f32,
pub invite_row_height: f32,
pub invite_row: Interactive<ContainedLabel>,
}
#[derive(Deserialize, Default)]
pub struct ContactList {
pub user_query_editor: FieldEditor,
pub user_query_editor_height: f32,
pub add_contact_button: IconButton,
pub header_row: Interactive<ContainedText>,
pub leave_call: Interactive<ContainedText>,
pub contact_row: Interactive<ContainerStyle>,
pub row_height: f32,
pub project_row: Interactive<ProjectRow>,
pub tree_branch: Interactive<TreeBranch>,
pub contact_avatar: ImageStyle,
pub contact_status_free: ContainerStyle,
pub contact_status_busy: ContainerStyle,
pub contact_username: ContainedText,
pub contact_button: Interactive<IconButton>,
pub contact_button_spacing: f32,
pub disabled_button: IconButton,
pub section_icon_size: f32,
pub calling_indicator: ContainedText,
}
#[derive(Deserialize, Default)]
pub struct ProjectRow {
#[serde(flatten)]
pub container: ContainerStyle,
pub name: ContainedText,
}
#[derive(Deserialize, Default, Clone, Copy)]
pub struct TreeBranch {
pub width: f32,
pub color: Color,
}
#[derive(Deserialize, Default)]
pub struct ContactFinder {
pub picker: Picker,
pub row_height: f32,
pub contact_avatar: ImageStyle,
pub contact_username: ContainerStyle,
pub contact_button: IconButton,
pub disabled_contact_button: IconButton,
}
#[derive(Clone, Deserialize, Default)]
@ -315,33 +377,6 @@ pub struct CommandPalette {
pub keystroke_spacing: f32,
}
#[derive(Deserialize, Default)]
pub struct ContactsPopover {
pub background: Color,
}
#[derive(Deserialize, Default)]
pub struct ContactsPanel {
#[serde(flatten)]
pub container: ContainerStyle,
pub user_query_editor: FieldEditor,
pub user_query_editor_height: f32,
pub add_contact_button: IconButton,
pub header_row: Interactive<ContainedText>,
pub contact_row: Interactive<ContainerStyle>,
pub project_row: Interactive<ProjectRow>,
pub row_height: f32,
pub contact_avatar: ImageStyle,
pub contact_username: ContainedText,
pub contact_button: Interactive<IconButton>,
pub contact_button_spacing: f32,
pub disabled_button: IconButton,
pub tree_branch: Interactive<TreeBranch>,
pub private_button: Interactive<IconButton>,
pub section_icon_size: f32,
pub invite_row: Interactive<ContainedLabel>,
}
#[derive(Deserialize, Default)]
pub struct InviteLink {
#[serde(flatten)]
@ -351,21 +386,6 @@ pub struct InviteLink {
pub icon: Icon,
}
#[derive(Deserialize, Default, Clone, Copy)]
pub struct TreeBranch {
pub width: f32,
pub color: Color,
}
#[derive(Deserialize, Default)]
pub struct ContactFinder {
pub row_height: f32,
pub contact_avatar: ImageStyle,
pub contact_username: ContainerStyle,
pub contact_button: IconButton,
pub disabled_contact_button: IconButton,
}
#[derive(Deserialize, Default)]
pub struct Icon {
#[serde(flatten)]
@ -384,16 +404,6 @@ pub struct IconButton {
pub button_width: f32,
}
#[derive(Deserialize, Default)]
pub struct ProjectRow {
#[serde(flatten)]
pub container: ContainerStyle,
pub name: ContainedText,
pub guests: ContainerStyle,
pub guest_avatar: ImageStyle,
pub guest_avatar_spacing: f32,
}
#[derive(Deserialize, Default)]
pub struct ChatMessage {
#[serde(flatten)]
@ -475,6 +485,40 @@ pub struct UpdateNotification {
pub dismiss_button: Interactive<IconButton>,
}
#[derive(Deserialize, Default)]
pub struct ProjectSharedNotification {
pub window_height: f32,
pub window_width: f32,
#[serde(default)]
pub background: Color,
pub owner_container: ContainerStyle,
pub owner_avatar: ImageStyle,
pub owner_metadata: ContainerStyle,
pub owner_username: ContainedText,
pub message: ContainedText,
pub worktree_roots: ContainedText,
pub button_width: f32,
pub open_button: ContainedText,
pub dismiss_button: ContainedText,
}
#[derive(Deserialize, Default)]
pub struct IncomingCallNotification {
pub window_height: f32,
pub window_width: f32,
#[serde(default)]
pub background: Color,
pub caller_container: ContainerStyle,
pub caller_avatar: ImageStyle,
pub caller_metadata: ContainerStyle,
pub caller_username: ContainedText,
pub caller_message: ContainedText,
pub worktree_roots: ContainedText,
pub button_width: f32,
pub accept_button: ContainedText,
pub decline_button: ContainedText,
}
#[derive(Clone, Deserialize, Default)]
pub struct Editor {
pub text_color: Color,

View file

@ -8,11 +8,16 @@ path = "src/workspace.rs"
doctest = false
[features]
test-support = ["client/test-support", "project/test-support", "settings/test-support"]
test-support = [
"call/test-support",
"client/test-support",
"project/test-support",
"settings/test-support"
]
[dependencies]
call = { path = "../call" }
client = { path = "../client" }
clock = { path = "../clock" }
collections = { path = "../collections" }
context_menu = { path = "../context_menu" }
drag_and_drop = { path = "../drag_and_drop" }
@ -33,6 +38,7 @@ serde_json = { version = "1.0", features = ["preserve_order"] }
smallvec = { version = "1.6", features = ["union"] }
[dev-dependencies]
call = { path = "../call", features = ["test-support"] }
client = { path = "../client", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }

View file

@ -1,9 +1,10 @@
use crate::{FollowerStatesByLeader, Pane};
use crate::{FollowerStatesByLeader, JoinProject, Pane, Workspace};
use anyhow::{anyhow, Result};
use client::PeerId;
use collections::HashMap;
use gpui::{elements::*, Axis, Border, ViewHandle};
use project::Collaborator;
use call::ActiveCall;
use gpui::{
elements::*, Axis, Border, CursorStyle, ModelHandle, MouseButton, RenderContext, ViewHandle,
};
use project::Project;
use serde::Deserialize;
use theme::Theme;
@ -56,11 +57,14 @@ impl PaneGroup {
pub(crate) fn render(
&self,
project: &ModelHandle<Project>,
theme: &Theme,
follower_states: &FollowerStatesByLeader,
collaborators: &HashMap<PeerId, Collaborator>,
active_call: Option<&ModelHandle<ActiveCall>>,
cx: &mut RenderContext<Workspace>,
) -> ElementBox {
self.root.render(theme, follower_states, collaborators)
self.root
.render(project, theme, follower_states, active_call, cx)
}
pub(crate) fn panes(&self) -> Vec<&ViewHandle<Pane>> {
@ -100,13 +104,16 @@ impl Member {
pub fn render(
&self,
project: &ModelHandle<Project>,
theme: &Theme,
follower_states: &FollowerStatesByLeader,
collaborators: &HashMap<PeerId, Collaborator>,
active_call: Option<&ModelHandle<ActiveCall>>,
cx: &mut RenderContext<Workspace>,
) -> ElementBox {
enum FollowIntoExternalProject {}
match self {
Member::Pane(pane) => {
let mut border = Border::default();
let leader = follower_states
.iter()
.find_map(|(leader_id, follower_states)| {
@ -116,21 +123,110 @@ impl Member {
None
}
})
.and_then(|leader_id| collaborators.get(leader_id));
if let Some(leader) = leader {
let leader_color = theme
.editor
.replica_selection_style(leader.replica_id)
.cursor;
.and_then(|leader_id| {
let room = active_call?.read(cx).room()?.read(cx);
let collaborator = project.read(cx).collaborators().get(leader_id)?;
let participant = room.remote_participants().get(&leader_id)?;
Some((collaborator.replica_id, participant))
});
let mut border = Border::default();
let prompt = if let Some((replica_id, leader)) = leader {
let leader_color = theme.editor.replica_selection_style(replica_id).cursor;
border = Border::all(theme.workspace.leader_border_width, leader_color);
border
.color
.fade_out(1. - theme.workspace.leader_border_opacity);
border.overlay = true;
}
ChildView::new(pane).contained().with_border(border).boxed()
match leader.location {
call::ParticipantLocation::SharedProject {
project_id: leader_project_id,
} => {
if Some(leader_project_id) == project.read(cx).remote_id() {
None
} else {
let leader_user = leader.user.clone();
let leader_user_id = leader.user.id;
Some(
MouseEventHandler::<FollowIntoExternalProject>::new(
pane.id(),
cx,
|_, _| {
Label::new(
format!(
"Follow {} on their active project",
leader_user.github_login,
),
theme
.workspace
.external_location_message
.text
.clone(),
)
.contained()
.with_style(
theme.workspace.external_location_message.container,
)
.boxed()
},
)
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(JoinProject {
project_id: leader_project_id,
follow_user_id: leader_user_id,
})
})
.aligned()
.bottom()
.right()
.boxed(),
)
}
}
call::ParticipantLocation::UnsharedProject => Some(
Label::new(
format!(
"{} is viewing an unshared Zed project",
leader.user.github_login
),
theme.workspace.external_location_message.text.clone(),
)
.contained()
.with_style(theme.workspace.external_location_message.container)
.aligned()
.bottom()
.right()
.boxed(),
),
call::ParticipantLocation::External => Some(
Label::new(
format!(
"{} is viewing a window outside of Zed",
leader.user.github_login
),
theme.workspace.external_location_message.text.clone(),
)
.contained()
.with_style(theme.workspace.external_location_message.container)
.aligned()
.bottom()
.right()
.boxed(),
),
}
} else {
None
};
Stack::new()
.with_child(ChildView::new(pane).contained().with_border(border).boxed())
.with_children(prompt)
.boxed()
}
Member::Axis(axis) => axis.render(theme, follower_states, collaborators),
Member::Axis(axis) => axis.render(project, theme, follower_states, active_call, cx),
}
}
@ -232,14 +328,16 @@ impl PaneAxis {
fn render(
&self,
project: &ModelHandle<Project>,
theme: &Theme,
follower_state: &FollowerStatesByLeader,
collaborators: &HashMap<PeerId, Collaborator>,
active_call: Option<&ModelHandle<ActiveCall>>,
cx: &mut RenderContext<Workspace>,
) -> ElementBox {
let last_member_ix = self.members.len() - 1;
Flex::new(self.axis)
.with_children(self.members.iter().enumerate().map(|(ix, member)| {
let mut member = member.render(theme, follower_state, collaborators);
let mut member = member.render(project, theme, follower_state, active_call, cx);
if ix < last_member_ix {
let mut border = theme.workspace.pane_divider;
border.left = false;

View file

@ -1,185 +0,0 @@
use crate::{sidebar::SidebarSide, AppState, ToggleFollow, Workspace};
use anyhow::Result;
use client::{proto, Client, Contact};
use gpui::{
elements::*, ElementBox, Entity, ImageData, MutableAppContext, RenderContext, Task, View,
ViewContext,
};
use project::Project;
use settings::Settings;
use std::sync::Arc;
use util::ResultExt;
pub struct WaitingRoom {
project_id: u64,
avatar: Option<Arc<ImageData>>,
message: String,
waiting: bool,
client: Arc<Client>,
_join_task: Task<Result<()>>,
}
impl Entity for WaitingRoom {
type Event = ();
fn release(&mut self, _: &mut MutableAppContext) {
if self.waiting {
self.client
.send(proto::LeaveProject {
project_id: self.project_id,
})
.log_err();
}
}
}
impl View for WaitingRoom {
fn ui_name() -> &'static str {
"WaitingRoom"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = &cx.global::<Settings>().theme.workspace;
Flex::column()
.with_children(self.avatar.clone().map(|avatar| {
Image::new(avatar)
.with_style(theme.joining_project_avatar)
.aligned()
.boxed()
}))
.with_child(
Text::new(
self.message.clone(),
theme.joining_project_message.text.clone(),
)
.contained()
.with_style(theme.joining_project_message.container)
.aligned()
.boxed(),
)
.aligned()
.contained()
.with_background_color(theme.background)
.boxed()
}
}
impl WaitingRoom {
pub fn new(
contact: Arc<Contact>,
project_index: usize,
app_state: Arc<AppState>,
cx: &mut ViewContext<Self>,
) -> Self {
let project_id = contact.projects[project_index].id;
let client = app_state.client.clone();
let _join_task = cx.spawn_weak({
let contact = contact.clone();
|this, mut cx| async move {
let project = Project::remote(
project_id,
app_state.client.clone(),
app_state.user_store.clone(),
app_state.project_store.clone(),
app_state.languages.clone(),
app_state.fs.clone(),
cx.clone(),
)
.await;
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| {
this.waiting = false;
match project {
Ok(project) => {
cx.replace_root_view(|cx| {
let mut workspace =
Workspace::new(project, app_state.default_item_factory, cx);
(app_state.initialize_workspace)(
&mut workspace,
&app_state,
cx,
);
workspace.toggle_sidebar(SidebarSide::Left, cx);
if let Some((host_peer_id, _)) = workspace
.project
.read(cx)
.collaborators()
.iter()
.find(|(_, collaborator)| collaborator.replica_id == 0)
{
if let Some(follow) = workspace
.toggle_follow(&ToggleFollow(*host_peer_id), cx)
{
follow.detach_and_log_err(cx);
}
}
workspace
});
}
Err(error) => {
let login = &contact.user.github_login;
let message = match error {
project::JoinProjectError::HostDeclined => {
format!("@{} declined your request.", login)
}
project::JoinProjectError::HostClosedProject => {
format!(
"@{} closed their copy of {}.",
login,
humanize_list(
&contact.projects[project_index]
.visible_worktree_root_names
)
)
}
project::JoinProjectError::HostWentOffline => {
format!("@{} went offline.", login)
}
project::JoinProjectError::Other(error) => {
log::error!("error joining project: {}", error);
"An error occurred.".to_string()
}
};
this.message = message;
cx.notify();
}
}
})
}
Ok(())
}
});
Self {
project_id,
avatar: contact.user.avatar.clone(),
message: format!(
"Asking to join @{}'s copy of {}...",
contact.user.github_login,
humanize_list(&contact.projects[project_index].visible_worktree_root_names)
),
waiting: true,
client,
_join_task,
}
}
}
fn humanize_list<'a>(items: impl IntoIterator<Item = &'a String>) -> String {
let mut list = String::new();
let mut items = items.into_iter().enumerate().peekable();
while let Some((ix, item)) = items.next() {
if ix > 0 {
list.push_str(", ");
if items.peek().is_none() {
list.push_str("and ");
}
}
list.push_str(item);
}
list
}

View file

@ -10,28 +10,22 @@ pub mod searchable;
pub mod sidebar;
mod status_bar;
mod toolbar;
mod waiting_room;
use anyhow::{anyhow, Context, Result};
use client::{
proto, Authenticate, Client, Contact, PeerId, Subscription, TypedEnvelope, User, UserStore,
};
use clock::ReplicaId;
use call::ActiveCall;
use client::{proto, Client, PeerId, TypedEnvelope, UserStore};
use collections::{hash_map, HashMap, HashSet};
use dock::{DefaultItemFactory, Dock, ToggleDockButton};
use drag_and_drop::DragAndDrop;
use futures::{channel::oneshot, FutureExt};
use futures::{channel::oneshot, FutureExt, StreamExt};
use gpui::{
actions,
color::Color,
elements::*,
geometry::{rect::RectF, vector::vec2f, PathBuilder},
impl_actions, impl_internal_actions,
json::{self, ToJson},
platform::{CursorStyle, WindowOptions},
AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Border, Entity, ImageData,
ModelContext, ModelHandle, MouseButton, MutableAppContext, PathPromptOptions, PromptLevel,
RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle,
AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
MouseButton, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View,
ViewContext, ViewHandle, WeakViewHandle,
};
use language::LanguageRegistry;
use log::{error, warn};
@ -52,7 +46,6 @@ use std::{
cell::RefCell,
fmt,
future::Future,
ops::Range,
path::{Path, PathBuf},
rc::Rc,
sync::{
@ -64,7 +57,6 @@ use std::{
use theme::{Theme, ThemeRegistry};
pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
use util::ResultExt;
use waiting_room::WaitingRoom;
type ProjectItemBuilders = HashMap<
TypeId,
@ -115,12 +107,6 @@ pub struct OpenPaths {
pub paths: Vec<PathBuf>,
}
#[derive(Clone, Deserialize, PartialEq)]
pub struct ToggleProjectOnline {
#[serde(skip_deserializing)]
pub project: Option<ModelHandle<Project>>,
}
#[derive(Clone, Deserialize, PartialEq)]
pub struct ActivatePane(pub usize);
@ -129,8 +115,8 @@ pub struct ToggleFollow(pub PeerId);
#[derive(Clone, PartialEq)]
pub struct JoinProject {
pub contact: Arc<Contact>,
pub project_index: usize,
pub project_id: u64,
pub follow_user_id: u64,
}
impl_internal_actions!(
@ -142,7 +128,7 @@ impl_internal_actions!(
RemoveWorktreeFromProject
]
);
impl_actions!(workspace, [ToggleProjectOnline, ActivatePane]);
impl_actions!(workspace, [ActivatePane]);
pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
pane::init(cx);
@ -173,14 +159,6 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
}
}
});
cx.add_global_action({
let app_state = Arc::downgrade(&app_state);
move |action: &JoinProject, cx: &mut MutableAppContext| {
if let Some(app_state) = app_state.upgrade() {
join_project(action.contact.clone(), action.project_index, &app_state, cx);
}
}
});
cx.add_async_action(Workspace::toggle_follow);
cx.add_async_action(Workspace::follow_next_collaborator);
@ -188,7 +166,6 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
cx.add_async_action(Workspace::save_all);
cx.add_action(Workspace::add_folder_to_project);
cx.add_action(Workspace::remove_folder_from_project);
cx.add_action(Workspace::toggle_project_online);
cx.add_action(
|workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
let pane = workspace.active_pane().clone();
@ -957,7 +934,7 @@ impl AppState {
let languages = Arc::new(LanguageRegistry::test());
let http_client = client::test::FakeHttpClient::with_404_response();
let client = Client::new(http_client.clone(), cx);
let project_store = cx.add_model(|_| ProjectStore::new(project::Db::open_fake()));
let project_store = cx.add_model(|_| ProjectStore::new());
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
let themes = ThemeRegistry::new((), cx.font_cache().clone());
Arc::new(Self {
@ -984,7 +961,7 @@ pub struct Workspace {
weak_self: WeakViewHandle<Self>,
client: Arc<Client>,
user_store: ModelHandle<client::UserStore>,
remote_entity_subscription: Option<Subscription>,
remote_entity_subscription: Option<client::Subscription>,
fs: Arc<dyn Fs>,
modal: Option<AnyViewHandle>,
center: PaneGroup,
@ -995,6 +972,7 @@ pub struct Workspace {
active_pane: ViewHandle<Pane>,
last_active_center_pane: Option<ViewHandle<Pane>>,
status_bar: ViewHandle<StatusBar>,
titlebar_item: Option<AnyViewHandle>,
dock: Dock,
notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
project: ModelHandle<Project>,
@ -1002,7 +980,9 @@ pub struct Workspace {
follower_states_by_leader: FollowerStatesByLeader,
last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
window_edited: bool,
active_call: Option<ModelHandle<ActiveCall>>,
_observe_current_user: Task<()>,
_active_call_observation: Option<gpui::Subscription>,
}
#[derive(Default)]
@ -1111,6 +1091,14 @@ impl Workspace {
drag_and_drop.register_container(weak_handle.clone());
});
let mut active_call = None;
let mut active_call_observation = None;
if cx.has_global::<ModelHandle<ActiveCall>>() {
let call = cx.global::<ModelHandle<ActiveCall>>().clone();
active_call_observation = Some(cx.observe(&call, |_, _, cx| cx.notify()));
active_call = Some(call);
}
let mut this = Workspace {
modal: None,
weak_self: weak_handle,
@ -1124,6 +1112,7 @@ impl Workspace {
active_pane: center_pane.clone(),
last_active_center_pane: Some(center_pane.clone()),
status_bar,
titlebar_item: None,
notifications: Default::default(),
client,
remote_entity_subscription: None,
@ -1136,7 +1125,9 @@ impl Workspace {
follower_states_by_leader: Default::default(),
last_leaders_by_pane: Default::default(),
window_edited: false,
active_call,
_observe_current_user,
_active_call_observation: active_call_observation,
};
this.project_remote_id_changed(this.project.read(cx).remote_id(), cx);
cx.defer(|this, cx| this.update_window_title(cx));
@ -1168,6 +1159,19 @@ impl Workspace {
&self.project
}
pub fn client(&self) -> &Arc<Client> {
&self.client
}
pub fn set_titlebar_item(
&mut self,
item: impl Into<AnyViewHandle>,
cx: &mut ViewContext<Self>,
) {
self.titlebar_item = Some(item.into());
cx.notify();
}
/// Call the given callback with a workspace whose project is local.
///
/// If the given workspace has a local project, then it will be passed
@ -1188,7 +1192,6 @@ impl Workspace {
let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
let mut workspace = Workspace::new(
Project::local(
false,
app_state.client.clone(),
app_state.user_store.clone(),
app_state.project_store.clone(),
@ -1238,7 +1241,7 @@ impl Workspace {
_: &CloseWindow,
cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
let prepare = self.prepare_to_close(cx);
let prepare = self.prepare_to_close(false, cx);
Some(cx.spawn(|this, mut cx| async move {
if prepare.await? {
this.update(&mut cx, |_, cx| {
@ -1250,8 +1253,44 @@ impl Workspace {
}))
}
pub fn prepare_to_close(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<bool>> {
self.save_all_internal(true, cx)
pub fn prepare_to_close(
&mut self,
quitting: bool,
cx: &mut ViewContext<Self>,
) -> Task<Result<bool>> {
let active_call = self.active_call.clone();
let window_id = cx.window_id();
let workspace_count = cx
.window_ids()
.flat_map(|window_id| cx.root_view::<Workspace>(window_id))
.count();
cx.spawn(|this, mut cx| async move {
if let Some(active_call) = active_call {
if !quitting
&& workspace_count == 1
&& active_call.read_with(&cx, |call, _| call.room().is_some())
{
let answer = cx
.prompt(
window_id,
PromptLevel::Warning,
"Do you want to leave the current call?",
&["Close window and hang up", "Cancel"],
)
.next()
.await;
if answer == Some(1) {
return anyhow::Ok(false);
} else {
active_call.update(&mut cx, |call, cx| call.hang_up(cx))?;
}
}
}
Ok(this
.update(&mut cx, |this, cx| this.save_all_internal(true, cx))
.await?)
})
}
fn save_all(&mut self, _: &SaveAll, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
@ -1393,17 +1432,6 @@ impl Workspace {
.update(cx, |project, cx| project.remove_worktree(*worktree_id, cx));
}
fn toggle_project_online(&mut self, action: &ToggleProjectOnline, cx: &mut ViewContext<Self>) {
let project = action
.project
.clone()
.unwrap_or_else(|| self.project.clone());
project.update(cx, |project, cx| {
let public = !project.is_online();
project.set_online(public, cx);
});
}
fn project_path_for_path(
&self,
abs_path: &Path,
@ -2068,46 +2096,12 @@ impl Workspace {
None
}
fn render_connection_status(&self, cx: &mut RenderContext<Self>) -> Option<ElementBox> {
let theme = &cx.global::<Settings>().theme;
match &*self.client.status().borrow() {
client::Status::ConnectionError
| client::Status::ConnectionLost
| client::Status::Reauthenticating { .. }
| client::Status::Reconnecting { .. }
| client::Status::ReconnectionError { .. } => Some(
Container::new(
Align::new(
ConstrainedBox::new(
Svg::new("icons/cloud_slash_12.svg")
.with_color(theme.workspace.titlebar.offline_icon.color)
.boxed(),
)
.with_width(theme.workspace.titlebar.offline_icon.width)
.boxed(),
)
.boxed(),
)
.with_style(theme.workspace.titlebar.offline_icon.container)
.boxed(),
),
client::Status::UpgradeRequired => Some(
Label::new(
"Please update Zed to collaborate".to_string(),
theme.workspace.titlebar.outdated_warning.text.clone(),
)
.contained()
.with_style(theme.workspace.titlebar.outdated_warning.container)
.aligned()
.boxed(),
),
_ => None,
}
pub fn is_following(&self, peer_id: PeerId) -> bool {
self.follower_states_by_leader.contains_key(&peer_id)
}
fn render_titlebar(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
let project = &self.project.read(cx);
let replica_id = project.replica_id();
let mut worktree_root_names = String::new();
for (i, name) in project.worktree_root_names(cx).enumerate() {
if i > 0 {
@ -2129,7 +2123,7 @@ impl Workspace {
enum TitleBar {}
ConstrainedBox::new(
MouseEventHandler::<TitleBar>::new(0, cx, |_, cx| {
MouseEventHandler::<TitleBar>::new(0, cx, |_, _| {
Container::new(
Stack::new()
.with_child(
@ -2138,21 +2132,10 @@ impl Workspace {
.left()
.boxed(),
)
.with_child(
Align::new(
Flex::row()
.with_children(self.render_collaborators(theme, cx))
.with_children(self.render_current_user(
self.user_store.read(cx).current_user().as_ref(),
replica_id,
theme,
cx,
))
.with_children(self.render_connection_status(cx))
.boxed(),
)
.right()
.boxed(),
.with_children(
self.titlebar_item
.as_ref()
.map(|item| ChildView::new(item).aligned().right().boxed()),
)
.boxed(),
)
@ -2221,125 +2204,6 @@ impl Workspace {
}
}
fn render_collaborators(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> Vec<ElementBox> {
let mut collaborators = self
.project
.read(cx)
.collaborators()
.values()
.cloned()
.collect::<Vec<_>>();
collaborators.sort_unstable_by_key(|collaborator| collaborator.replica_id);
collaborators
.into_iter()
.filter_map(|collaborator| {
Some(self.render_avatar(
collaborator.user.avatar.clone()?,
collaborator.replica_id,
Some((collaborator.peer_id, &collaborator.user.github_login)),
theme,
cx,
))
})
.collect()
}
fn render_current_user(
&self,
user: Option<&Arc<User>>,
replica_id: ReplicaId,
theme: &Theme,
cx: &mut RenderContext<Self>,
) -> Option<ElementBox> {
let status = *self.client.status().borrow();
if let Some(avatar) = user.and_then(|user| user.avatar.clone()) {
Some(self.render_avatar(avatar, replica_id, None, 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".to_string(), 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(),
)
}
}
fn render_avatar(
&self,
avatar: Arc<ImageData>,
replica_id: ReplicaId,
peer: Option<(PeerId, &str)>,
theme: &Theme,
cx: &mut RenderContext<Self>,
) -> ElementBox {
let replica_color = theme.editor.replica_selection_style(replica_id).cursor;
let is_followed = peer.map_or(false, |(peer_id, _)| {
self.follower_states_by_leader.contains_key(&peer_id)
});
let mut avatar_style = theme.workspace.titlebar.avatar;
if is_followed {
avatar_style.border = Border::all(1.0, replica_color);
}
let content = Stack::new()
.with_child(
Image::new(avatar)
.with_style(avatar_style)
.constrained()
.with_width(theme.workspace.titlebar.avatar_width)
.aligned()
.boxed(),
)
.with_child(
AvatarRibbon::new(replica_color)
.constrained()
.with_width(theme.workspace.titlebar.avatar_ribbon.width)
.with_height(theme.workspace.titlebar.avatar_ribbon.height)
.aligned()
.bottom()
.boxed(),
)
.constrained()
.with_width(theme.workspace.titlebar.avatar_width)
.contained()
.with_margin_left(theme.workspace.titlebar.avatar_margin)
.boxed();
if let Some((peer_id, peer_github_login)) = peer {
MouseEventHandler::<ToggleFollow>::new(replica_id.into(), cx, move |_, _| content)
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(ToggleFollow(peer_id))
})
.with_tooltip::<ToggleFollow, _>(
peer_id.0 as usize,
if is_followed {
format!("Unfollow {}", peer_github_login)
} else {
format!("Follow {}", peer_github_login)
},
Some(Box::new(FollowNextCollaborator)),
theme.tooltip.clone(),
cx,
)
.boxed()
} else {
content
}
}
fn render_disconnected_overlay(&self, cx: &mut RenderContext<Workspace>) -> Option<ElementBox> {
if self.project.read(cx).is_read_only() {
enum DisconnectedOverlay {}
@ -2698,6 +2562,7 @@ impl View for Workspace {
.with_child(
Stack::new()
.with_child({
let project = self.project.clone();
Flex::row()
.with_children(
if self.left_sidebar.read(cx).active_item().is_some() {
@ -2715,9 +2580,11 @@ impl View for Workspace {
Flex::column()
.with_child(
FlexItem::new(self.center.render(
&project,
&theme,
&self.follower_states_by_leader,
self.project.read(cx).collaborators(),
self.active_call.as_ref(),
cx,
))
.flex(1., true)
.boxed(),
@ -2814,87 +2681,6 @@ impl WorkspaceHandle for ViewHandle<Workspace> {
}
}
pub struct AvatarRibbon {
color: Color,
}
impl AvatarRibbon {
pub fn new(color: Color) -> AvatarRibbon {
AvatarRibbon { color }
}
}
impl Element for AvatarRibbon {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: gpui::SizeConstraint,
_: &mut gpui::LayoutContext,
) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
(constraint.max, ())
}
fn paint(
&mut self,
bounds: gpui::geometry::rect::RectF,
_: gpui::geometry::rect::RectF,
_: &mut Self::LayoutState,
cx: &mut gpui::PaintContext,
) -> Self::PaintState {
let mut path = PathBuilder::new();
path.reset(bounds.lower_left());
path.curve_to(
bounds.origin() + vec2f(bounds.height(), 0.),
bounds.origin(),
);
path.line_to(bounds.upper_right() - vec2f(bounds.height(), 0.));
path.curve_to(bounds.lower_right(), bounds.upper_right());
path.line_to(bounds.lower_left());
cx.scene.push_path(path.build(self.color, None));
}
fn dispatch_event(
&mut self,
_: &gpui::Event,
_: RectF,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
_: &mut gpui::EventContext,
) -> bool {
false
}
fn rect_for_text_range(
&self,
_: Range<usize>,
_: RectF,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &gpui::MeasurementContext,
) -> Option<RectF> {
None
}
fn debug(
&self,
bounds: gpui::geometry::rect::RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &gpui::DebugContext,
) -> gpui::json::Value {
json::json!({
"type": "AvatarRibbon",
"bounds": bounds.to_json(),
"color": self.color.to_json(),
})
}
}
impl std::fmt::Debug for OpenPaths {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OpenPaths")
@ -2964,7 +2750,6 @@ pub fn open_paths(
cx.add_window((app_state.build_window_options)(), |cx| {
let project = Project::local(
false,
app_state.client.clone(),
app_state.user_store.clone(),
app_state.project_store.clone(),
@ -2989,44 +2774,14 @@ pub fn open_paths(
})
.await;
if let Some(project) = new_project {
project
.update(&mut cx, |project, cx| project.restore_state(cx))
.await
.log_err();
}
(workspace, items)
})
}
pub fn join_project(
contact: Arc<Contact>,
project_index: usize,
app_state: &Arc<AppState>,
cx: &mut MutableAppContext,
) {
let project_id = contact.projects[project_index].id;
for window_id in cx.window_ids().collect::<Vec<_>>() {
if let Some(workspace) = cx.root_view::<Workspace>(window_id) {
if workspace.read(cx).project().read(cx).remote_id() == Some(project_id) {
cx.activate_window(window_id);
return;
}
}
}
cx.add_window((app_state.build_window_options)(), |cx| {
WaitingRoom::new(contact, project_index, app_state.clone(), cx)
});
}
fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
let (window_id, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
let mut workspace = Workspace::new(
Project::local(
false,
app_state.client.clone(),
app_state.user_store.clone(),
app_state.project_store.clone(),
@ -3236,7 +2991,7 @@ mod tests {
// When there are no dirty items, there's nothing to do.
let item1 = cx.add_view(&workspace, |_| TestItem::new());
workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
let task = workspace.update(cx, |w, cx| w.prepare_to_close(cx));
let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
assert!(task.await.unwrap());
// When there are dirty untitled items, prompt to save each one. If the user
@ -3256,7 +3011,7 @@ mod tests {
w.add_item(Box::new(item2.clone()), cx);
w.add_item(Box::new(item3.clone()), cx);
});
let task = workspace.update(cx, |w, cx| w.prepare_to_close(cx));
let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
cx.foreground().run_until_parked();
cx.simulate_prompt_answer(window_id, 2 /* cancel */);
cx.foreground().run_until_parked();

View file

@ -19,15 +19,15 @@ activity_indicator = { path = "../activity_indicator" }
assets = { path = "../assets" }
auto_update = { path = "../auto_update" }
breadcrumbs = { path = "../breadcrumbs" }
call = { path = "../call" }
chat_panel = { path = "../chat_panel" }
cli = { path = "../cli" }
collab_ui = { path = "../collab_ui" }
collections = { path = "../collections" }
command_palette = { path = "../command_palette" }
context_menu = { path = "../context_menu" }
client = { path = "../client" }
clock = { path = "../clock" }
contacts_panel = { path = "../contacts_panel" }
contacts_status_item = { path = "../contacts_status_item" }
diagnostics = { path = "../diagnostics" }
editor = { path = "../editor" }
file_finder = { path = "../file_finder" }
@ -105,17 +105,19 @@ tree-sitter-html = "0.19.0"
url = "2.2"
[dev-dependencies]
text = { path = "../text", features = ["test-support"] }
call = { path = "../call", features = ["test-support"] }
client = { path = "../client", features = ["test-support"] }
editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
rpc = { path = "../rpc", features = ["test-support"] }
client = { path = "../client", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
text = { path = "../text", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }
env_logger = "0.9"
serde_json = { version = "1.0", features = ["preserve_order"] }
unindent = "0.1.7"

View file

@ -112,7 +112,6 @@ fn main() {
go_to_line::init(cx);
file_finder::init(cx);
chat_panel::init(cx);
contacts_panel::init(cx);
outline::init(cx);
project_symbols::init(cx);
project_panel::init(cx);
@ -138,11 +137,11 @@ fn main() {
})
.detach();
let project_store = cx.add_model(|_| ProjectStore::new());
let db = cx.background().block(db);
client.start_telemetry(db.clone());
client.report_event("start app", Default::default());
let project_store = cx.add_model(|_| ProjectStore::new(db.clone()));
let app_state = Arc::new(AppState {
languages,
themes,
@ -159,6 +158,7 @@ fn main() {
journal::init(app_state.clone(), cx);
theme_selector::init(app_state.clone(), cx);
zed::init(&app_state, cx);
collab_ui::init(app_state.clone(), cx);
cx.set_menus(menus::menus());

View file

@ -244,10 +244,6 @@ pub fn menus() -> Vec<Menu<'static>> {
name: "Project Panel",
action: Box::new(project_panel::ToggleFocus),
},
MenuItem::Action {
name: "Contacts Panel",
action: Box::new(contacts_panel::ToggleFocus),
},
MenuItem::Action {
name: "Command Palette",
action: Box::new(command_palette::Toggle),

View file

@ -10,9 +10,8 @@ use anyhow::{anyhow, Context, Result};
use assets::Assets;
use breadcrumbs::Breadcrumbs;
pub use client;
use collab_ui::CollabTitlebarItem;
use collections::VecDeque;
pub use contacts_panel;
use contacts_panel::ContactsPanel;
pub use editor;
use editor::{Editor, MultiBuffer};
use gpui::{
@ -214,15 +213,9 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
workspace.toggle_sidebar_item_focus(SidebarSide::Left, 0, cx);
},
);
cx.add_action(
|workspace: &mut Workspace,
_: &contacts_panel::ToggleFocus,
cx: &mut ViewContext<Workspace>| {
workspace.toggle_sidebar_item_focus(SidebarSide::Right, 0, cx);
},
);
activity_indicator::init(cx);
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
settings::KeymapFileContent::load_defaults(cx);
}
@ -231,7 +224,8 @@ pub fn initialize_workspace(
app_state: &Arc<AppState>,
cx: &mut ViewContext<Workspace>,
) {
cx.subscribe(&cx.handle(), {
let workspace_handle = cx.handle();
cx.subscribe(&workspace_handle, {
move |_, _, event, cx| {
if let workspace::Event::PaneAdded(pane) = event {
pane.update(cx, |pane, cx| {
@ -285,16 +279,11 @@ pub fn initialize_workspace(
}));
});
let project_panel = ProjectPanel::new(workspace.project().clone(), cx);
let contact_panel = cx.add_view(|cx| {
ContactsPanel::new(
app_state.user_store.clone(),
app_state.project_store.clone(),
workspace.weak_handle(),
cx,
)
});
let collab_titlebar_item =
cx.add_view(|cx| CollabTitlebarItem::new(&workspace_handle, &app_state.user_store, cx));
workspace.set_titlebar_item(collab_titlebar_item, cx);
let project_panel = ProjectPanel::new(workspace.project().clone(), cx);
workspace.left_sidebar().update(cx, |sidebar, cx| {
sidebar.add_item(
"icons/folder_tree_16.svg",
@ -303,14 +292,6 @@ pub fn initialize_workspace(
cx,
)
});
workspace.right_sidebar().update(cx, |sidebar, cx| {
sidebar.add_item(
"icons/user_group_16.svg",
"Contacts Panel".to_string(),
contact_panel,
cx,
)
});
let diagnostic_summary =
cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace.project(), cx));
@ -363,7 +344,9 @@ fn quit(_: &Quit, cx: &mut gpui::MutableAppContext) {
// If the user cancels any save prompt, then keep the app open.
for workspace in workspaces {
if !workspace
.update(&mut cx, |workspace, cx| workspace.prepare_to_close(cx))
.update(&mut cx, |workspace, cx| {
workspace.prepare_to_close(true, cx)
})
.await?
{
return Ok(());
@ -1772,6 +1755,7 @@ mod tests {
let state = Arc::get_mut(&mut app_state).unwrap();
state.initialize_workspace = initialize_workspace;
state.build_window_options = build_window_options;
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
workspace::init(app_state.clone(), cx);
editor::init(cx);
pane::init(cx);

View file

@ -2,7 +2,6 @@ import Theme from "../themes/common/theme";
import chatPanel from "./chatPanel";
import { text } from "./components";
import contactFinder from "./contactFinder";
import contactsPanel from "./contactsPanel";
import contactsPopover from "./contactsPopover";
import commandPalette from "./commandPalette";
import editor from "./editor";
@ -14,8 +13,11 @@ import contextMenu from "./contextMenu";
import projectDiagnostics from "./projectDiagnostics";
import contactNotification from "./contactNotification";
import updateNotification from "./updateNotification";
import projectSharedNotification from "./projectSharedNotification";
import tooltip from "./tooltip";
import terminal from "./terminal";
import contactList from "./contactList";
import incomingCallNotification from "./incomingCallNotification";
export const panel = {
padding: { top: 12, bottom: 12 },
@ -36,7 +38,7 @@ export default function app(theme: Theme): Object {
projectPanel: projectPanel(theme),
chatPanel: chatPanel(theme),
contactsPopover: contactsPopover(theme),
contactsPanel: contactsPanel(theme),
contactList: contactList(theme),
contactFinder: contactFinder(theme),
search: search(theme),
breadcrumbs: {
@ -47,6 +49,8 @@ export default function app(theme: Theme): Object {
},
contactNotification: contactNotification(theme),
updateNotification: updateNotification(theme),
projectSharedNotification: projectSharedNotification(theme),
incomingCallNotification: incomingCallNotification(theme),
tooltip: tooltip(theme),
terminal: terminal(theme),
};

View file

@ -1,8 +1,9 @@
import Theme from "../themes/common/theme";
import picker from "./picker";
import { backgroundColor, iconColor } from "./components";
import { backgroundColor, border, iconColor, player, text } from "./components";
export default function contactFinder(theme: Theme) {
const sideMargin = 6;
const contactButton = {
background: backgroundColor(theme, 100),
color: iconColor(theme, "primary"),
@ -12,7 +13,31 @@ export default function contactFinder(theme: Theme) {
};
return {
...picker(theme),
picker: {
item: {
...picker(theme).item,
margin: { left: sideMargin, right: sideMargin }
},
empty: picker(theme).empty,
inputEditor: {
background: backgroundColor(theme, 500),
cornerRadius: 6,
text: text(theme, "mono", "primary"),
placeholderText: text(theme, "mono", "placeholder", { size: "sm" }),
selection: player(theme, 1).selection,
border: border(theme, "secondary"),
padding: {
bottom: 4,
left: 8,
right: 8,
top: 4,
},
margin: {
left: sideMargin,
right: sideMargin,
}
}
},
rowHeight: 28,
contactAvatar: {
cornerRadius: 10,

View file

@ -1,18 +1,17 @@
import Theme from "../themes/common/theme";
import { panel } from "./app";
import {
backgroundColor,
border,
borderColor,
iconColor,
player,
text,
} from "./components";
import { backgroundColor, border, borderColor, iconColor, player, text } from "./components";
export default function contactsPanel(theme: Theme) {
export default function contactList(theme: Theme) {
const nameMargin = 8;
const sidePadding = 12;
const contactButton = {
background: backgroundColor(theme, 100),
color: iconColor(theme, "primary"),
iconWidth: 8,
buttonWidth: 16,
cornerRadius: 8,
};
const projectRow = {
guestAvatarSpacing: 4,
height: 24,
@ -39,17 +38,7 @@ export default function contactsPanel(theme: Theme) {
},
};
const contactButton = {
background: backgroundColor(theme, 100),
color: iconColor(theme, "primary"),
iconWidth: 8,
buttonWidth: 16,
cornerRadius: 8,
};
return {
...panel,
padding: { top: panel.padding.top, bottom: 0 },
userQueryEditor: {
background: backgroundColor(theme, 500),
cornerRadius: 6,
@ -64,28 +53,20 @@ export default function contactsPanel(theme: Theme) {
top: 4,
},
margin: {
left: sidePadding,
right: sidePadding,
left: 6
},
},
userQueryEditorHeight: 32,
userQueryEditorHeight: 33,
addContactButton: {
margin: { left: 6, right: 12 },
color: iconColor(theme, "primary"),
buttonWidth: 16,
buttonWidth: 28,
iconWidth: 16,
},
privateButton: {
iconWidth: 12,
color: iconColor(theme, "primary"),
cornerRadius: 5,
buttonWidth: 12,
},
rowHeight: 28,
sectionIconSize: 8,
headerRow: {
...text(theme, "mono", "secondary", { size: "sm" }),
margin: { top: 14 },
margin: { top: 6 },
padding: {
left: sidePadding,
right: sidePadding,
@ -95,6 +76,26 @@ export default function contactsPanel(theme: Theme) {
background: backgroundColor(theme, 100, "active"),
},
},
leaveCall: {
background: backgroundColor(theme, 100),
border: border(theme, "secondary"),
cornerRadius: 6,
margin: {
top: 1,
},
padding: {
top: 1,
bottom: 1,
left: 7,
right: 7,
},
...text(theme, "sans", "secondary", { size: "xs" }),
hover: {
...text(theme, "sans", "active", { size: "xs" }),
background: backgroundColor(theme, "on300", "hovered"),
border: border(theme, "primary"),
},
},
contactRow: {
padding: {
left: sidePadding,
@ -104,20 +105,22 @@ export default function contactsPanel(theme: Theme) {
background: backgroundColor(theme, 100, "active"),
},
},
treeBranch: {
color: borderColor(theme, "active"),
width: 1,
hover: {
color: borderColor(theme, "active"),
},
active: {
color: borderColor(theme, "active"),
},
},
contactAvatar: {
cornerRadius: 10,
width: 18,
},
contactStatusFree: {
cornerRadius: 4,
padding: 4,
margin: { top: 12, left: 12 },
background: iconColor(theme, "ok"),
},
contactStatusBusy: {
cornerRadius: 4,
padding: 4,
margin: { top: 12, left: 12 },
background: iconColor(theme, "error"),
},
contactUsername: {
...text(theme, "mono", "primary", { size: "sm" }),
margin: {
@ -136,6 +139,19 @@ export default function contactsPanel(theme: Theme) {
background: backgroundColor(theme, 100),
color: iconColor(theme, "muted"),
},
callingIndicator: {
...text(theme, "mono", "muted", { size: "xs" })
},
treeBranch: {
color: borderColor(theme, "active"),
width: 1,
hover: {
color: borderColor(theme, "active"),
},
active: {
color: borderColor(theme, "active"),
},
},
projectRow: {
...projectRow,
background: backgroundColor(theme, 300),
@ -150,16 +166,5 @@ export default function contactsPanel(theme: Theme) {
background: backgroundColor(theme, 300, "active"),
},
},
inviteRow: {
padding: {
left: sidePadding,
right: sidePadding,
},
border: { top: true, width: 1, color: borderColor(theme, "primary") },
text: text(theme, "sans", "secondary", { size: "sm" }),
hover: {
text: text(theme, "sans", "active", { size: "sm" }),
},
},
};
}
}

View file

@ -1,8 +1,28 @@
import Theme from "../themes/common/theme";
import { backgroundColor } from "./components";
import { backgroundColor, border, borderColor, popoverShadow, text } from "./components";
export default function workspace(theme: Theme) {
export default function contactsPopover(theme: Theme) {
const sidePadding = 12;
return {
background: backgroundColor(theme, 300),
background: backgroundColor(theme, 300, "base"),
cornerRadius: 6,
padding: { top: 6 },
margin: { top: -6 },
shadow: popoverShadow(theme),
border: border(theme, "primary"),
width: 300,
height: 400,
inviteRowHeight: 28,
inviteRow: {
padding: {
left: sidePadding,
right: sidePadding,
},
border: { top: true, width: 1, color: borderColor(theme, "primary") },
text: text(theme, "sans", "secondary", { size: "sm" }),
hover: {
text: text(theme, "sans", "active", { size: "sm" }),
},
},
}
}

View file

@ -0,0 +1,44 @@
import Theme from "../themes/common/theme";
import { backgroundColor, borderColor, text } from "./components";
export default function incomingCallNotification(theme: Theme): Object {
const avatarSize = 48;
return {
windowHeight: 74,
windowWidth: 380,
background: backgroundColor(theme, 300),
callerContainer: {
padding: 12,
},
callerAvatar: {
height: avatarSize,
width: avatarSize,
cornerRadius: avatarSize / 2,
},
callerMetadata: {
margin: { left: 10 },
},
callerUsername: {
...text(theme, "sans", "active", { size: "sm", weight: "bold" }),
margin: { top: -3 },
},
callerMessage: {
...text(theme, "sans", "secondary", { size: "xs" }),
margin: { top: -3 },
},
worktreeRoots: {
...text(theme, "sans", "secondary", { size: "xs", weight: "bold" }),
margin: { top: -3 },
},
buttonWidth: 96,
acceptButton: {
background: backgroundColor(theme, "ok", "active"),
border: { left: true, bottom: true, width: 1, color: borderColor(theme, "primary") },
...text(theme, "sans", "ok", { size: "xs", weight: "extra_bold" })
},
declineButton: {
border: { left: true, width: 1, color: borderColor(theme, "primary") },
...text(theme, "sans", "error", { size: "xs", weight: "extra_bold" })
},
};
}

View file

@ -0,0 +1,44 @@
import Theme from "../themes/common/theme";
import { backgroundColor, borderColor, text } from "./components";
export default function projectSharedNotification(theme: Theme): Object {
const avatarSize = 48;
return {
windowHeight: 74,
windowWidth: 380,
background: backgroundColor(theme, 300),
ownerContainer: {
padding: 12,
},
ownerAvatar: {
height: avatarSize,
width: avatarSize,
cornerRadius: avatarSize / 2,
},
ownerMetadata: {
margin: { left: 10 },
},
ownerUsername: {
...text(theme, "sans", "active", { size: "sm", weight: "bold" }),
margin: { top: -3 },
},
message: {
...text(theme, "sans", "secondary", { size: "xs" }),
margin: { top: -3 },
},
worktreeRoots: {
...text(theme, "sans", "secondary", { size: "xs", weight: "bold" }),
margin: { top: -3 },
},
buttonWidth: 96,
openButton: {
background: backgroundColor(theme, "info", "active"),
border: { left: true, bottom: true, width: 1, color: borderColor(theme, "primary") },
...text(theme, "sans", "info", { size: "xs", weight: "extra_bold" })
},
dismissButton: {
border: { left: true, width: 1, color: borderColor(theme, "primary") },
...text(theme, "sans", "secondary", { size: "xs", weight: "extra_bold" })
},
};
}

View file

@ -16,6 +16,27 @@ export function workspaceBackground(theme: Theme) {
export default function workspace(theme: Theme) {
const titlebarPadding = 6;
const titlebarButton = {
background: backgroundColor(theme, 100),
border: border(theme, "secondary"),
cornerRadius: 6,
margin: {
top: 1,
},
padding: {
top: 1,
bottom: 1,
left: 7,
right: 7,
},
...text(theme, "sans", "secondary", { size: "xs" }),
hover: {
...text(theme, "sans", "active", { size: "xs" }),
background: backgroundColor(theme, "on300", "hovered"),
border: border(theme, "primary"),
},
};
const avatarWidth = 18;
return {
background: backgroundColor(theme, 300),
@ -27,6 +48,14 @@ export default function workspace(theme: Theme) {
padding: 12,
...text(theme, "sans", "primary", { size: "lg" }),
},
externalLocationMessage: {
background: backgroundColor(theme, "info"),
border: border(theme, "secondary"),
cornerRadius: 6,
padding: 12,
margin: { bottom: 8, right: 8 },
...text(theme, "sans", "secondary", { size: "xs" }),
},
leaderBorderOpacity: 0.7,
leaderBorderWidth: 2.0,
tabBar: tabBar(theme),
@ -52,7 +81,7 @@ export default function workspace(theme: Theme) {
},
statusBar: statusBar(theme),
titlebar: {
avatarWidth: 18,
avatarWidth,
avatarMargin: 8,
height: 33,
background: backgroundColor(theme, 100),
@ -62,12 +91,20 @@ export default function workspace(theme: Theme) {
},
title: text(theme, "sans", "primary"),
avatar: {
cornerRadius: 10,
cornerRadius: avatarWidth / 2,
border: {
color: "#00000088",
width: 1,
},
},
inactiveAvatar: {
cornerRadius: avatarWidth / 2,
border: {
color: "#00000088",
width: 1,
},
grayscale: true,
},
avatarRibbon: {
height: 3,
width: 12,
@ -76,24 +113,7 @@ export default function workspace(theme: Theme) {
},
border: border(theme, "primary", { bottom: true, overlay: true }),
signInPrompt: {
background: backgroundColor(theme, 100),
border: border(theme, "secondary"),
cornerRadius: 6,
margin: {
top: 1,
},
padding: {
top: 1,
bottom: 1,
left: 7,
right: 7,
},
...text(theme, "sans", "secondary", { size: "xs" }),
hover: {
...text(theme, "sans", "active", { size: "xs" }),
background: backgroundColor(theme, "on300", "hovered"),
border: border(theme, "primary"),
},
...titlebarButton
},
offlineIcon: {
color: iconColor(theme, "secondary"),
@ -118,6 +138,30 @@ export default function workspace(theme: Theme) {
},
cornerRadius: 6,
},
toggleContactsButton: {
cornerRadius: 6,
color: iconColor(theme, "secondary"),
iconWidth: 8,
buttonWidth: 20,
active: {
background: backgroundColor(theme, "on300", "active"),
color: iconColor(theme, "active"),
},
hover: {
background: backgroundColor(theme, "on300", "hovered"),
color: iconColor(theme, "active"),
},
},
toggleContactsBadge: {
cornerRadius: 3,
padding: 2,
margin: { top: 3, left: 3 },
border: { width: 1, color: workspaceBackground(theme) },
background: iconColor(theme, "feature"),
},
shareButton: {
...titlebarButton
}
},
toolbar: {
height: 34,