Allow opening shared screen via the contacts popover

Antonio Scandurra created

Change summary

crates/collab_ui/src/contact_list.rs | 139 ++++++++++++++++++++++++++++-
crates/gpui/src/app.rs               |   5 +
crates/theme/src/theme.rs            |   2 
crates/workspace/src/workspace.rs    |  53 ++++++++--
styles/src/styleTree/contactList.ts  |   5 +
5 files changed, 184 insertions(+), 20 deletions(-)

Detailed changes

crates/collab_ui/src/contact_list.rs 🔗

@@ -17,7 +17,7 @@ use serde::Deserialize;
 use settings::Settings;
 use theme::IconButton;
 use util::ResultExt;
-use workspace::JoinProject;
+use workspace::{JoinProject, OpenSharedScreen};
 
 impl_actions!(contact_list, [RemoveContact, RespondToContactRequest]);
 impl_internal_actions!(contact_list, [ToggleExpanded, Call, LeaveCall]);
@@ -67,6 +67,10 @@ enum ContactEntry {
         host_user_id: u64,
         is_last: bool,
     },
+    ParticipantScreen {
+        peer_id: PeerId,
+        is_last: bool,
+    },
     IncomingRequest(Arc<User>),
     OutgoingRequest(Arc<User>),
     Contact(Arc<Contact>),
@@ -97,6 +101,16 @@ impl PartialEq for ContactEntry {
                     return project_id_1 == project_id_2;
                 }
             }
+            ContactEntry::ParticipantScreen {
+                peer_id: peer_id_1, ..
+            } => {
+                if let ContactEntry::ParticipantScreen {
+                    peer_id: peer_id_2, ..
+                } = other
+                {
+                    return peer_id_1 == peer_id_2;
+                }
+            }
             ContactEntry::IncomingRequest(user_1) => {
                 if let ContactEntry::IncomingRequest(user_2) = other {
                     return user_1.id == user_2.id;
@@ -216,6 +230,15 @@ impl ContactList {
                     &theme.contact_list,
                     cx,
                 ),
+                ContactEntry::ParticipantScreen { peer_id, is_last } => {
+                    Self::render_participant_screen(
+                        *peer_id,
+                        *is_last,
+                        is_selected,
+                        &theme.contact_list,
+                        cx,
+                    )
+                }
                 ContactEntry::IncomingRequest(user) => Self::render_contact_request(
                     user.clone(),
                     this.user_store.clone(),
@@ -347,6 +370,9 @@ impl ContactList {
                             follow_user_id: *host_user_id,
                         });
                     }
+                    ContactEntry::ParticipantScreen { peer_id, .. } => {
+                        cx.dispatch_action(OpenSharedScreen { peer_id: *peer_id });
+                    }
                     _ => {}
                 }
             }
@@ -430,11 +456,10 @@ impl ContactList {
                 executor.clone(),
             ));
             for mat in matches {
-                let participant = &room.remote_participants()[&PeerId(mat.candidate_id as u32)];
+                let peer_id = PeerId(mat.candidate_id as u32);
+                let participant = &room.remote_participants()[&peer_id];
                 participant_entries.push(ContactEntry::CallParticipant {
-                    user: room.remote_participants()[&PeerId(mat.candidate_id as u32)]
-                        .user
-                        .clone(),
+                    user: participant.user.clone(),
                     is_pending: false,
                 });
                 let mut projects = participant.projects.iter().peekable();
@@ -443,7 +468,13 @@ 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(),
+                        is_last: projects.peek().is_none() && participant.tracks.is_empty(),
+                    });
+                }
+                if !participant.tracks.is_empty() {
+                    participant_entries.push(ContactEntry::ParticipantScreen {
+                        peer_id,
+                        is_last: true,
                     });
                 }
             }
@@ -763,6 +794,102 @@ impl ContactList {
         .boxed()
     }
 
