Collab UI v2 (#2618)

This PR implements
https://www.figma.com/file/pLq7dvhx2mFeWFOedXpUQ5/Project-%E2%80%93-Collaboration-UI?type=design&node-id=666%3A8610&t=XFGk6KEtTCFXBvCQ-1

Release notes:
- [x] Redesigned collaboration UI
- [x] Added voice support
This commit is contained in:
Mikayla Maki
2023-06-28 09:14:39 -07:00
committed by GitHub
368 changed files with 4748 additions and 439 deletions
+5 -1
View File
@@ -3,6 +3,7 @@ use client::{proto, User};
use collections::HashMap;
use gpui::WeakModelHandle;
pub use live_kit_client::Frame;
use live_kit_client::RemoteAudioTrack;
use project::Project;
use std::{fmt, sync::Arc};
@@ -42,7 +43,10 @@ pub struct RemoteParticipant {
pub peer_id: proto::PeerId,
pub projects: Vec<proto::ParticipantProject>,
pub location: ParticipantLocation,
pub tracks: HashMap<live_kit_client::Sid, Arc<RemoteVideoTrack>>,
pub muted: bool,
pub speaking: bool,
pub video_tracks: HashMap<live_kit_client::Sid, Arc<RemoteVideoTrack>>,
pub audio_tracks: HashMap<live_kit_client::Sid, Arc<RemoteAudioTrack>>,
}
#[derive(Clone)]
+355 -31
View File
@@ -12,7 +12,10 @@ use fs::Fs;
use futures::{FutureExt, StreamExt};
use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle};
use language::LanguageRegistry;
use live_kit_client::{LocalTrackPublication, LocalVideoTrack, RemoteVideoTrackUpdate};
use live_kit_client::{
LocalAudioTrack, LocalTrackPublication, LocalVideoTrack, RemoteAudioTrackUpdate,
RemoteVideoTrackUpdate,
};
use postage::stream::Stream;
use project::Project;
use std::{future::Future, mem, pin::Pin, sync::Arc, time::Duration};
@@ -28,6 +31,9 @@ pub enum Event {
RemoteVideoTracksChanged {
participant_id: proto::PeerId,
},
RemoteAudioTracksChanged {
participant_id: proto::PeerId,
},
RemoteProjectShared {
owner: Arc<User>,
project_id: u64,
@@ -112,9 +118,9 @@ impl Room {
}
});
let mut track_changes = room.remote_video_track_updates();
let _maintain_tracks = cx.spawn_weak(|this, mut cx| async move {
while let Some(track_change) = track_changes.next().await {
let mut track_video_changes = room.remote_video_track_updates();
let _maintain_video_tracks = cx.spawn_weak(|this, mut cx| async move {
while let Some(track_change) = track_video_changes.next().await {
let this = if let Some(this) = this.upgrade(&cx) {
this
} else {
@@ -127,16 +133,41 @@ impl Room {
}
});
cx.foreground()
.spawn(room.connect(&connection_info.server_url, &connection_info.token))
.detach_and_log_err(cx);
let mut track_audio_changes = room.remote_audio_track_updates();
let _maintain_audio_tracks = cx.spawn_weak(|this, mut cx| async move {
while let Some(track_change) = track_audio_changes.next().await {
let this = if let Some(this) = this.upgrade(&cx) {
this
} else {
break;
};
this.update(&mut cx, |this, cx| {
this.remote_audio_track_updated(track_change, cx).log_err()
});
}
});
let connect = room.connect(&connection_info.server_url, &connection_info.token);
cx.spawn(|this, mut cx| async move {
connect.await?;
this.update(&mut cx, |this, cx| this.share_microphone(cx))
.await?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
Some(LiveKitRoom {
room,
screen_track: ScreenTrack::None,
screen_track: LocalTrack::None,
microphone_track: LocalTrack::None,
next_publish_id: 0,
muted_by_user: false,
deafened: false,
speaking: false,
_maintain_room,
_maintain_tracks,
_maintain_tracks: [_maintain_video_tracks, _maintain_audio_tracks],
})
} else {
None
@@ -618,20 +649,32 @@ impl Room {
peer_id,
projects: participant.projects,
location,
tracks: Default::default(),
muted: false,
speaking: false,
video_tracks: Default::default(),
audio_tracks: Default::default(),
},
);
if let Some(live_kit) = this.live_kit.as_ref() {
let tracks =
let video_tracks =
live_kit.room.remote_video_tracks(&user.id.to_string());
for track in tracks {
let audio_tracks =
live_kit.room.remote_audio_tracks(&user.id.to_string());
for track in video_tracks {
this.remote_video_track_updated(
RemoteVideoTrackUpdate::Subscribed(track),
cx,
)
.log_err();
}
for track in audio_tracks {
this.remote_audio_track_updated(
RemoteAudioTrackUpdate::Subscribed(track),
cx,
)
.log_err();
}
}
}
}
@@ -706,7 +749,7 @@ impl Room {
.remote_participants
.get_mut(&user_id)
.ok_or_else(|| anyhow!("subscribed to track by unknown participant"))?;
participant.tracks.insert(
participant.video_tracks.insert(
track_id.clone(),
Arc::new(RemoteVideoTrack {
live_kit_track: track,
@@ -725,7 +768,7 @@ impl Room {
.remote_participants
.get_mut(&user_id)
.ok_or_else(|| anyhow!("unsubscribed from track by unknown participant"))?;
participant.tracks.remove(&track_id);
participant.video_tracks.remove(&track_id);
cx.emit(Event::RemoteVideoTracksChanged {
participant_id: participant.peer_id,
});
@@ -736,6 +779,84 @@ impl Room {
Ok(())
}
fn remote_audio_track_updated(
&mut self,
change: RemoteAudioTrackUpdate,
cx: &mut ModelContext<Self>,
) -> Result<()> {
match change {
RemoteAudioTrackUpdate::ActiveSpeakersChanged { speakers } => {
let mut speaker_ids = speakers
.into_iter()
.filter_map(|speaker_sid| speaker_sid.parse().ok())
.collect::<Vec<u64>>();
speaker_ids.sort_unstable();
for (sid, participant) in &mut self.remote_participants {
if let Ok(_) = speaker_ids.binary_search(sid) {
participant.speaking = true;
} else {
participant.speaking = false;
}
}
if let Some(id) = self.client.user_id() {
if let Some(room) = &mut self.live_kit {
if let Ok(_) = speaker_ids.binary_search(&id) {
room.speaking = true;
} else {
room.speaking = false;
}
}
}
cx.notify();
}
RemoteAudioTrackUpdate::MuteChanged { track_id, muted } => {
for participant in &mut self.remote_participants.values_mut() {
let mut found = false;
for track in participant.audio_tracks.values() {
if track.sid() == track_id {
found = true;
break;
}
}
if found {
participant.muted = muted;
break;
}
}
cx.notify();
}
RemoteAudioTrackUpdate::Subscribed(track) => {
let user_id = track.publisher_id().parse()?;
let track_id = track.sid().to_string();
let participant = self
.remote_participants
.get_mut(&user_id)
.ok_or_else(|| anyhow!("subscribed to track by unknown participant"))?;
participant.audio_tracks.insert(track_id.clone(), track);
cx.emit(Event::RemoteAudioTracksChanged {
participant_id: participant.peer_id,
});
}
RemoteAudioTrackUpdate::Unsubscribed {
publisher_id,
track_id,
} => {
let user_id = publisher_id.parse()?;
let participant = self
.remote_participants
.get_mut(&user_id)
.ok_or_else(|| anyhow!("unsubscribed from track by unknown participant"))?;
participant.audio_tracks.remove(&track_id);
cx.emit(Event::RemoteAudioTracksChanged {
participant_id: participant.peer_id,
});
}
}
cx.notify();
Ok(())
}
fn check_invariants(&self) {
#[cfg(any(test, feature = "test-support"))]
{
@@ -908,7 +1029,116 @@ impl Room {
pub fn is_screen_sharing(&self) -> bool {
self.live_kit.as_ref().map_or(false, |live_kit| {
!matches!(live_kit.screen_track, ScreenTrack::None)
!matches!(live_kit.screen_track, LocalTrack::None)
})
}
pub fn is_sharing_mic(&self) -> bool {
self.live_kit.as_ref().map_or(false, |live_kit| {
!matches!(live_kit.microphone_track, LocalTrack::None)
})
}
pub fn is_muted(&self) -> bool {
self.live_kit
.as_ref()
.and_then(|live_kit| match &live_kit.microphone_track {
LocalTrack::None => None,
LocalTrack::Pending { muted, .. } => Some(*muted),
LocalTrack::Published { muted, .. } => Some(*muted),
})
.unwrap_or(false)
}
pub fn is_speaking(&self) -> bool {
self.live_kit
.as_ref()
.map_or(false, |live_kit| live_kit.speaking)
}
pub fn is_deafened(&self) -> Option<bool> {
self.live_kit.as_ref().map(|live_kit| live_kit.deafened)
}
pub fn share_microphone(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
if self.status.is_offline() {
return Task::ready(Err(anyhow!("room is offline")));
} else if self.is_sharing_mic() {
return Task::ready(Err(anyhow!("microphone was already shared")));
}
let publish_id = if let Some(live_kit) = self.live_kit.as_mut() {
let publish_id = post_inc(&mut live_kit.next_publish_id);
live_kit.microphone_track = LocalTrack::Pending {
publish_id,
muted: false,
};
cx.notify();
publish_id
} else {
return Task::ready(Err(anyhow!("live-kit was not initialized")));
};
cx.spawn_weak(|this, mut cx| async move {
let publish_track = async {
let track = LocalAudioTrack::create();
this.upgrade(&cx)
.ok_or_else(|| anyhow!("room was dropped"))?
.read_with(&cx, |this, _| {
this.live_kit
.as_ref()
.map(|live_kit| live_kit.room.publish_audio_track(&track))
})
.ok_or_else(|| anyhow!("live-kit was not initialized"))?
.await
};
let publication = publish_track.await;
this.upgrade(&cx)
.ok_or_else(|| anyhow!("room was dropped"))?
.update(&mut cx, |this, cx| {
let live_kit = this
.live_kit
.as_mut()
.ok_or_else(|| anyhow!("live-kit was not initialized"))?;
let (canceled, muted) = if let LocalTrack::Pending {
publish_id: cur_publish_id,
muted,
} = &live_kit.microphone_track
{
(*cur_publish_id != publish_id, *muted)
} else {
(true, false)
};
match publication {
Ok(publication) => {
if canceled {
live_kit.room.unpublish_track(publication);
} else {
if muted {
cx.background().spawn(publication.set_mute(muted)).detach();
}
live_kit.microphone_track = LocalTrack::Published {
track_publication: publication,
muted,
};
cx.notify();
}
Ok(())
}
Err(error) => {
if canceled {
Ok(())
} else {
live_kit.microphone_track = LocalTrack::None;
cx.notify();
Err(error)
}
}
}
})
})
}
@@ -921,7 +1151,10 @@ impl Room {
let (displays, publish_id) = if let Some(live_kit) = self.live_kit.as_mut() {
let publish_id = post_inc(&mut live_kit.next_publish_id);
live_kit.screen_track = ScreenTrack::Pending { publish_id };
live_kit.screen_track = LocalTrack::Pending {
publish_id,
muted: false,
};
cx.notify();
(live_kit.room.display_sources(), publish_id)
} else {
@@ -955,13 +1188,14 @@ impl Room {
.as_mut()
.ok_or_else(|| anyhow!("live-kit was not initialized"))?;
let canceled = if let ScreenTrack::Pending {
let (canceled, muted) = if let LocalTrack::Pending {
publish_id: cur_publish_id,
muted,
} = &live_kit.screen_track
{
*cur_publish_id != publish_id
(*cur_publish_id != publish_id, *muted)
} else {
true
(true, false)
};
match publication {
@@ -969,7 +1203,13 @@ impl Room {
if canceled {
live_kit.room.unpublish_track(publication);
} else {
live_kit.screen_track = ScreenTrack::Published(publication);
if muted {
cx.background().spawn(publication.set_mute(muted)).detach();
}
live_kit.screen_track = LocalTrack::Published {
track_publication: publication,
muted,
};
cx.notify();
}
Ok(())
@@ -978,7 +1218,7 @@ impl Room {
if canceled {
Ok(())
} else {
live_kit.screen_track = ScreenTrack::None;
live_kit.screen_track = LocalTrack::None;
cx.notify();
Err(error)
}
@@ -987,6 +1227,77 @@ impl Room {
})
})
}
fn set_mute(
live_kit: &mut LiveKitRoom,
should_mute: bool,
cx: &mut ModelContext<Self>,
) -> Result<Task<Result<()>>> {
if !should_mute {
// clear user muting state.
live_kit.muted_by_user = false;
}
match &mut live_kit.microphone_track {
LocalTrack::None => Err(anyhow!("microphone was not shared")),
LocalTrack::Pending { muted, .. } => {
*muted = should_mute;
cx.notify();
Ok(Task::Ready(Some(Ok(()))))
}
LocalTrack::Published {
track_publication,
muted,
} => {
*muted = should_mute;
cx.notify();
Ok(cx.background().spawn(track_publication.set_mute(*muted)))
}
}
}
pub fn toggle_mute(&mut self, cx: &mut ModelContext<Self>) -> Result<Task<Result<()>>> {
let should_mute = !self.is_muted();
if let Some(live_kit) = self.live_kit.as_mut() {
let ret = Self::set_mute(live_kit, should_mute, cx);
live_kit.muted_by_user = should_mute;
ret
} else {
Err(anyhow!("LiveKit not started"))
}
}
pub fn toggle_deafen(&mut self, cx: &mut ModelContext<Self>) -> Result<Task<Result<()>>> {
if let Some(live_kit) = self.live_kit.as_mut() {
(*live_kit).deafened = !live_kit.deafened;
let mut tasks = Vec::with_capacity(self.remote_participants.len());
// Context notification is sent within set_mute itself.
let mut mute_task = None;
// When deafening, mute user's mic as well.
// When undeafening, unmute user's mic unless it was manually muted prior to deafening.
if live_kit.deafened || !live_kit.muted_by_user {
mute_task = Some(Self::set_mute(live_kit, live_kit.deafened, cx)?);
};
for participant in self.remote_participants.values() {
for track in live_kit
.room
.remote_audio_track_publications(&participant.user.id.to_string())
{
tasks.push(cx.foreground().spawn(track.set_enabled(!live_kit.deafened)));
}
}
Ok(cx.foreground().spawn(async move {
if let Some(mute_task) = mute_task {
mute_task.await?;
}
for task in tasks {
task.await?;
}
Ok(())
}))
} else {
Err(anyhow!("LiveKit not started"))
}
}
pub fn unshare_screen(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
if self.status.is_offline() {
@@ -998,13 +1309,15 @@ impl Room {
.as_mut()
.ok_or_else(|| anyhow!("live-kit was not initialized"))?;
match mem::take(&mut live_kit.screen_track) {
ScreenTrack::None => Err(anyhow!("screen was not shared")),
ScreenTrack::Pending { .. } => {
LocalTrack::None => Err(anyhow!("screen was not shared")),
LocalTrack::Pending { .. } => {
cx.notify();
Ok(())
}
ScreenTrack::Published(track) => {
live_kit.room.unpublish_track(track);
LocalTrack::Published {
track_publication, ..
} => {
live_kit.room.unpublish_track(track_publication);
cx.notify();
Ok(())
}
@@ -1023,19 +1336,30 @@ impl Room {
struct LiveKitRoom {
room: Arc<live_kit_client::Room>,
screen_track: ScreenTrack,
screen_track: LocalTrack,
microphone_track: LocalTrack,
/// Tracks whether we're currently in a muted state due to auto-mute from deafening or manual mute performed by user.
muted_by_user: bool,
deafened: bool,
speaking: bool,
next_publish_id: usize,
_maintain_room: Task<()>,
_maintain_tracks: Task<()>,
_maintain_tracks: [Task<()>; 2],
}
enum ScreenTrack {
enum LocalTrack {
None,
Pending { publish_id: usize },
Published(LocalTrackPublication),
Pending {
publish_id: usize,
muted: bool,
},
Published {
track_publication: LocalTrackPublication,
muted: bool,
},
}
impl Default for ScreenTrack {
impl Default for LocalTrack {
fn default() -> Self {
Self::None
}
+3 -3
View File
@@ -257,7 +257,7 @@ async fn test_basic_calls(
room_b.read_with(cx_b, |room, _| {
assert_eq!(
room.remote_participants()[&client_a.user_id().unwrap()]
.tracks
.video_tracks
.len(),
1
);
@@ -274,7 +274,7 @@ async fn test_basic_calls(
room_c.read_with(cx_c, |room, _| {
assert_eq!(
room.remote_participants()[&client_a.user_id().unwrap()]
.tracks
.video_tracks
.len(),
1
);
@@ -7014,7 +7014,7 @@ async fn test_join_call_after_screen_was_shared(
room.remote_participants()
.get(&client_a.user_id().unwrap())
.unwrap()
.tracks
.video_tracks
.len(),
1
);
+2
View File
@@ -37,8 +37,10 @@ picker = { path = "../picker" }
project = { path = "../project" }
settings = { path = "../settings" }
theme = { path = "../theme" }
theme_selector = { path = "../theme_selector" }
util = { path = "../util" }
workspace = { path = "../workspace" }
zed-actions = {path = "../zed-actions"}
anyhow.workspace = true
futures.workspace = true
+280 -120
View File
@@ -1,6 +1,7 @@
use crate::{
contact_notification::ContactNotification, contacts_popover, face_pile::FacePile,
toggle_screen_sharing, ToggleScreenSharing,
toggle_deafen, toggle_mute, toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute,
ToggleScreenSharing,
};
use call::{ActiveCall, ParticipantLocation, Room};
use client::{proto::PeerId, Client, ContactEventKind, SignIn, SignOut, User, UserStore};
@@ -17,13 +18,13 @@ use gpui::{
AppContext, Entity, ImageData, LayoutContext, ModelHandle, SceneBuilder, Subscription, View,
ViewContext, ViewHandle, WeakViewHandle,
};
use project::Project;
use project::{Project, RepositoryEntry};
use std::{ops::Range, sync::Arc};
use theme::{AvatarStyle, Theme};
use util::ResultExt;
use workspace::{FollowNextCollaborator, Workspace};
const MAX_TITLE_LENGTH: usize = 75;
// const MAX_TITLE_LENGTH: usize = 75;
actions!(
collab,
@@ -78,27 +79,34 @@ impl View for CollabTitlebarItem {
let user = self.user_store.read(cx).current_user();
let peer_id = self.client.peer_id();
if let Some(((user, peer_id), room)) = user
.as_ref()
.zip(peer_id)
.zip(ActiveCall::global(cx).read(cx).room().cloned())
{
left_container
.add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx));
right_container.add_children(self.render_collaborators(&workspace, &theme, &room, cx));
right_container
.add_child(self.render_current_user(&workspace, &theme, &user, peer_id, cx));
.add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx));
right_container.add_child(self.render_leave_call(&theme, cx));
let muted = room.read(cx).is_muted();
let speaking = room.read(cx).is_speaking();
left_container.add_child(
self.render_current_user(&workspace, &theme, &user, peer_id, muted, speaking, cx),
);
left_container.add_children(self.render_collaborators(&workspace, &theme, &room, cx));
right_container.add_child(self.render_toggle_mute(&theme, &room, cx));
right_container.add_child(self.render_toggle_deafen(&theme, &room, cx));
right_container.add_child(self.render_toggle_screen_sharing_button(&theme, &room, cx));
}
let status = workspace.read(cx).client().status();
let status = &*status.borrow();
if matches!(status, client::Status::Connected { .. }) {
right_container.add_child(self.render_toggle_contacts_button(&theme, cx));
right_container.add_child(self.render_user_menu_button(&theme, cx));
let avatar = user.as_ref().and_then(|user| user.avatar.clone());
right_container.add_child(self.render_user_menu_button(&theme, avatar, cx));
} else {
right_container.add_children(self.render_connection_status(status, cx));
right_container.add_child(self.render_sign_in_button(&theme, cx));
right_container.add_child(self.render_user_menu_button(&theme, None, cx));
}
Stack::new()
@@ -108,7 +116,6 @@ impl View for CollabTitlebarItem {
.with_child(
right_container.contained().with_background_color(
theme
.workspace
.titlebar
.container
.background_color
@@ -163,7 +170,6 @@ impl CollabTitlebarItem {
}),
);
let view_id = cx.view_id();
Self {
workspace: workspace.weak_handle(),
project,
@@ -171,6 +177,7 @@ impl CollabTitlebarItem {
client,
contacts_popover: None,
user_menu: cx.add_view(|cx| {
let view_id = cx.view_id();
let mut menu = ContextMenu::new(view_id, cx);
menu.set_position_mode(OverlayPositionMode::Local);
menu
@@ -185,55 +192,45 @@ impl CollabTitlebarItem {
theme: Arc<Theme>,
cx: &ViewContext<Self>,
) -> AnyElement<Self> {
let names_and_branches = project.visible_worktrees(cx).map(|worktree| {
let mut names_and_branches = project.visible_worktrees(cx).map(|worktree| {
let worktree = worktree.read(cx);
(worktree.root_name(), worktree.root_git_entry())
});
fn push_str(buffer: &mut String, index: &mut usize, str: &str) {
buffer.push_str(str);
*index += str.chars().count();
}
let mut indices = Vec::new();
let mut index = 0;
let mut title = String::new();
let mut names_and_branches = names_and_branches.peekable();
while let Some((name, entry)) = names_and_branches.next() {
let pre_index = index;
push_str(&mut title, &mut index, name);
indices.extend((pre_index..index).into_iter());
if let Some(branch) = entry.and_then(|entry| entry.branch()) {
push_str(&mut title, &mut index, "/");
push_str(&mut title, &mut index, &branch);
}
if names_and_branches.peek().is_some() {
push_str(&mut title, &mut index, ", ");
if index >= MAX_TITLE_LENGTH {
title.push_str(" …");
break;
}
}
}
let text_style = theme.workspace.titlebar.title.clone();
let item_spacing = theme.workspace.titlebar.item_spacing;
let (name, entry) = names_and_branches.next().unwrap_or(("", None));
let branch_prepended = entry
.as_ref()
.and_then(RepositoryEntry::branch)
.map(|branch| format!("/{branch}"));
let text_style = theme.titlebar.title.clone();
let item_spacing = theme.titlebar.item_spacing;
let mut highlight = text_style.clone();
highlight.color = theme.workspace.titlebar.highlight_color;
highlight.color = theme.titlebar.highlight_color;
let style = LabelStyle {
text: text_style,
highlight_text: Some(highlight),
};
Label::new(title, style)
.with_highlights(indices)
.contained()
.with_margin_right(item_spacing)
.aligned()
.left()
.into_any_named("title-with-git-information")
let mut ret = Flex::row().with_child(
Label::new(name.to_owned(), style.clone())
.with_highlights((0..name.len()).into_iter().collect())
.contained()
.aligned()
.left()
.into_any_named("title-project-name"),
);
if let Some(git_branch) = branch_prepended {
ret = ret.with_child(
Label::new(git_branch, style)
.contained()
.with_margin_right(item_spacing)
.aligned()
.left()
.into_any_named("title-project-branch"),
)
}
ret.into_any()
}
fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
@@ -297,50 +294,29 @@ impl CollabTitlebarItem {
}
pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) {
let theme = theme::current(cx).clone();
let avatar_style = theme.workspace.titlebar.leader_avatar.clone();
let item_style = theme
.context_menu
.item
.inactive_state()
.disabled_style()
.clone();
self.user_menu.update(cx, |user_menu, cx| {
let items = if let Some(user) = self.user_store.read(cx).current_user() {
let items = if let Some(_) = self.user_store.read(cx).current_user() {
vec![
ContextMenuItem::Static(Box::new(move |_| {
Flex::row()
.with_children(user.avatar.clone().map(|avatar| {
Self::render_face(
avatar,
avatar_style.clone(),
Color::transparent_black(),
)
}))
.with_child(Label::new(
user.github_login.clone(),
item_style.label.clone(),
))
.contained()
.with_style(item_style.container)
.into_any()
})),
ContextMenuItem::action("Sign out", SignOut),
ContextMenuItem::action("Settings", zed_actions::OpenSettings),
ContextMenuItem::action("Theme", theme_selector::Toggle),
ContextMenuItem::separator(),
ContextMenuItem::action(
"Send Feedback",
"Share Feedback",
feedback::feedback_editor::GiveFeedback,
),
ContextMenuItem::action("Sign out", SignOut),
]
} else {
vec![
ContextMenuItem::action("Sign in", SignIn),
ContextMenuItem::action("Settings", zed_actions::OpenSettings),
ContextMenuItem::action("Theme", theme_selector::Toggle),
ContextMenuItem::separator(),
ContextMenuItem::action(
"Send Feedback",
"Share Feedback",
feedback::feedback_editor::GiveFeedback,
),
]
};
user_menu.show(Default::default(), AnchorCorner::TopRight, items, cx);
});
}
@@ -350,7 +326,7 @@ impl CollabTitlebarItem {
theme: &Theme,
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
let titlebar = &theme.workspace.titlebar;
let titlebar = &theme.titlebar;
let badge = if self
.user_store
@@ -391,7 +367,7 @@ impl CollabTitlebarItem {
.toggle_contacts_button
.in_state(self.contacts_popover.is_some())
.style_for(state);
Svg::new("icons/user_plus_16.svg")
Svg::new("icons/radix/person.svg")
.with_color(style.color)
.constrained()
.with_width(style.icon_width)
@@ -418,7 +394,6 @@ impl CollabTitlebarItem {
.with_children(self.render_contacts_popover_host(titlebar, cx))
.into_any()
}
fn render_toggle_screen_sharing_button(
&self,
theme: &Theme,
@@ -428,16 +403,21 @@ impl CollabTitlebarItem {
let icon;
let tooltip;
if room.read(cx).is_screen_sharing() {
icon = "icons/enable_screen_sharing_12.svg";
icon = "icons/radix/desktop.svg";
tooltip = "Stop Sharing Screen"
} else {
icon = "icons/disable_screen_sharing_12.svg";
icon = "icons/radix/desktop.svg";
tooltip = "Share Screen";
}
let titlebar = &theme.workspace.titlebar;
let active = room.read(cx).is_screen_sharing();
let titlebar = &theme.titlebar;
MouseEventHandler::<ToggleScreenSharing, Self>::new(0, cx, |state, _| {
let style = titlebar.call_control.style_for(state);
let style = titlebar
.screen_share_button
.in_state(active)
.style_for(state);
Svg::new(icon)
.with_color(style.color)
.constrained()
@@ -463,7 +443,141 @@ impl CollabTitlebarItem {
.aligned()
.into_any()
}
fn render_toggle_mute(
&self,
theme: &Theme,
room: &ModelHandle<Room>,
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
let icon;
let tooltip;
let is_muted = room.read(cx).is_muted();
if is_muted {
icon = "icons/radix/mic-mute.svg";
tooltip = "Unmute microphone\nRight click for options";
} else {
icon = "icons/radix/mic.svg";
tooltip = "Mute microphone\nRight click for options";
}
let titlebar = &theme.titlebar;
MouseEventHandler::<ToggleMute, Self>::new(0, cx, |state, _| {
let style = titlebar
.toggle_microphone_button
.in_state(is_muted)
.style_for(state);
let image = Svg::new(icon)
.with_color(style.color)
.constrained()
.with_width(style.icon_width)
.aligned()
.constrained()
.with_width(style.button_width)
.with_height(style.button_width)
.contained()
.with_style(style.container);
if let Some(color) = style.container.background_color {
image.with_background_color(color)
} else {
image
}
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, _, cx| {
toggle_mute(&Default::default(), cx)
})
.with_tooltip::<ToggleMute>(
0,
tooltip.into(),
Some(Box::new(ToggleMute)),
theme.tooltip.clone(),
cx,
)
.aligned()
.into_any()
}
fn render_toggle_deafen(
&self,
theme: &Theme,
room: &ModelHandle<Room>,
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
let icon;
let tooltip;
let is_deafened = room.read(cx).is_deafened().unwrap_or(false);
if is_deafened {
icon = "icons/radix/speaker-off.svg";
tooltip = "Unmute speakers\nRight click for options";
} else {
icon = "icons/radix/speaker-loud.svg";
tooltip = "Mute speakers\nRight click for options";
}
let titlebar = &theme.titlebar;
MouseEventHandler::<ToggleDeafen, Self>::new(0, cx, |state, _| {
let style = titlebar
.toggle_speakers_button
.in_state(is_deafened)
.style_for(state);
Svg::new(icon)
.with_color(style.color)
.constrained()
.with_width(style.icon_width)
.aligned()
.constrained()
.with_width(style.button_width)
.with_height(style.button_width)
.contained()
.with_style(style.container)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, _, cx| {
toggle_deafen(&Default::default(), cx)
})
.with_tooltip::<ToggleDeafen>(
0,
tooltip.into(),
Some(Box::new(ToggleDeafen)),
theme.tooltip.clone(),
cx,
)
.aligned()
.into_any()
}
fn render_leave_call(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let icon = "icons/radix/exit.svg";
let tooltip = "Leave call";
let titlebar = &theme.titlebar;
MouseEventHandler::<LeaveCall, Self>::new(0, cx, |state, _| {
let style = titlebar.leave_call_button.style_for(state);
Svg::new(icon)
.with_color(style.color)
.constrained()
.with_width(style.icon_width)
.aligned()
.constrained()
.with_width(style.button_width)
.with_height(style.button_width)
.contained()
.with_style(style.container)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, _, cx| {
ActiveCall::global(cx)
.update(cx, |call, cx| call.hang_up(cx))
.detach_and_log_err(cx);
})
.with_tooltip::<LeaveCall>(
0,
tooltip.into(),
Some(Box::new(LeaveCall)),
theme.tooltip.clone(),
cx,
)
.aligned()
.into_any()
}
fn render_in_call_share_unshare_button(
&self,
workspace: &ViewHandle<Workspace>,
@@ -476,14 +590,14 @@ impl CollabTitlebarItem {
}
let is_shared = project.read(cx).is_shared();
let label = if is_shared { "Unshare" } else { "Share" };
let label = if is_shared { "Stop Sharing" } else { "Share" };
let tooltip = if is_shared {
"Unshare project from call participants"
"Stop sharing project with call participants"
} else {
"Share project with call participants"
};
let titlebar = &theme.workspace.titlebar;
let titlebar = &theme.titlebar;
enum ShareUnshare {}
Some(
@@ -514,7 +628,7 @@ impl CollabTitlebarItem {
)
.aligned()
.contained()
.with_margin_left(theme.workspace.titlebar.item_spacing)
.with_margin_left(theme.titlebar.item_spacing)
.into_any(),
)
}
@@ -522,24 +636,51 @@ impl CollabTitlebarItem {
fn render_user_menu_button(
&self,
theme: &Theme,
avatar: Option<Arc<ImageData>>,
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
let titlebar = &theme.workspace.titlebar;
let tooltip = theme.tooltip.clone();
let user_menu_button_style = if avatar.is_some() {
&theme.titlebar.user_menu.user_menu_button_online
} else {
&theme.titlebar.user_menu.user_menu_button_offline
};
let avatar_style = &user_menu_button_style.avatar;
Stack::new()
.with_child(
MouseEventHandler::<ToggleUserMenu, Self>::new(0, cx, |state, _| {
let style = titlebar.call_control.style_for(state);
Svg::new("icons/ellipsis_14.svg")
.with_color(style.color)
.constrained()
.with_width(style.icon_width)
let style = user_menu_button_style
.user_menu
.inactive_state()
.style_for(state);
let mut dropdown = Flex::row().align_children_center();
if let Some(avatar_img) = avatar {
dropdown = dropdown.with_child(Self::render_face(
avatar_img,
*avatar_style,
Color::transparent_black(),
None,
));
};
dropdown
.with_child(
Svg::new("icons/caret_down_8.svg")
.with_color(user_menu_button_style.icon.color)
.constrained()
.with_width(user_menu_button_style.icon.width)
.contained()
.into_any(),
)
.aligned()
.constrained()
.with_width(style.button_width)
.with_height(style.button_width)
.with_height(style.width)
.contained()
.with_style(style.container)
.into_any()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, this, cx| {
@@ -549,11 +690,10 @@ impl CollabTitlebarItem {
0,
"Toggle user menu".to_owned(),
Some(Box::new(ToggleUserMenu)),
theme.tooltip.clone(),
tooltip,
cx,
)
.contained()
.with_margin_left(theme.workspace.titlebar.item_spacing),
.contained(),
)
.with_child(
ChildView::new(&self.user_menu, cx)
@@ -565,9 +705,9 @@ impl CollabTitlebarItem {
}
fn render_sign_in_button(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let titlebar = &theme.workspace.titlebar;
let titlebar = &theme.titlebar;
MouseEventHandler::<SignIn, Self>::new(0, cx, |state, _| {
let style = titlebar.sign_in_prompt.inactive_state().style_for(state);
let style = titlebar.sign_in_button.inactive_state().style_for(state);
Label::new("Sign In", style.text.clone())
.contained()
.with_style(style.container)
@@ -629,11 +769,13 @@ impl CollabTitlebarItem {
replica_id,
participant.peer_id,
Some(participant.location),
participant.muted,
participant.speaking,
workspace,
theme,
cx,
))
.with_margin_right(theme.workspace.titlebar.face_pile_spacing),
.with_margin_right(theme.titlebar.face_pile_spacing),
)
})
.collect()
@@ -645,19 +787,24 @@ impl CollabTitlebarItem {
theme: &Theme,
user: &Arc<User>,
peer_id: PeerId,
muted: bool,
speaking: bool,
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
let replica_id = workspace.read(cx).project().read(cx).replica_id();
Container::new(self.render_face_pile(
user,
Some(replica_id),
peer_id,
None,
muted,
speaking,
workspace,
theme,
cx,
))
.with_margin_right(theme.workspace.titlebar.item_spacing)
.with_margin_right(theme.titlebar.item_spacing)
.into_any()
}
@@ -667,6 +814,8 @@ impl CollabTitlebarItem {
replica_id: Option<ReplicaId>,
peer_id: PeerId,
location: Option<ParticipantLocation>,
muted: bool,
speaking: bool,
workspace: &ViewHandle<Workspace>,
theme: &Theme,
cx: &mut ViewContext<Self>,
@@ -689,15 +838,23 @@ impl CollabTitlebarItem {
})
.unwrap_or(false);
let leader_style = theme.workspace.titlebar.leader_avatar;
let follower_style = theme.workspace.titlebar.follower_avatar;
let leader_style = theme.titlebar.leader_avatar;
let follower_style = theme.titlebar.follower_avatar;
let microphone_state = if muted {
Some(theme.titlebar.muted)
} else if speaking {
Some(theme.titlebar.speaking)
} else {
None
};
let mut background_color = theme
.workspace
.titlebar
.container
.background_color
.unwrap_or_default();
if let Some(replica_id) = replica_id {
if followed_by_self {
let selection = theme.editor.replica_selection_style(replica_id).selection;
@@ -708,11 +865,12 @@ impl CollabTitlebarItem {
let mut content = Stack::new()
.with_children(user.avatar.as_ref().map(|avatar| {
let face_pile = FacePile::new(theme.workspace.titlebar.follower_avatar_overlap)
let face_pile = FacePile::new(theme.titlebar.follower_avatar_overlap)
.with_child(Self::render_face(
avatar.clone(),
Self::location_style(workspace, location, leader_style, cx),
background_color,
microphone_state,
))
.with_children(
(|| {
@@ -744,6 +902,7 @@ impl CollabTitlebarItem {
avatar.clone(),
follower_style,
background_color,
None,
))
}))
})()
@@ -753,7 +912,7 @@ impl CollabTitlebarItem {
let mut container = face_pile
.contained()
.with_style(theme.workspace.titlebar.leader_selection);
.with_style(theme.titlebar.leader_selection);
if let Some(replica_id) = replica_id {
if followed_by_self {
@@ -770,8 +929,8 @@ impl CollabTitlebarItem {
Some(
AvatarRibbon::new(color)
.constrained()
.with_width(theme.workspace.titlebar.avatar_ribbon.width)
.with_height(theme.workspace.titlebar.avatar_ribbon.height)
.with_width(theme.titlebar.avatar_ribbon.width)
.with_height(theme.titlebar.avatar_ribbon.height)
.aligned()
.bottom(),
)
@@ -862,12 +1021,13 @@ impl CollabTitlebarItem {
avatar: Arc<ImageData>,
avatar_style: AvatarStyle,
background_color: Color,
microphone_state: Option<Color>,
) -> AnyElement<V> {
Image::from_data(avatar)
.with_style(avatar_style.image)
.aligned()
.contained()
.with_background_color(background_color)
.with_background_color(microphone_state.unwrap_or(background_color))
.with_corner_radius(avatar_style.outer_corner_radius)
.constrained()
.with_width(avatar_style.outer_width)
@@ -891,22 +1051,22 @@ impl CollabTitlebarItem {
| client::Status::Reconnecting { .. }
| client::Status::ReconnectionError { .. } => Some(
Svg::new("icons/cloud_slash_12.svg")
.with_color(theme.workspace.titlebar.offline_icon.color)
.with_color(theme.titlebar.offline_icon.color)
.constrained()
.with_width(theme.workspace.titlebar.offline_icon.width)
.with_width(theme.titlebar.offline_icon.width)
.aligned()
.contained()
.with_style(theme.workspace.titlebar.offline_icon.container)
.with_style(theme.titlebar.offline_icon.container)
.into_any(),
),
client::Status::UpgradeRequired => Some(
MouseEventHandler::<ConnectionStatusButton, Self>::new(0, cx, |_, _| {
Label::new(
"Please update Zed to collaborate",
theme.workspace.titlebar.outdated_warning.text.clone(),
theme.titlebar.outdated_warning.text.clone(),
)
.contained()
.with_style(theme.workspace.titlebar.outdated_warning.container)
.with_style(theme.titlebar.outdated_warning.container)
.aligned()
})
.with_cursor_style(CursorStyle::PointingHand)
+38 -2
View File
@@ -9,13 +9,23 @@ mod notifications;
mod project_shared_notification;
mod sharing_status_indicator;
use call::ActiveCall;
use call::{ActiveCall, Room};
pub use collab_titlebar_item::{CollabTitlebarItem, ToggleContactsMenu};
use gpui::{actions, AppContext, Task};
use std::sync::Arc;
use util::ResultExt;
use workspace::AppState;
actions!(collab, [ToggleScreenSharing]);
actions!(
collab,
[
ToggleScreenSharing,
ToggleMute,
ToggleDeafen,
LeaveCall,
ShareMicrophone
]
);
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
collab_titlebar_item::init(cx);
@@ -27,6 +37,9 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
sharing_status_indicator::init(cx);
cx.add_global_action(toggle_screen_sharing);
cx.add_global_action(toggle_mute);
cx.add_global_action(toggle_deafen);
cx.add_global_action(share_microphone);
}
pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
@@ -41,3 +54,26 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
toggle_screen_sharing.detach_and_log_err(cx);
}
}
pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
room.update(cx, Room::toggle_mute)
.map(|task| task.detach_and_log_err(cx))
.log_err();
}
}
pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
room.update(cx, Room::toggle_deafen)
.map(|task| task.detach_and_log_err(cx))
.log_err();
}
}
pub fn share_microphone(_: &ShareMicrophone, cx: &mut AppContext) {
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
room.update(cx, Room::share_microphone)
.detach_and_log_err(cx)
}
}
+2 -2
View File
@@ -514,10 +514,10 @@ impl ContactList {
project_id: project.id,
worktree_root_names: project.worktree_root_names.clone(),
host_user_id: participant.user.id,
is_last: projects.peek().is_none() && participant.tracks.is_empty(),
is_last: projects.peek().is_none() && participant.video_tracks.is_empty(),
});
}
if !participant.tracks.is_empty() {
if !participant.video_tracks.is_empty() {
participant_entries.push(ContactEntry::ParticipantScreen {
peer_id: participant.peer_id,
is_last: true,
+4 -1
View File
@@ -7,6 +7,7 @@ use std::{
fmt::{self, Display},
marker::PhantomData,
mem,
panic::Location,
pin::Pin,
rc::Rc,
sync::Arc,
@@ -965,10 +966,12 @@ impl<T> Task<T> {
}
impl<T: 'static, E: 'static + Display> Task<Result<T, E>> {
#[track_caller]
pub fn detach_and_log_err(self, cx: &mut AppContext) {
let caller = Location::caller();
cx.spawn(|_| async move {
if let Err(err) = self.await {
log::error!("{:#}", err);
log::error!("{}:{}: {:#}", caller.file(), caller.line(), err);
}
})
.detach();
@@ -8,14 +8,18 @@ class LKRoomDelegate: RoomDelegate {
var onDidDisconnect: @convention(c) (UnsafeRawPointer) -> Void
var onDidSubscribeToRemoteAudioTrack: @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void
var onDidUnsubscribeFromRemoteAudioTrack: @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void
var onMuteChangedFromRemoteAudioTrack: @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void
var onActiveSpeakersChanged: @convention(c) (UnsafeRawPointer, CFArray) -> Void
var onDidSubscribeToRemoteVideoTrack: @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void
var onDidUnsubscribeFromRemoteVideoTrack: @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void
init(
data: UnsafeRawPointer,
onDidDisconnect: @escaping @convention(c) (UnsafeRawPointer) -> Void,
onDidSubscribeToRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void,
onDidUnsubscribeFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void,
onMuteChangedFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void,
onActiveSpeakersChanged: @convention(c) (UnsafeRawPointer, CFArray) -> Void,
onDidSubscribeToRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void,
onDidUnsubscribeFromRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void)
{
@@ -25,6 +29,8 @@ class LKRoomDelegate: RoomDelegate {
self.onDidUnsubscribeFromRemoteAudioTrack = onDidUnsubscribeFromRemoteAudioTrack
self.onDidSubscribeToRemoteVideoTrack = onDidSubscribeToRemoteVideoTrack
self.onDidUnsubscribeFromRemoteVideoTrack = onDidUnsubscribeFromRemoteVideoTrack
self.onMuteChangedFromRemoteAudioTrack = onMuteChangedFromRemoteAudioTrack
self.onActiveSpeakersChanged = onActiveSpeakersChanged
}
func room(_ room: Room, didUpdate connectionState: ConnectionState, oldValue: ConnectionState) {
@@ -40,6 +46,17 @@ class LKRoomDelegate: RoomDelegate {
self.onDidSubscribeToRemoteAudioTrack(self.data, participant.identity as CFString, track.sid! as CFString, Unmanaged.passUnretained(track).toOpaque())
}
}
func room(_ room: Room, participant: Participant, didUpdate publication: TrackPublication, muted: Bool) {
if publication.kind == .audio {
self.onMuteChangedFromRemoteAudioTrack(self.data, publication.sid as CFString, muted)
}
}
func room(_ room: Room, didUpdate speakers: [Participant]) {
guard let speaker_ids = speakers.compactMap({ $0.identity as CFString }) as CFArray? else { return }
self.onActiveSpeakersChanged(self.data, speaker_ids)
}
func room(_ room: Room, participant: RemoteParticipant, didUnsubscribe publication: RemoteTrackPublication, track: Track) {
if track.kind == .video {
@@ -89,6 +106,8 @@ public func LKRoomDelegateCreate(
onDidDisconnect: @escaping @convention(c) (UnsafeRawPointer) -> Void,
onDidSubscribeToRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void,
onDidUnsubscribeFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void,
onMuteChangedFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void,
onActiveSpeakerChanged: @escaping @convention(c) (UnsafeRawPointer, CFArray) -> Void,
onDidSubscribeToRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void,
onDidUnsubscribeFromRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void
) -> UnsafeMutableRawPointer {
@@ -97,6 +116,8 @@ public func LKRoomDelegateCreate(
onDidDisconnect: onDidDisconnect,
onDidSubscribeToRemoteAudioTrack: onDidSubscribeToRemoteAudioTrack,
onDidUnsubscribeFromRemoteAudioTrack: onDidUnsubscribeFromRemoteAudioTrack,
onMuteChangedFromRemoteAudioTrack: onMuteChangedFromRemoteAudioTrack,
onActiveSpeakersChanged: onActiveSpeakerChanged,
onDidSubscribeToRemoteVideoTrack: onDidSubscribeToRemoteVideoTrack,
onDidUnsubscribeFromRemoteVideoTrack: onDidUnsubscribeFromRemoteVideoTrack
)
@@ -169,6 +190,18 @@ public func LKRoomAudioTracksForRemoteParticipant(room: UnsafeRawPointer, partic
return nil;
}
@_cdecl("LKRoomAudioTrackPublicationsForRemoteParticipant")
public func LKRoomAudioTrackPublicationsForRemoteParticipant(room: UnsafeRawPointer, participantId: CFString) -> CFArray? {
let room = Unmanaged<Room>.fromOpaque(room).takeUnretainedValue()
for (_, participant) in room.remoteParticipants {
if participant.identity == participantId as String {
return participant.audioTracks.compactMap { $0 as? RemoteTrackPublication } as CFArray?
}
}
return nil;
}
@_cdecl("LKRoomVideoTracksForRemoteParticipant")
public func LKRoomVideoTracksForRemoteParticipant(room: UnsafeRawPointer, participantId: CFString) -> CFArray? {
@@ -201,19 +234,6 @@ public func LKCreateScreenShareTrackForDisplay(display: UnsafeMutableRawPointer)
return Unmanaged.passRetained(track).toOpaque()
}
@_cdecl("LKRemoteAudioTrackStart")
public func LKRemoteAudioTrackStart(track: UnsafeRawPointer, onStart: @escaping @convention(c) (UnsafeRawPointer, Bool) -> Void, callbackData: UnsafeRawPointer) {
let track = Unmanaged<Track>.fromOpaque(track).takeUnretainedValue() as! RemoteAudioTrack
track.start().then { success in
onStart(callbackData, success)
}
.catch { _ in
onStart(callbackData, false)
}
}
@_cdecl("LKVideoRendererCreate")
public func LKVideoRendererCreate(data: UnsafeRawPointer, onFrame: @escaping @convention(c) (UnsafeRawPointer, CVPixelBuffer) -> Bool, onDrop: @escaping @convention(c) (UnsafeRawPointer) -> Void) -> UnsafeMutableRawPointer {
Unmanaged.passRetained(LKVideoRenderer(data: data, onFrame: onFrame, onDrop: onDrop)).toOpaque()
@@ -247,3 +267,43 @@ public func LKDisplaySources(data: UnsafeRawPointer, callback: @escaping @conven
callback(data, nil, error.localizedDescription as CFString)
}
}
@_cdecl("LKLocalTrackPublicationSetMute")
public func LKLocalTrackPublicationSetMute(
publication: UnsafeRawPointer,
muted: Bool,
on_complete: @escaping @convention(c) (UnsafeRawPointer, CFString?) -> Void,
callback_data: UnsafeRawPointer
) {
let publication = Unmanaged<LocalTrackPublication>.fromOpaque(publication).takeUnretainedValue()
if muted {
publication.mute().then {
on_complete(callback_data, nil)
}.catch { error in
on_complete(callback_data, error.localizedDescription as CFString)
}
} else {
publication.unmute().then {
on_complete(callback_data, nil)
}.catch { error in
on_complete(callback_data, error.localizedDescription as CFString)
}
}
}
@_cdecl("LKRemoteTrackPublicationSetEnabled")
public func LKRemoteTrackPublicationSetEnabled(
publication: UnsafeRawPointer,
enabled: Bool,
on_complete: @escaping @convention(c) (UnsafeRawPointer, CFString?) -> Void,
callback_data: UnsafeRawPointer
) {
let publication = Unmanaged<RemoteTrackPublication>.fromOpaque(publication).takeUnretainedValue()
publication.set(enabled: enabled).then {
on_complete(callback_data, nil)
}.catch { error in
on_complete(callback_data, error.localizedDescription as CFString)
}
}
+34 -2
View File
@@ -74,19 +74,51 @@ fn main() {
panic!("unexpected message");
}
audio_track_publication.set_mute(true).await.unwrap();
println!("waiting for mute changed!");
if let RemoteAudioTrackUpdate::MuteChanged { track_id, muted } =
audio_track_updates.next().await.unwrap()
{
let remote_tracks = room_b.remote_audio_tracks("test-participant-1");
assert_eq!(remote_tracks[0].sid(), track_id);
assert_eq!(muted, true);
} else {
panic!("unexpected message");
}
audio_track_publication.set_mute(false).await.unwrap();
if let RemoteAudioTrackUpdate::MuteChanged { track_id, muted } =
audio_track_updates.next().await.unwrap()
{
let remote_tracks = room_b.remote_audio_tracks("test-participant-1");
assert_eq!(remote_tracks[0].sid(), track_id);
assert_eq!(muted, false);
} else {
panic!("unexpected message");
}
println!("Pausing for 5 seconds to test audio, make some noise!");
let timer = cx.background().timer(Duration::from_secs(5));
timer.await;
let remote_audio_track = room_b
.remote_audio_tracks("test-participant-1")
.pop()
.unwrap();
room_a.unpublish_track(audio_track_publication);
// Clear out any active speakers changed messages
let mut next = audio_track_updates.next().await.unwrap();
while let RemoteAudioTrackUpdate::ActiveSpeakersChanged { speakers } = next {
println!("Speakers changed: {:?}", speakers);
next = audio_track_updates.next().await.unwrap();
}
if let RemoteAudioTrackUpdate::Unsubscribed {
publisher_id,
track_id,
} = audio_track_updates.next().await.unwrap()
} = next
{
assert_eq!(publisher_id, "test-participant-1");
assert_eq!(remote_audio_track.sid(), track_id);
+202 -9
View File
@@ -32,6 +32,15 @@ extern "C" {
publisher_id: CFStringRef,
track_id: CFStringRef,
),
on_mute_changed_from_remote_audio_track: extern "C" fn(
callback_data: *mut c_void,
track_id: CFStringRef,
muted: bool,
),
on_active_speakers_changed: extern "C" fn(
callback_data: *mut c_void,
participants: CFArrayRef,
),
on_did_subscribe_to_remote_video_track: extern "C" fn(
callback_data: *mut c_void,
publisher_id: CFStringRef,
@@ -72,6 +81,11 @@ extern "C" {
participant_id: CFStringRef,
) -> CFArrayRef;
fn LKRoomAudioTrackPublicationsForRemoteParticipant(
room: *const c_void,
participant_id: CFStringRef,
) -> CFArrayRef;
fn LKRoomVideoTracksForRemoteParticipant(
room: *const c_void,
participant_id: CFStringRef,
@@ -84,12 +98,6 @@ extern "C" {
) -> *const c_void;
fn LKRemoteAudioTrackGetSid(track: *const c_void) -> CFStringRef;
// fn LKRemoteAudioTrackStart(
// track: *const c_void,
// callback: extern "C" fn(*mut c_void, bool),
// callback_data: *mut c_void
// );
fn LKVideoTrackAddRenderer(track: *const c_void, renderer: *const c_void);
fn LKRemoteVideoTrackGetSid(track: *const c_void) -> CFStringRef;
@@ -103,6 +111,20 @@ extern "C" {
);
fn LKCreateScreenShareTrackForDisplay(display: *const c_void) -> *const c_void;
fn LKLocalAudioTrackCreateTrack() -> *const c_void;
fn LKLocalTrackPublicationSetMute(
publication: *const c_void,
muted: bool,
on_complete: extern "C" fn(callback_data: *mut c_void, error: CFStringRef),
callback_data: *mut c_void,
);
fn LKRemoteTrackPublicationSetEnabled(
publication: *const c_void,
enabled: bool,
on_complete: extern "C" fn(callback_data: *mut c_void, error: CFStringRef),
callback_data: *mut c_void,
);
}
pub type Sid = String;
@@ -206,7 +228,7 @@ impl Room {
let tx =
unsafe { Box::from_raw(tx as *mut oneshot::Sender<Result<LocalTrackPublication>>) };
if error.is_null() {
let _ = tx.send(Ok(LocalTrackPublication(publication)));
let _ = tx.send(Ok(LocalTrackPublication::new(publication)));
} else {
let error = unsafe { CFString::wrap_under_get_rule(error).to_string() };
let _ = tx.send(Err(anyhow!(error)));
@@ -232,7 +254,7 @@ impl Room {
let tx =
unsafe { Box::from_raw(tx as *mut oneshot::Sender<Result<LocalTrackPublication>>) };
if error.is_null() {
let _ = tx.send(Ok(LocalTrackPublication(publication)));
let _ = tx.send(Ok(LocalTrackPublication::new(publication)));
} else {
let error = unsafe { CFString::wrap_under_get_rule(error).to_string() };
let _ = tx.send(Err(anyhow!(error)));
@@ -246,7 +268,7 @@ impl Room {
Box::into_raw(Box::new(tx)) as *mut c_void,
);
}
async { rx.await.unwrap().context("error publishing video track") }
async { rx.await.unwrap().context("error publishing audio track") }
}
pub fn unpublish_track(&self, publication: LocalTrackPublication) {
@@ -313,6 +335,31 @@ impl Room {
}
}
pub fn remote_audio_track_publications(
&self,
participant_id: &str,
) -> Vec<Arc<RemoteTrackPublication>> {
unsafe {
let tracks = LKRoomAudioTrackPublicationsForRemoteParticipant(
self.native_room,
CFString::new(participant_id).as_concrete_TypeRef(),
);
if tracks.is_null() {
Vec::new()
} else {
let tracks = CFArray::wrap_under_get_rule(tracks);
tracks
.into_iter()
.map(|native_track_publication| {
let native_track_publication = *native_track_publication;
Arc::new(RemoteTrackPublication::new(native_track_publication))
})
.collect()
}
}
}
pub fn remote_audio_track_updates(&self) -> mpsc::UnboundedReceiver<RemoteAudioTrackUpdate> {
let (tx, rx) = mpsc::unbounded();
self.remote_audio_track_subscribers.lock().push(tx);
@@ -343,6 +390,28 @@ impl Room {
});
}
fn mute_changed_from_remote_audio_track(&self, track_id: String, muted: bool) {
self.remote_audio_track_subscribers.lock().retain(|tx| {
tx.unbounded_send(RemoteAudioTrackUpdate::MuteChanged {
track_id: track_id.clone(),
muted,
})
.is_ok()
});
}
// A vec of publisher IDs
fn active_speakers_changed(&self, speakers: Vec<String>) {
self.remote_audio_track_subscribers
.lock()
.retain(move |tx| {
tx.unbounded_send(RemoteAudioTrackUpdate::ActiveSpeakersChanged {
speakers: speakers.clone(),
})
.is_ok()
});
}
fn did_subscribe_to_remote_video_track(&self, track: RemoteVideoTrack) {
let track = Arc::new(track);
self.remote_video_track_subscribers.lock().retain(|tx| {
@@ -407,6 +476,8 @@ impl RoomDelegate {
Self::on_did_disconnect,
Self::on_did_subscribe_to_remote_audio_track,
Self::on_did_unsubscribe_from_remote_audio_track,
Self::on_mute_change_from_remote_audio_track,
Self::on_active_speakers_changed,
Self::on_did_subscribe_to_remote_video_track,
Self::on_did_unsubscribe_from_remote_video_track,
)
@@ -455,6 +526,42 @@ impl RoomDelegate {
let _ = Weak::into_raw(room);
}
extern "C" fn on_mute_change_from_remote_audio_track(
room: *mut c_void,
track_id: CFStringRef,
muted: bool,
) {
let room = unsafe { Weak::from_raw(room as *mut Room) };
let track_id = unsafe { CFString::wrap_under_get_rule(track_id).to_string() };
if let Some(room) = room.upgrade() {
room.mute_changed_from_remote_audio_track(track_id, muted);
}
let _ = Weak::into_raw(room);
}
extern "C" fn on_active_speakers_changed(room: *mut c_void, participants: CFArrayRef) {
if participants.is_null() {
return;
}
let room = unsafe { Weak::from_raw(room as *mut Room) };
let speakers = unsafe {
CFArray::wrap_under_get_rule(participants)
.into_iter()
.map(
|speaker: core_foundation::base::ItemRef<'_, *const c_void>| {
CFString::wrap_under_get_rule(*speaker as CFStringRef).to_string()
},
)
.collect()
};
if let Some(room) = room.upgrade() {
room.active_speakers_changed(speakers);
}
let _ = Weak::into_raw(room);
}
extern "C" fn on_did_subscribe_to_remote_video_track(
room: *mut c_void,
publisher_id: CFStringRef,
@@ -525,12 +632,88 @@ impl Drop for LocalVideoTrack {
pub struct LocalTrackPublication(*const c_void);
impl LocalTrackPublication {
pub fn new(native_track_publication: *const c_void) -> Self {
unsafe {
CFRetain(native_track_publication);
}
Self(native_track_publication)
}
pub fn set_mute(&self, muted: bool) -> impl Future<Output = Result<()>> {
let (tx, rx) = futures::channel::oneshot::channel();
extern "C" fn complete_callback(callback_data: *mut c_void, error: CFStringRef) {
let tx = unsafe { Box::from_raw(callback_data as *mut oneshot::Sender<Result<()>>) };
if error.is_null() {
tx.send(Ok(())).ok();
} else {
let error = unsafe { CFString::wrap_under_get_rule(error).to_string() };
tx.send(Err(anyhow!(error))).ok();
}
}
unsafe {
LKLocalTrackPublicationSetMute(
self.0,
muted,
complete_callback,
Box::into_raw(Box::new(tx)) as *mut c_void,
)
}
async move { rx.await.unwrap() }
}
}
impl Drop for LocalTrackPublication {
fn drop(&mut self) {
unsafe { CFRelease(self.0) }
}
}
pub struct RemoteTrackPublication(*const c_void);
impl RemoteTrackPublication {
pub fn new(native_track_publication: *const c_void) -> Self {
unsafe {
CFRetain(native_track_publication);
}
Self(native_track_publication)
}
pub fn set_enabled(&self, enabled: bool) -> impl Future<Output = Result<()>> {
let (tx, rx) = futures::channel::oneshot::channel();
extern "C" fn complete_callback(callback_data: *mut c_void, error: CFStringRef) {
let tx = unsafe { Box::from_raw(callback_data as *mut oneshot::Sender<Result<()>>) };
if error.is_null() {
tx.send(Ok(())).ok();
} else {
let error = unsafe { CFString::wrap_under_get_rule(error).to_string() };
tx.send(Err(anyhow!(error))).ok();
}
}
unsafe {
LKRemoteTrackPublicationSetEnabled(
self.0,
enabled,
complete_callback,
Box::into_raw(Box::new(tx)) as *mut c_void,
)
}
async move { rx.await.unwrap() }
}
}
impl Drop for RemoteTrackPublication {
fn drop(&mut self) {
unsafe { CFRelease(self.0) }
}
}
#[derive(Debug)]
pub struct RemoteAudioTrack {
_native_track: *const c_void,
@@ -557,6 +740,14 @@ impl RemoteAudioTrack {
pub fn publisher_id(&self) -> &str {
&self.publisher_id
}
pub fn enable(&self) -> impl Future<Output = Result<()>> {
async { Ok(()) }
}
pub fn disable(&self) -> impl Future<Output = Result<()>> {
async { Ok(()) }
}
}
#[derive(Debug)]
@@ -639,6 +830,8 @@ pub enum RemoteVideoTrackUpdate {
}
pub enum RemoteAudioTrackUpdate {
ActiveSpeakersChanged { speakers: Vec<Sid> },
MuteChanged { track_id: Sid, muted: bool },
Subscribed(Arc<RemoteAudioTrack>),
Unsubscribed { publisher_id: Sid, track_id: Sid },
}
+42
View File
@@ -410,6 +410,23 @@ impl Room {
.collect()
}
pub fn remote_audio_track_publications(
&self,
publisher_id: &str,
) -> Vec<Arc<RemoteTrackPublication>> {
if !self.is_connected() {
return Vec::new();
}
self.test_server()
.audio_tracks(self.token())
.unwrap()
.into_iter()
.filter(|track| track.publisher_id() == publisher_id)
.map(|_track| Arc::new(RemoteTrackPublication {}))
.collect()
}
pub fn remote_video_tracks(&self, publisher_id: &str) -> Vec<Arc<RemoteVideoTrack>> {
if !self.is_connected() {
return Vec::new();
@@ -475,6 +492,20 @@ impl Drop for Room {
pub struct LocalTrackPublication;
impl LocalTrackPublication {
pub fn set_mute(&self, _mute: bool) -> impl Future<Output = Result<()>> {
async { Ok(()) }
}
}
pub struct RemoteTrackPublication;
impl RemoteTrackPublication {
pub fn set_enabled(&self, _enabled: bool) -> impl Future<Output = Result<()>> {
async { Ok(()) }
}
}
#[derive(Clone)]
pub struct LocalVideoTrack {
frames_rx: async_broadcast::Receiver<Frame>,
@@ -517,6 +548,7 @@ impl RemoteVideoTrack {
}
}
#[derive(Debug)]
pub struct RemoteAudioTrack {
sid: Sid,
publisher_id: Sid,
@@ -530,6 +562,14 @@ impl RemoteAudioTrack {
pub fn publisher_id(&self) -> &str {
&self.publisher_id
}
pub fn enable(&self) -> impl Future<Output = Result<()>> {
async { Ok(()) }
}
pub fn disable(&self) -> impl Future<Output = Result<()>> {
async { Ok(()) }
}
}
#[derive(Clone)]
@@ -540,6 +580,8 @@ pub enum RemoteVideoTrackUpdate {
#[derive(Clone)]
pub enum RemoteAudioTrackUpdate {
ActiveSpeakersChanged { speakers: Vec<Sid> },
MuteChanged { track_id: Sid, muted: bool },
Subscribed(Arc<RemoteAudioTrack>),
Unsubscribed { publisher_id: Sid, track_id: Sid },
}
+8
View File
@@ -45,6 +45,12 @@ pub trait PickerDelegate: Sized + 'static {
fn center_selection_after_match_updates(&self) -> bool {
false
}
fn render_header(&self, _cx: &AppContext) -> Option<AnyElement<Picker<Self>>> {
None
}
fn render_footer(&self, _cx: &AppContext) -> Option<AnyElement<Picker<Self>>> {
None
}
}
impl<D: PickerDelegate> Entity for Picker<D> {
@@ -77,6 +83,7 @@ impl<D: PickerDelegate> View for Picker<D> {
.contained()
.with_style(editor_style),
)
.with_children(self.delegate.render_header(cx))
.with_children(if match_count == 0 {
if query.is_empty() {
None
@@ -118,6 +125,7 @@ impl<D: PickerDelegate> View for Picker<D> {
.into_any(),
)
})
.with_children(self.delegate.render_footer(cx))
.contained()
.with_style(container_style)
.constrained()
+22 -4
View File
@@ -66,6 +66,7 @@ pub struct Theme {
pub feedback: FeedbackStyle,
pub welcome: WelcomeStyle,
pub color_scheme: ColorScheme,
pub titlebar: Titlebar,
}
#[derive(Deserialize, Default, Clone, JsonSchema)]
@@ -80,7 +81,6 @@ pub struct ThemeMeta {
pub struct Workspace {
pub background: Color,
pub blank_pane: BlankPaneStyle,
pub titlebar: Titlebar,
pub tab_bar: TabBar,
pub pane_divider: Border,
pub leader_border_opacity: f32,
@@ -129,13 +129,31 @@ pub struct Titlebar {
pub leader_avatar: AvatarStyle,
pub follower_avatar: AvatarStyle,
pub inactive_avatar_grayscale: bool,
pub sign_in_prompt: Toggleable<Interactive<ContainedText>>,
pub sign_in_button: Toggleable<Interactive<ContainedText>>,
pub outdated_warning: ContainedText,
pub share_button: Toggleable<Interactive<ContainedText>>,
pub call_control: Interactive<IconButton>,
pub muted: Color,
pub speaking: Color,
pub screen_share_button: Toggleable<Interactive<IconButton>>,
pub toggle_contacts_button: Toggleable<Interactive<IconButton>>,
pub user_menu_button: Toggleable<Interactive<IconButton>>,
pub toggle_microphone_button: Toggleable<Interactive<IconButton>>,
pub toggle_speakers_button: Toggleable<Interactive<IconButton>>,
pub leave_call_button: Interactive<IconButton>,
pub toggle_contacts_badge: ContainerStyle,
pub user_menu: UserMenu,
}
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct UserMenu {
pub user_menu_button_online: UserMenuButton,
pub user_menu_button_offline: UserMenuButton,
}
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct UserMenuButton {
pub user_menu: Toggleable<Interactive<Icon>>,
pub avatar: AvatarStyle,
pub icon: Icon,
}
#[derive(Copy, Clone, Deserialize, Default, JsonSchema)]
+4 -4
View File
@@ -2296,11 +2296,11 @@ impl Workspace {
// (https://github.com/zed-industries/zed/issues/1290)
let is_fullscreen = cx.window_is_fullscreen();
let container_theme = if is_fullscreen {
let mut container_theme = theme.workspace.titlebar.container;
let mut container_theme = theme.titlebar.container;
container_theme.padding.left = container_theme.padding.right;
container_theme
} else {
theme.workspace.titlebar.container
theme.titlebar.container
};
enum TitleBar {}
@@ -2320,7 +2320,7 @@ impl Workspace {
}
})
.constrained()
.with_height(theme.workspace.titlebar.height)
.with_height(theme.titlebar.height)
.into_any_named("titlebar")
}
@@ -2765,7 +2765,7 @@ impl Workspace {
let call = self.active_call()?;
let room = call.read(cx).room()?.read(cx);
let participant = room.remote_participant_for_peer_id(peer_id)?;
let track = participant.tracks.values().next()?.clone();
let track = participant.video_tracks.values().next()?.clone();
let user = participant.user.clone();
for item in pane.read(cx).items_of_type::<SharedScreen>() {
+10
View File
@@ -0,0 +1,10 @@
[package]
name = "zed-actions"
version = "0.1.0"
edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
gpui = { path = "../gpui" }
+28
View File
@@ -0,0 +1,28 @@
use gpui::actions;
actions!(
zed,
[
About,
Hide,
HideOthers,
ShowAll,
Minimize,
Zoom,
ToggleFullScreen,
Quit,
DebugElements,
OpenLog,
OpenLicenses,
OpenTelemetryLog,
OpenKeymap,
OpenSettings,
OpenLocalSettings,
OpenDefaultSettings,
OpenDefaultKeymap,
IncreaseBufferFontSize,
DecreaseBufferFontSize,
ResetBufferFontSize,
ResetDatabase,
]
);
+1 -1
View File
@@ -66,7 +66,7 @@ util = { path = "../util" }
vim = { path = "../vim" }
workspace = { path = "../workspace" }
welcome = { path = "../welcome" }
zed-actions = {path = "../zed-actions"}
anyhow.workspace = true
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
async-tar = "0.4.2"
+1 -1
View File
@@ -889,6 +889,6 @@ pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] {
("Go to file", &file_finder::Toggle),
("Open command palette", &command_palette::Toggle),
("Open recent projects", &recent_projects::OpenRecent),
("Change your settings", &zed::OpenSettings),
("Change your settings", &zed_actions::OpenSettings),
]
}
+3 -30
View File
@@ -20,7 +20,6 @@ use feedback::{
};
use futures::{channel::mpsc, StreamExt};
use gpui::{
actions,
anyhow::{self, Result},
geometry::vector::vec2f,
impl_actions,
@@ -50,6 +49,7 @@ use workspace::{
notifications::simple_message_notification::MessageNotification, open_new, AppState, NewFile,
NewWindow, Workspace, WorkspaceSettings,
};
use zed_actions::*;
#[derive(Deserialize, Clone, PartialEq)]
pub struct OpenBrowser {
@@ -58,33 +58,6 @@ pub struct OpenBrowser {
impl_actions!(zed, [OpenBrowser]);
actions!(
zed,
[
About,
Hide,
HideOthers,
ShowAll,
Minimize,
Zoom,
ToggleFullScreen,
Quit,
DebugElements,
OpenLog,
OpenLicenses,
OpenTelemetryLog,
OpenKeymap,
OpenSettings,
OpenLocalSettings,
OpenDefaultSettings,
OpenDefaultKeymap,
IncreaseBufferFontSize,
DecreaseBufferFontSize,
ResetBufferFontSize,
ResetDatabase,
]
);
pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
cx.add_action(about);
cx.add_global_action(|_: &Hide, cx: &mut gpui::AppContext| {
@@ -735,8 +708,8 @@ mod tests {
use editor::{scroll::autoscroll::Autoscroll, DisplayPoint, Editor};
use fs::{FakeFs, Fs};
use gpui::{
elements::Empty, executor::Deterministic, Action, AnyElement, AppContext, AssetSource,
Element, Entity, TestAppContext, View, ViewHandle,
actions, elements::Empty, executor::Deterministic, Action, AnyElement, AppContext,
AssetSource, Element, Entity, TestAppContext, View, ViewHandle,
};
use language::LanguageRegistry;
use node_runtime::NodeRuntime;