Show shared screen as a pane item

Antonio Scandurra created

Change summary

Cargo.lock                             |  16 --
crates/call/Cargo.toml                 |   1 
crates/call/src/call.rs                |   2 
crates/call/src/participant.rs         |  14 -
crates/call/src/room.rs                |  65 ++++------
crates/collab/src/integration_tests.rs |  26 ---
crates/language/Cargo.toml             |   2 
crates/workspace/src/pane_group.rs     |  35 +----
crates/workspace/src/shared_screen.rs  | 175 ++++++++++++++++++++++++++++
crates/workspace/src/workspace.rs      |  76 +++++++++--
10 files changed, 286 insertions(+), 126 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -170,17 +170,6 @@ dependencies = [
  "rust-embed",
 ]
 
-[[package]]
-name = "async-broadcast"
-version = "0.3.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90622698a1218e0b2fb846c97b5f19a0831f6baddee73d9454156365ccfa473b"
-dependencies = [
- "easy-parallel",
- "event-listener",
- "futures-core",
-]
-
 [[package]]
 name = "async-broadcast"
 version = "0.4.1"
@@ -727,6 +716,7 @@ name = "call"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "async-broadcast",
  "client",
  "collections",
  "futures 0.3.24",
@@ -2983,7 +2973,7 @@ name = "language"
 version = "0.1.0"
 dependencies = [
  "anyhow",
- "async-broadcast 0.3.4",
+ "async-broadcast",
  "async-trait",
  "client",
  "clock",
@@ -3156,7 +3146,7 @@ name = "live_kit_client"
 version = "0.1.0"
 dependencies = [
  "anyhow",
- "async-broadcast 0.4.1",
+ "async-broadcast",
  "async-trait",
  "block",
  "byteorder",

crates/call/Cargo.toml 🔗

@@ -27,6 +27,7 @@ project = { path = "../project" }
 util = { path = "../util" }
 
 anyhow = "1.0.38"
+async-broadcast = "0.4"
 futures = "0.3"
 postage = { version = "0.4.1", features = ["futures-traits"] }
 

crates/call/src/participant.rs 🔗

@@ -1,8 +1,8 @@
 use anyhow::{anyhow, Result};
 use client::{proto, User};
 use collections::HashMap;
-use gpui::{Task, WeakModelHandle};
-use live_kit_client::Frame;
+use gpui::WeakModelHandle;
+pub use live_kit_client::Frame;
 use project::Project;
 use std::sync::Arc;
 
@@ -41,18 +41,16 @@ pub struct RemoteParticipant {
     pub user: Arc<User>,
     pub projects: Vec<proto::ParticipantProject>,
     pub location: ParticipantLocation,
-    pub tracks: HashMap<live_kit_client::Sid, RemoteVideoTrack>,
+    pub tracks: HashMap<live_kit_client::Sid, Arc<RemoteVideoTrack>>,
 }
 
 #[derive(Clone)]
 pub struct RemoteVideoTrack {
-    pub(crate) frame: Option<Frame>,
-    pub(crate) _live_kit_track: Arc<live_kit_client::RemoteVideoTrack>,
-    pub(crate) _maintain_frame: Arc<Task<()>>,
+    pub(crate) live_kit_track: Arc<live_kit_client::RemoteVideoTrack>,
 }
 
 impl RemoteVideoTrack {
-    pub fn frame(&self) -> Option<&Frame> {
-        self.frame.as_ref()
+    pub fn frames(&self) -> async_broadcast::Receiver<Frame> {
+        self.live_kit_track.frames()
     }
 }

crates/call/src/room.rs 🔗

@@ -7,7 +7,7 @@ use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore};
 use collections::{BTreeMap, HashSet};
 use futures::StreamExt;
 use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task};
-use live_kit_client::{LocalTrackPublication, LocalVideoTrack, RemoteVideoTrackUpdate};
+use live_kit_client::{LocalTrackPublication, LocalVideoTrack, RemoteVideoTrackUpdate, Sid};
 use postage::stream::Stream;
 use project::Project;
 use std::{mem, os::unix::prelude::OsStrExt, sync::Arc};
@@ -15,9 +15,16 @@ use util::{post_inc, ResultExt};
 
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub enum Event {
-    Frame {
+    ParticipantLocationChanged {
         participant_id: PeerId,
-        track_id: live_kit_client::Sid,
+    },
+    RemoteVideoTrackShared {
+        participant_id: PeerId,
+        track_id: Sid,
+    },
+    RemoteVideoTrackUnshared {
+        peer_id: PeerId,
+        track_id: Sid,
     },
     RemoteProjectShared {
         owner: Arc<User>,
@@ -356,7 +363,12 @@ impl Room {
                         if let Some(remote_participant) = this.remote_participants.get_mut(&peer_id)
                         {
                             remote_participant.projects = participant.projects;
-                            remote_participant.location = location;
+                            if location != remote_participant.location {
+                                remote_participant.location = location;
+                                cx.emit(Event::ParticipantLocationChanged {
+                                    participant_id: peer_id,
+                                });
+                            }
                         } else {
                             this.remote_participants.insert(
                                 peer_id,
@@ -430,44 +442,16 @@ impl Room {
                     .remote_participants
                     .get_mut(&peer_id)
                     .ok_or_else(|| anyhow!("subscribed to track by unknown participant"))?;
-                let mut frames = track.frames();
                 participant.tracks.insert(
                     track_id.clone(),
-                    RemoteVideoTrack {
-                        frame: None,
-                        _live_kit_track: track,
-                        _maintain_frame: Arc::new(cx.spawn_weak(|this, mut cx| async move {
-                            while let Some(frame) = frames.next().await {
-                                let this = if let Some(this) = this.upgrade(&cx) {
-                                    this
-                                } else {
-                                    break;
-                                };
-
-                                let done = this.update(&mut cx, |this, cx| {
-                                    if let Some(track) =
-                                        this.remote_participants.get_mut(&peer_id).and_then(
-                                            |participant| participant.tracks.get_mut(&track_id),
-                                        )
-                                    {
-                                        track.frame = Some(frame);
-                                        cx.emit(Event::Frame {
-                                            participant_id: peer_id,
-                                            track_id: track_id.clone(),
-                                        });
-                                        false
-                                    } else {
-                                        true
-                                    }
-                                });
-
-                                if done {
-                                    break;
-                                }
-                            }
-                        })),
-                    },
+                    Arc::new(RemoteVideoTrack {
+                        live_kit_track: track,
+                    }),
                 );
+                cx.emit(Event::RemoteVideoTrackShared {
+                    participant_id: peer_id,
+                    track_id,
+                });
             }
             RemoteVideoTrackUpdate::Unsubscribed {
                 publisher_id,
@@ -479,6 +463,7 @@ impl Room {
                     .get_mut(&peer_id)
                     .ok_or_else(|| anyhow!("unsubscribed from track by unknown participant"))?;
                 participant.tracks.remove(&track_id);
+                cx.emit(Event::RemoteVideoTrackUnshared { peer_id, track_id });
             }
         }
 
@@ -750,7 +735,7 @@ struct LiveKitRoom {
     _maintain_tracks: Task<()>,
 }
 
-pub enum ScreenTrack {
+enum ScreenTrack {
     None,
     Pending { publish_id: usize },
     Published(LocalTrackPublication),

crates/collab/src/integration_tests.rs 🔗

@@ -5,10 +5,7 @@ use crate::{
 };
 use ::rpc::Peer;
 use anyhow::anyhow;
-use call::{
-    room::{self, Event},
-    ActiveCall, ParticipantLocation, Room,
-};
+use call::{room, ActiveCall, ParticipantLocation, Room};
 use client::{
     self, test::FakeHttpClient, Channel, ChannelDetails, ChannelList, Client, Connection,
     Credentials, EstablishConnectionError, PeerId, User, UserStore, RECEIVE_TIMEOUT,
@@ -33,7 +30,7 @@ use language::{
     range_to_lsp, tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
     LanguageConfig, LanguageRegistry, OffsetRangeExt, Point, Rope,
 };
-use live_kit_client::{Frame, MacOSDisplay};
+use live_kit_client::MacOSDisplay;
 use lsp::{self, FakeLanguageServer};
 use parking_lot::Mutex;
 use project::{
@@ -202,36 +199,25 @@ async fn test_basic_calls(
         .await
         .unwrap();
 
-    let frame = Frame {
-        width: 800,
-        height: 600,
-        label: "a".into(),
-    };
-    display.send_frame(frame.clone());
     deterministic.run_until_parked();
 
     assert_eq!(events_b.borrow().len(), 1);
     let event = events_b.borrow().first().unwrap().clone();
-    if let Event::Frame {
+    if let call::room::Event::RemoteVideoTrackShared {
         participant_id,
         track_id,
     } = event
     {
         assert_eq!(participant_id, client_a.peer_id().unwrap());
         room_b.read_with(cx_b, |room, _| {
-            assert_eq!(
-                room.remote_participants()[&client_a.peer_id().unwrap()].tracks[&track_id].frame(),
-                Some(&frame)
-            );
+            assert!(room.remote_participants()[&client_a.peer_id().unwrap()]
+                .tracks
+                .contains_key(&track_id));
         });
     } else {
         panic!("unexpected event")
     }
 
-    display.send_frame(frame.clone());
-    deterministic.run_until_parked();
-    assert_eq!(events_b.borrow().len(), 2);
-
     // User A leaves the room.
     active_call_a.update(cx_a, |call, cx| {
         call.hang_up(cx).unwrap();

crates/language/Cargo.toml 🔗

@@ -36,7 +36,7 @@ text = { path = "../text" }
 theme = { path = "../theme" }
 util = { path = "../util" }
 anyhow = "1.0.38"
-async-broadcast = "0.3.4"
+async-broadcast = "0.4"
 async-trait = "0.1"
 futures = "0.3"
 lazy_static = "1.4"

crates/workspace/src/pane_group.rs 🔗

@@ -2,9 +2,7 @@ use crate::{FollowerStatesByLeader, JoinProject, Pane, Workspace};
 use anyhow::{anyhow, Result};
 use call::{ActiveCall, ParticipantLocation};
 use gpui::{
-    elements::*,
-    geometry::{rect::RectF, vector::vec2f},
-    Axis, Border, CursorStyle, ModelHandle, MouseButton, RenderContext, ViewHandle,
+    elements::*, Axis, Border, CursorStyle, ModelHandle, MouseButton, RenderContext, ViewHandle,
 };
 use project::Project;
 use serde::Deserialize;
@@ -144,30 +142,6 @@ impl Member {
                     Border::default()
                 };
 
-                let content = if leader.as_ref().map_or(false, |(_, leader)| {
-                    leader.location == ParticipantLocation::External && !leader.tracks.is_empty()
-                }) {
-                    let (_, leader) = leader.unwrap();
-                    let track = leader.tracks.values().next().unwrap();
-                    let frame = track.frame().cloned();
-                    Canvas::new(move |bounds, _, cx| {
-                        if let Some(frame) = frame.clone() {
-                            let size = constrain_size_preserving_aspect_ratio(
-                                bounds.size(),
-                                vec2f(frame.width() as f32, frame.height() as f32),
-                            );
-                            let origin = bounds.origin() + (bounds.size() / 2.) - size / 2.;
-                            cx.scene.push_surface(gpui::mac::Surface {
-                                bounds: RectF::new(origin, size),
-                                image_buffer: frame.image(),
-                            });
-                        }
-                    })
-                    .boxed()
-                } else {
-                    ChildView::new(pane, cx).boxed()
-                };
-
                 let prompt = if let Some((_, leader)) = leader {
                     match leader.location {
                         ParticipantLocation::SharedProject {
@@ -251,7 +225,12 @@ impl Member {
                 };
 
                 Stack::new()
-                    .with_child(Container::new(content).with_border(border).boxed())
+                    .with_child(
+                        ChildView::new(pane, cx)
+                            .contained()
+                            .with_border(border)
+                            .boxed(),
+                    )
                     .with_children(prompt)
                     .boxed()
             }

crates/workspace/src/shared_screen.rs 🔗

@@ -0,0 +1,175 @@
+use crate::{Item, ItemNavHistory};
+use anyhow::{anyhow, Result};
+use call::participant::{Frame, RemoteVideoTrack};
+use client::{PeerId, User};
+use futures::StreamExt;
+use gpui::{
+    elements::*,
+    geometry::{rect::RectF, vector::vec2f},
+    Entity, ModelHandle, RenderContext, Task, View, ViewContext,
+};
+use smallvec::SmallVec;
+use std::{
+    path::PathBuf,
+    sync::{Arc, Weak},
+};
+
+pub enum Event {
+    Close,
+}
+
+pub struct SharedScreen {
+    track: Weak<RemoteVideoTrack>,
+    frame: Option<Frame>,
+    pub peer_id: PeerId,
+    user: Arc<User>,
+    nav_history: Option<ItemNavHistory>,
+    _maintain_frame: Task<()>,
+}
+
+impl SharedScreen {
+    pub fn new(
+        track: &Arc<RemoteVideoTrack>,
+        peer_id: PeerId,
+        user: Arc<User>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
+        let mut frames = track.frames();
+        Self {
+            track: Arc::downgrade(track),
+            frame: None,
+            peer_id,
+            user,
+            nav_history: Default::default(),
+            _maintain_frame: cx.spawn(|this, mut cx| async move {
+                while let Some(frame) = frames.next().await {
+                    this.update(&mut cx, |this, cx| {
+                        this.frame = Some(frame);
+                        cx.notify();
+                    })
+                }
+                this.update(&mut cx, |_, cx| cx.emit(Event::Close));
+            }),
+        }
+    }
+}
+
+impl Entity for SharedScreen {
+    type Event = Event;
+}
+
+impl View for SharedScreen {
+    fn ui_name() -> &'static str {
+        "SharedScreen"
+    }
+
+    fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
+        let frame = self.frame.clone();
+        Canvas::new(move |bounds, _, cx| {
+            if let Some(frame) = frame.clone() {
+                let size = constrain_size_preserving_aspect_ratio(
+                    bounds.size(),
+                    vec2f(frame.width() as f32, frame.height() as f32),
+                );
+                let origin = bounds.origin() + (bounds.size() / 2.) - size / 2.;
+                cx.scene.push_surface(gpui::mac::Surface {
+                    bounds: RectF::new(origin, size),
+                    image_buffer: frame.image(),
+                });
+            }
+        })
+        .boxed()
+    }
+}
+
+impl Item for SharedScreen {
+    fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
+        if let Some(nav_history) = self.nav_history.as_ref() {
+            nav_history.push::<()>(None, cx);
+        }
+    }
+
+    fn tab_content(
+        &self,
+        _: Option<usize>,
+        style: &theme::Tab,
+        _: &gpui::AppContext,
+    ) -> gpui::ElementBox {
+        Flex::row()
+            .with_child(
+                Svg::new("icons/disable_screen_sharing_12.svg")
+                    .with_color(style.label.text.color)
+                    .constrained()
+                    .with_width(style.icon_width)
+                    .aligned()
+                    .contained()
+                    .with_margin_right(style.spacing)
+                    .boxed(),
+            )
+            .with_child(
+                Label::new(
+                    format!("{}'s screen", self.user.github_login),
+                    style.label.clone(),
+                )
+                .aligned()
+                .boxed(),
+            )
+            .boxed()
+    }
+
+    fn project_path(&self, _: &gpui::AppContext) -> Option<project::ProjectPath> {
+        Default::default()
+    }
+
+    fn project_entry_ids(&self, _: &gpui::AppContext) -> SmallVec<[project::ProjectEntryId; 3]> {
+        Default::default()
+    }
+
+    fn is_singleton(&self, _: &gpui::AppContext) -> bool {
+        false
+    }
+
+    fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
+        self.nav_history = Some(history);
+    }
+
+    fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self> {
+        let track = self.track.upgrade()?;
+        Some(Self::new(&track, self.peer_id, self.user.clone(), cx))
+    }
+
+    fn can_save(&self, _: &gpui::AppContext) -> bool {
+        false
+    }
+
+    fn save(
+        &mut self,
+        _: ModelHandle<project::Project>,
+        _: &mut ViewContext<Self>,
+    ) -> Task<Result<()>> {
+        Task::ready(Err(anyhow!("Item::save called on SharedScreen")))
+    }
+
+    fn save_as(
+        &mut self,
+        _: ModelHandle<project::Project>,
+        _: PathBuf,
+        _: &mut ViewContext<Self>,
+    ) -> Task<Result<()>> {
+        Task::ready(Err(anyhow!("Item::save_as called on SharedScreen")))
+    }
+
+    fn reload(
+        &mut self,
+        _: ModelHandle<project::Project>,
+        _: &mut ViewContext<Self>,
+    ) -> Task<Result<()>> {
+        Task::ready(Err(anyhow!("Item::reload called on SharedScreen")))
+    }
+
+    fn to_item_events(event: &Self::Event) -> Vec<crate::ItemEvent> {
+        match event {
+            Event::Close => vec![crate::ItemEvent::CloseItem],
+        }
+    }
+}

crates/workspace/src/workspace.rs 🔗

@@ -6,6 +6,7 @@ pub mod dock;
 pub mod pane;
 pub mod pane_group;
 pub mod searchable;
+mod shared_screen;
 pub mod sidebar;
 mod status_bar;
 mod toolbar;
@@ -36,6 +37,7 @@ use project::{Project, ProjectEntryId, ProjectPath, ProjectStore, Worktree, Work
 use searchable::SearchableItemHandle;
 use serde::Deserialize;
 use settings::{Autosave, DockAnchor, Settings};
+use shared_screen::SharedScreen;
 use sidebar::{Sidebar, SidebarButtons, SidebarSide, ToggleSidebarItem};
 use smallvec::SmallVec;
 use status_bar::StatusBar;
@@ -1097,14 +1099,7 @@ impl Workspace {
         if cx.has_global::<ModelHandle<ActiveCall>>() {
             let call = cx.global::<ModelHandle<ActiveCall>>().clone();
             let mut subscriptions = Vec::new();
-            subscriptions.push(cx.observe(&call, |_, _, cx| cx.notify()));
-            subscriptions.push(cx.subscribe(&call, |this, _, event, cx| {
-                if let call::room::Event::Frame { participant_id, .. } = event {
-                    if this.follower_states_by_leader.contains_key(&participant_id) {
-                        cx.notify();
-                    }
-                }
-            }));
+            subscriptions.push(cx.subscribe(&call, Self::on_active_call_event));
             active_call = Some((call, subscriptions));
         }
 
@@ -2517,13 +2512,43 @@ impl Workspace {
     }
 
     fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
+        cx.notify();
+
+        let call = self.active_call()?;
+        let room = call.read(cx).room()?.read(cx);
+        let participant = room.remote_participants().get(&leader_id)?;
+
         let mut items_to_add = Vec::new();
-        for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
-            if let Some(FollowerItem::Loaded(item)) = state
-                .active_view_id
-                .and_then(|id| state.items_by_leader_view_id.get(&id))
-            {
-                items_to_add.push((pane.clone(), item.boxed_clone()));
+        match participant.location {
+            call::ParticipantLocation::SharedProject { project_id } => {
+                if Some(project_id) == self.project.read(cx).remote_id() {
+                    for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
+                        if let Some(FollowerItem::Loaded(item)) = state
+                            .active_view_id
+                            .and_then(|id| state.items_by_leader_view_id.get(&id))
+                        {
+                            items_to_add.push((pane.clone(), item.boxed_clone()));
+                        }
+                    }
+                }
+            }
+            call::ParticipantLocation::UnsharedProject => {}
+            call::ParticipantLocation::External => {
+                let track = participant.tracks.values().next()?.clone();
+                let user = participant.user.clone();
+
+                'outer: for (pane, _) in self.follower_states_by_leader.get(&leader_id)? {
+                    for item in pane.read(cx).items_of_type::<SharedScreen>() {
+                        if item.read(cx).peer_id == leader_id {
+                            items_to_add.push((pane.clone(), Box::new(item)));
+                            continue 'outer;
+                        }
+                    }
+
+                    let shared_screen =
+                        cx.add_view(|cx| SharedScreen::new(&track, leader_id, user.clone(), cx));
+                    items_to_add.push((pane.clone(), Box::new(shared_screen)));
+                }
             }
         }
 
@@ -2532,8 +2557,8 @@ impl Workspace {
             if pane == self.active_pane {
                 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
             }
-            cx.notify();
         }
+
         None
     }
 
@@ -2561,6 +2586,27 @@ impl Workspace {
     fn active_call(&self) -> Option<&ModelHandle<ActiveCall>> {
         self.active_call.as_ref().map(|(call, _)| call)
     }
+
+    fn on_active_call_event(
+        &mut self,
+        _: ModelHandle<ActiveCall>,
+        event: &call::room::Event,
+        cx: &mut ViewContext<Self>,
+    ) {
+        match event {
+            call::room::Event::ParticipantLocationChanged {
+                participant_id: peer_id,
+            }
+            | call::room::Event::RemoteVideoTrackShared {
+                participant_id: peer_id,
+                ..
+            }
+            | call::room::Event::RemoteVideoTrackUnshared { peer_id, .. } => {
+                self.leader_updated(*peer_id, cx);
+            }
+            _ => {}
+        }
+    }
 }
 
 impl Entity for Workspace {