+    fn render_participant_screen(
+        peer_id: PeerId,
+        is_last: bool,
+        is_selected: bool,
+        theme: &theme::ContactList,
+        cx: &mut RenderContext<Self>,
+    ) -> ElementBox {
+        let font_cache = cx.font_cache();
+        let host_avatar_height = theme
+            .contact_avatar
+            .width
+            .or(theme.contact_avatar.height)
+            .unwrap_or(0.);
+        let row = &theme.project_row.default;
+        let tree_branch = theme.tree_branch;
+        let line_height = row.name.text.line_height(font_cache);
+        let cap_height = row.name.text.cap_height(font_cache);
+        let baseline_offset =
+            row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.;
+
+        MouseEventHandler::<OpenSharedScreen>::new(peer_id.0 as usize, cx, |mouse_state, _| {
+            let tree_branch = *tree_branch.style_for(mouse_state, is_selected);
+            let row = theme.project_row.style_for(mouse_state, is_selected);
+
+            Flex::row()
+                .with_child(
+                    Stack::new()
+                        .with_child(
+                            Canvas::new(move |bounds, _, cx| {
+                                let start_x = bounds.min_x() + (bounds.width() / 2.)
+                                    - (tree_branch.width / 2.);
+                                let end_x = bounds.max_x();
+                                let start_y = bounds.min_y();
+                                let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
+
+                                cx.scene.push_quad(gpui::Quad {
+                                    bounds: RectF::from_points(
+                                        vec2f(start_x, start_y),
+                                        vec2f(
+                                            start_x + tree_branch.width,
+                                            if is_last { end_y } else { bounds.max_y() },
+                                        ),
+                                    ),
+                                    background: Some(tree_branch.color),
+                                    border: gpui::Border::default(),
+                                    corner_radius: 0.,
+                                });
+                                cx.scene.push_quad(gpui::Quad {
+                                    bounds: RectF::from_points(
+                                        vec2f(start_x, end_y),
+                                        vec2f(end_x, end_y + tree_branch.width),
+                                    ),
+                                    background: Some(tree_branch.color),
+                                    border: gpui::Border::default(),
+                                    corner_radius: 0.,
+                                });
+                            })
+                            .boxed(),
+                        )
+                        .constrained()
+                        .with_width(host_avatar_height)
+                        .boxed(),
+                )
+                .with_child(
+                    Svg::new("icons/disable_screen_sharing_12.svg")
+                        .with_color(row.icon.color)
+                        .constrained()
+                        .with_width(row.icon.width)
+                        .aligned()
+                        .left()
+                        .contained()
+                        .with_style(row.icon.container)
+                        .boxed(),
+                )
+                .with_child(
+                    Label::new("Screen Sharing".into(), row.name.text.clone())
+                        .aligned()
+                        .left()
+                        .contained()
+                        .with_style(row.name.container)
+                        .flex(1., false)
+                        .boxed(),
+                )
+                .constrained()
+                .with_height(theme.row_height)
+                .contained()
+                .with_style(row.container)
+                .boxed()
+        })
+        .with_cursor_style(CursorStyle::PointingHand)
+        .on_click(MouseButton::Left, move |_, cx| {
+            cx.dispatch_action(OpenSharedScreen { peer_id });
+        })
+        .boxed()
+    }
+
     fn render_header(
         section: Section,
         theme: &theme::ContactList,

crates/gpui/src/app.rs 🔗

@@ -3835,6 +3835,11 @@ impl<'a, T: View> ViewContext<'a, T> {
         self.app.notify_view(self.window_id, self.view_id);
     }
 
+    pub fn dispatch_action(&mut self, action: impl Action) {
+        self.app
+            .dispatch_action_at(self.window_id, self.view_id, action)
+    }
+
     pub fn dispatch_any_action(&mut self, action: Box<dyn Action>) {
         self.app
             .dispatch_any_action_at(self.window_id, self.view_id, action)

crates/theme/src/theme.rs 🔗

@@ -120,6 +120,7 @@ pub struct ContactList {
 pub struct ProjectRow {
     #[serde(flatten)]
     pub container: ContainerStyle,
+    pub icon: Icon,
     pub name: ContainedText,
 }
 
@@ -381,7 +382,6 @@ pub struct Icon {
     pub container: ContainerStyle,
     pub color: Color,
     pub width: f32,
-    pub path: String,
 }
 
 #[derive(Deserialize, Clone, Copy, Default)]

crates/workspace/src/workspace.rs 🔗

@@ -121,12 +121,18 @@ pub struct JoinProject {
     pub follow_user_id: u64,
 }
 
+#[derive(Clone, PartialEq)]
+pub struct OpenSharedScreen {
+    pub peer_id: PeerId,
+}
+
 impl_internal_actions!(
     workspace,
     [
         OpenPaths,
         ToggleFollow,
         JoinProject,
+        OpenSharedScreen,
         RemoveWorktreeFromProject
     ]
 );
@@ -166,6 +172,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
     cx.add_async_action(Workspace::follow_next_collaborator);
     cx.add_async_action(Workspace::close);
     cx.add_async_action(Workspace::save_all);
+    cx.add_action(Workspace::open_shared_screen);
     cx.add_action(Workspace::add_folder_to_project);
     cx.add_action(Workspace::remove_folder_from_project);
     cx.add_action(
@@ -1788,6 +1795,15 @@ impl Workspace {
         item
     }
 
+    pub fn open_shared_screen(&mut self, action: &OpenSharedScreen, cx: &mut ViewContext<Self>) {
+        if let Some(shared_screen) =
+            self.shared_screen_for_peer(action.peer_id, &self.active_pane, cx)
+        {
+            let pane = self.active_pane.clone();
+            Pane::add_item(self, &pane, Box::new(shared_screen), false, true, None, cx);
+        }
+    }
+
     pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext<Self>) -> bool {
         let result = self.panes.iter().find_map(|pane| {
             pane.read(cx)
@@ -2534,20 +2550,10 @@ impl Workspace {
             }
             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;
-                        }
+                for (pane, _) in self.follower_states_by_leader.get(&leader_id)? {
+                    if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
+                        items_to_add.push((pane.clone(), Box::new(shared_screen)));
                     }
-
-                    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)));
                 }
             }
         }
@@ -2562,6 +2568,27 @@ impl Workspace {
         None
     }
 
+    fn shared_screen_for_peer(
+        &self,
+        peer_id: PeerId,
+        pane: &ViewHandle<Pane>,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<ViewHandle<SharedScreen>> {
+        let call = self.active_call()?;
+        let room = call.read(cx).room()?.read(cx);
+        let participant = room.remote_participants().get(&peer_id)?;
+        let track = participant.tracks.values().next()?.clone();
+        let user = participant.user.clone();
+
+        for item in pane.read(cx).items_of_type::<SharedScreen>() {
+            if item.read(cx).peer_id == peer_id {
+                return Some(item);
+            }
+        }
+
+        Some(cx.add_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
+    }
+
     pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
         if !active {
             for pane in &self.panes {

styles/src/styleTree/contactList.ts 🔗

@@ -166,6 +166,11 @@ export default function contactsPanel(colorScheme: ColorScheme) {
     projectRow: {
       ...projectRow,
       background: background(layer, "on"),
+      icon: {
+        margin: { left: nameMargin },
+        color: foreground(layer, "variant"),
+        width: 12,
+      },
       name: {
         ...projectRow.name,
         ...text(layer, "mono", { size: "sm" }),