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:
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 },
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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>() {
|
||||
|
||||
@@ -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" }
|
||||
@@ -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,
|
||||
]
|
||||
);
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user