From 88af35fe4727f5187608cfb396def24f4c60c004 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 21 Jul 2025 13:44:51 +0200 Subject: [PATCH] collab: Add screen selector (#31506) Instead of selecting a screen to share arbitrarily, we'll now allow user to select the screen to share. Note that sharing multiple screens at the time is still not supported (though prolly not too far-fetched). Related to #4666 ![image](https://github.com/user-attachments/assets/1afb664f-3cdb-4e0a-bb29-9d7093d87fa5) Release Notes: - Added screen selector dropdown to screen share button --------- Co-authored-by: Kirill Bulatov Co-authored-by: Cole Miller --- .config/hakari.toml | 2 +- Cargo.lock | 6 +- Cargo.toml | 4 +- crates/call/src/call_impl/room.rs | 61 +++--- crates/collab/src/tests/following_tests.rs | 12 +- crates/collab/src/tests/integration_tests.rs | 21 +- crates/collab_ui/src/collab_panel.rs | 22 +- crates/gpui/src/app.rs | 2 +- crates/gpui/src/platform.rs | 31 ++- .../src/platform/linux/headless/client.rs | 2 +- crates/gpui/src/platform/linux/platform.rs | 4 +- .../gpui/src/platform/linux/wayland/client.rs | 2 +- crates/gpui/src/platform/linux/x11/client.rs | 2 +- crates/gpui/src/platform/mac/platform.rs | 2 +- .../gpui/src/platform/mac/screen_capture.rs | 102 ++++++++-- .../gpui/src/platform/scap_screen_capture.rs | 99 ++++++--- crates/gpui/src/platform/test.rs | 2 +- crates/gpui/src/platform/test/platform.rs | 22 +- crates/gpui/src/platform/windows/platform.rs | 2 +- .../src/livekit_client/playback.rs | 6 +- .../src/mock_client/participant.rs | 6 +- crates/title_bar/Cargo.toml | 1 + crates/title_bar/src/collab.rs | 192 +++++++++++++++--- crates/title_bar/src/title_bar.rs | 4 +- .../ui/src/components/button/icon_button.rs | 3 +- crates/ui/src/components/context_menu.rs | 6 + 26 files changed, 473 insertions(+), 145 deletions(-) diff --git a/.config/hakari.toml b/.config/hakari.toml index 5168887581c8a1fdae0478e74b0f01225c7a1465..2050065cc2d6be2a27ec012dcd125af992793eeb 100644 --- a/.config/hakari.toml +++ b/.config/hakari.toml @@ -24,7 +24,7 @@ workspace-members = [ third-party = [ { name = "reqwest", version = "0.11.27" }, # build of remote_server should not include scap / its x11 dependency - { name = "scap", git = "https://github.com/zed-industries/scap", rev = "270538dc780f5240723233ff901e1054641ed318" }, + { name = "scap", git = "https://github.com/zed-industries/scap", rev = "808aa5c45b41e8f44729d02e38fd00a2fe2722e7" }, ] [final-excludes] diff --git a/Cargo.lock b/Cargo.lock index a5ea621cd14662cba0838558b70d3e13b51c7840..bc69de7a7cd551590252a0a06f3dca927197e8d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14185,7 +14185,7 @@ dependencies = [ [[package]] name = "scap" version = "0.0.8" -source = "git+https://github.com/zed-industries/scap?rev=270538dc780f5240723233ff901e1054641ed318#270538dc780f5240723233ff901e1054641ed318" +source = "git+https://github.com/zed-industries/scap?rev=808aa5c45b41e8f44729d02e38fd00a2fe2722e7#808aa5c45b41e8f44729d02e38fd00a2fe2722e7" dependencies = [ "anyhow", "cocoa 0.25.0", @@ -16484,6 +16484,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" name = "title_bar" version = "0.1.0" dependencies = [ + "anyhow", "auto_update", "call", "chrono", @@ -18729,8 +18730,7 @@ dependencies = [ [[package]] name = "windows-capture" version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59d10b4be8b907c7055bc7270dd68d2b920978ffacc1599dcb563a79f0e68d16" +source = "git+https://github.com/zed-industries/windows-capture.git?rev=f0d6c1b6691db75461b732f6d5ff56eed002eeb9#f0d6c1b6691db75461b732f6d5ff56eed002eeb9" dependencies = [ "clap", "ctrlc", diff --git a/Cargo.toml b/Cargo.toml index aa9af9a423eb0d283df821a46424a4702154bce5..0169d32eb6a671dad002e8d520e54a38a5901f8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -553,8 +553,7 @@ rustc-demangle = "0.1.23" rustc-hash = "2.1.0" rustls = { version = "0.23.26" } rustls-platform-verifier = "0.5.0" -# When updating scap rev, also update it in .config/hakari.toml -scap = { git = "https://github.com/zed-industries/scap", rev = "270538dc780f5240723233ff901e1054641ed318", default-features = false } +scap = { git = "https://github.com/zed-industries/scap", rev = "808aa5c45b41e8f44729d02e38fd00a2fe2722e7", default-features = false } schemars = { version = "1.0", features = ["indexmap2"] } semver = "1.0" serde = { version = "1.0", features = ["derive", "rc"] } @@ -708,6 +707,7 @@ features = [ [patch.crates-io] notify = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" } notify-types = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" } +windows-capture = { git = "https://github.com/zed-industries/windows-capture.git", rev = "f0d6c1b6691db75461b732f6d5ff56eed002eeb9" } # Makes the workspace hack crate refer to the local one, but only when you're building locally workspace-hack = { path = "tooling/workspace-hack" } diff --git a/crates/call/src/call_impl/room.rs b/crates/call/src/call_impl/room.rs index 7aac72ed46e777a1c70a194cf79f9bad160d1028..afeee4c924feb2990668f953d5b2f7dfcff26f34 100644 --- a/crates/call/src/call_impl/room.rs +++ b/crates/call/src/call_impl/room.rs @@ -11,15 +11,18 @@ use client::{ use collections::{BTreeMap, HashMap, HashSet}; use fs::Fs; use futures::{FutureExt, StreamExt}; -use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Task, WeakEntity}; +use gpui::{ + App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, ScreenCaptureSource, + ScreenCaptureStream, Task, WeakEntity, +}; use gpui_tokio::Tokio; use language::LanguageRegistry; use livekit::{LocalTrackPublication, ParticipantIdentity, RoomEvent}; -use livekit_client::{self as livekit, TrackSid}; +use livekit_client::{self as livekit, AudioStream, TrackSid}; use postage::{sink::Sink, stream::Stream, watch}; use project::Project; use settings::Settings as _; -use std::{any::Any, future::Future, mem, rc::Rc, sync::Arc, time::Duration}; +use std::{future::Future, mem, rc::Rc, sync::Arc, time::Duration}; use util::{ResultExt, TryFutureExt, post_inc}; pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); @@ -1251,12 +1254,21 @@ impl Room { }) } - pub fn is_screen_sharing(&self) -> bool { + pub fn is_sharing_screen(&self) -> bool { self.live_kit.as_ref().map_or(false, |live_kit| { !matches!(live_kit.screen_track, LocalTrack::None) }) } + pub fn shared_screen_id(&self) -> Option { + self.live_kit.as_ref().and_then(|lk| match lk.screen_track { + LocalTrack::Published { ref _stream, .. } => { + _stream.metadata().ok().map(|meta| meta.id) + } + _ => 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) @@ -1369,11 +1381,15 @@ impl Room { }) } - pub fn share_screen(&mut self, cx: &mut Context) -> Task> { + pub fn share_screen( + &mut self, + source: Rc, + cx: &mut Context, + ) -> Task> { if self.status.is_offline() { return Task::ready(Err(anyhow!("room is offline"))); } - if self.is_screen_sharing() { + if self.is_sharing_screen() { return Task::ready(Err(anyhow!("screen was already shared"))); } @@ -1386,20 +1402,8 @@ impl Room { return Task::ready(Err(anyhow!("live-kit was not initialized"))); }; - let sources = cx.screen_capture_sources(); - cx.spawn(async move |this, cx| { - let sources = sources - .await - .map_err(|error| error.into()) - .and_then(|sources| sources); - let source = - sources.and_then(|sources| sources.into_iter().next().context("no display found")); - - let publication = match source { - Ok(source) => participant.publish_screenshare_track(&*source, cx).await, - Err(error) => Err(error), - }; + let publication = participant.publish_screenshare_track(&*source, cx).await; this.update(cx, |this, cx| { let live_kit = this @@ -1426,7 +1430,7 @@ impl Room { } else { live_kit.screen_track = LocalTrack::Published { track_publication: publication, - _stream: Box::new(stream), + _stream: stream, }; cx.notify(); } @@ -1492,7 +1496,7 @@ impl Room { } } - pub fn unshare_screen(&mut self, cx: &mut Context) -> Result<()> { + pub fn unshare_screen(&mut self, play_sound: bool, cx: &mut Context) -> Result<()> { anyhow::ensure!(!self.status.is_offline(), "room is offline"); let live_kit = self @@ -1516,7 +1520,10 @@ impl Room { cx.notify(); } - Audio::play_sound(Sound::StopScreenshare, cx); + if play_sound { + Audio::play_sound(Sound::StopScreenshare, cx); + } + Ok(()) } } @@ -1624,8 +1631,8 @@ fn spawn_room_connection( struct LiveKitRoom { room: Rc, - screen_track: LocalTrack, - microphone_track: LocalTrack, + 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, @@ -1663,18 +1670,18 @@ impl LiveKitRoom { } } -enum LocalTrack { +enum LocalTrack { None, Pending { publish_id: usize, }, Published { track_publication: LocalTrackPublication, - _stream: Box, + _stream: Box, }, } -impl Default for LocalTrack { +impl Default for LocalTrack { fn default() -> Self { Self::None } diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index 1a4c3a70a4c6cb622cb90dcd636a845c77c756c6..d9fd8ffeb2a6c693c3570409070f7a0fbfe33ea2 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -439,7 +439,7 @@ async fn test_basic_following( editor_a1.item_id() ); - #[cfg(all(not(target_os = "macos"), not(target_os = "windows")))] + // #[cfg(all(not(target_os = "macos"), not(target_os = "windows")))] { use crate::rpc::RECONNECT_TIMEOUT; use gpui::TestScreenCaptureSource; @@ -456,11 +456,19 @@ async fn test_basic_following( .await .unwrap(); cx_b.set_screen_capture_sources(vec![display]); + let source = cx_b + .read(|cx| cx.screen_capture_sources()) + .await + .unwrap() + .unwrap() + .into_iter() + .next() + .unwrap(); active_call_b .update(cx_b, |call, cx| { call.room() .unwrap() - .update(cx, |room, cx| room.share_screen(cx)) + .update(cx, |room, cx| room.share_screen(source, cx)) }) .await .unwrap(); diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index d1099a327a4d090dcd26fff8d5308e36922a49b6..9795c27574d1b744e02064683925366012b3defa 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -277,11 +277,19 @@ async fn test_basic_calls( let events_b = active_call_events(cx_b); let events_c = active_call_events(cx_c); cx_a.set_screen_capture_sources(vec![display]); + let screen_a = cx_a + .update(|cx| cx.screen_capture_sources()) + .await + .unwrap() + .unwrap() + .into_iter() + .next() + .unwrap(); active_call_a .update(cx_a, |call, cx| { call.room() .unwrap() - .update(cx, |room, cx| room.share_screen(cx)) + .update(cx, |room, cx| room.share_screen(screen_a, cx)) }) .await .unwrap(); @@ -6312,11 +6320,20 @@ async fn test_join_call_after_screen_was_shared( // User A shares their screen let display = gpui::TestScreenCaptureSource::new(); cx_a.set_screen_capture_sources(vec![display]); + let screen_a = cx_a + .update(|cx| cx.screen_capture_sources()) + .await + .unwrap() + .unwrap() + .into_iter() + .next() + .unwrap(); + active_call_a .update(cx_a, |call, cx| { call.room() .unwrap() - .update(cx, |room, cx| room.share_screen(cx)) + .update(cx, |room, cx| room.share_screen(screen_a, cx)) }) .await .unwrap(); diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index ec23e2c3f536dc38db05f448f0d239d243a15756..4d5973481e6cf776355c2e0d2a6cbc6d9f02d1b4 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -144,10 +144,22 @@ pub fn init(cx: &mut App) { if let Some(room) = room { window.defer(cx, move |_window, cx| { room.update(cx, |room, cx| { - if room.is_screen_sharing() { - room.unshare_screen(cx).ok(); + if room.is_sharing_screen() { + room.unshare_screen(true, cx).ok(); } else { - room.share_screen(cx).detach_and_log_err(cx); + let sources = cx.screen_capture_sources(); + + cx.spawn(async move |room, cx| { + let sources = sources.await??; + let first = sources.into_iter().next(); + if let Some(first) = first { + room.update(cx, |room, cx| room.share_screen(first, cx))? + .await + } else { + Ok(()) + } + }) + .detach_and_log_err(cx); }; }); }); @@ -528,10 +540,10 @@ impl CollabPanel { project_id: project.id, worktree_root_names: project.worktree_root_names.clone(), host_user_id: user_id, - is_last: projects.peek().is_none() && !room.is_screen_sharing(), + is_last: projects.peek().is_none() && !room.is_sharing_screen(), }); } - if room.is_screen_sharing() { + if room.is_sharing_screen() { self.entries.push(ListEntry::ParticipantScreen { peer_id: None, is_last: true, diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 2771de9aac2bc721126091826aa672d194589e61..759d33563e0af1be038a98f78712f7b3f18ef327 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -696,7 +696,7 @@ impl App { /// Returns a list of available screen capture sources. pub fn screen_capture_sources( &self, - ) -> oneshot::Receiver>>> { + ) -> oneshot::Receiver>>> { self.platform.screen_capture_sources() } diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 8918fdd28bda083abd0335f6f500b35d01895c58..6f227f1d077e96337c82ad7eba9b1d0fd9c7dfc0 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -85,7 +85,7 @@ pub(crate) use test::*; pub(crate) use windows::*; #[cfg(any(test, feature = "test-support"))] -pub use test::{TestDispatcher, TestScreenCaptureSource}; +pub use test::{TestDispatcher, TestScreenCaptureSource, TestScreenCaptureStream}; /// Returns a background executor for the current platform. pub fn background_executor() -> BackgroundExecutor { @@ -189,13 +189,12 @@ pub(crate) trait Platform: 'static { false } #[cfg(feature = "screen-capture")] - fn screen_capture_sources( - &self, - ) -> oneshot::Receiver>>>; + fn screen_capture_sources(&self) + -> oneshot::Receiver>>>; #[cfg(not(feature = "screen-capture"))] fn screen_capture_sources( &self, - ) -> oneshot::Receiver>>> { + ) -> oneshot::Receiver>>> { let (sources_tx, sources_rx) = oneshot::channel(); sources_tx .send(Err(anyhow::anyhow!( @@ -293,10 +292,23 @@ pub trait PlatformDisplay: Send + Sync + Debug { } } +/// Metadata for a given [ScreenCaptureSource] +#[derive(Clone)] +pub struct SourceMetadata { + /// Opaque identifier of this screen. + pub id: u64, + /// Human-readable label for this source. + pub label: Option, + /// Whether this source is the main display. + pub is_main: Option, + /// Video resolution of this source. + pub resolution: Size, +} + /// A source of on-screen video content that can be captured. pub trait ScreenCaptureSource { - /// Returns the video resolution of this source. - fn resolution(&self) -> Result>; + /// Returns metadata for this source. + fn metadata(&self) -> Result; /// Start capture video from this source, invoking the given callback /// with each frame. @@ -308,7 +320,10 @@ pub trait ScreenCaptureSource { } /// A video stream captured from a screen. -pub trait ScreenCaptureStream {} +pub trait ScreenCaptureStream { + /// Returns metadata for this source. + fn metadata(&self) -> Result; +} /// A frame of video captured from a screen. pub struct ScreenCaptureFrame(pub PlatformScreenCaptureFrame); diff --git a/crates/gpui/src/platform/linux/headless/client.rs b/crates/gpui/src/platform/linux/headless/client.rs index 663a740389e68c0505a4b3f1f55a3b4681aacfa6..da54db371033bac53e2ac3324306fa86eb57fb57 100644 --- a/crates/gpui/src/platform/linux/headless/client.rs +++ b/crates/gpui/src/platform/linux/headless/client.rs @@ -73,7 +73,7 @@ impl LinuxClient for HeadlessClient { #[cfg(feature = "screen-capture")] fn screen_capture_sources( &self, - ) -> futures::channel::oneshot::Receiver>>> + ) -> futures::channel::oneshot::Receiver>>> { let (mut tx, rx) = futures::channel::oneshot::channel(); tx.send(Err(anyhow::anyhow!( diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index c4b90ccf084627640a6cda5806d1bcb63415feb3..a52841e1afe4b0a396c68ef72587777edd5eb14e 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -56,7 +56,7 @@ pub trait LinuxClient { #[cfg(feature = "screen-capture")] fn screen_capture_sources( &self, - ) -> oneshot::Receiver>>>; + ) -> oneshot::Receiver>>>; fn open_window( &self, @@ -245,7 +245,7 @@ impl Platform for P { #[cfg(feature = "screen-capture")] fn screen_capture_sources( &self, - ) -> oneshot::Receiver>>> { + ) -> oneshot::Receiver>>> { self.screen_capture_sources() } diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index 57d1dcec04ee6aa1828c98286c9115df4ccb6d44..72e4477ecf697a9f6443dffb80e0637202d3b848 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -672,7 +672,7 @@ impl LinuxClient for WaylandClient { #[cfg(feature = "screen-capture")] fn screen_capture_sources( &self, - ) -> futures::channel::oneshot::Receiver>>> + ) -> futures::channel::oneshot::Receiver>>> { // TODO: Get screen capture working on wayland. Be sure to try window resizing as that may // be tricky. diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index 0606f619c6fb808e4be42abe07f51d1e124a69f4..d1cb7d00cc7468f7b9bc02b10dfde04e195b8950 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -1448,7 +1448,7 @@ impl LinuxClient for X11Client { #[cfg(feature = "screen-capture")] fn screen_capture_sources( &self, - ) -> futures::channel::oneshot::Receiver>>> + ) -> futures::channel::oneshot::Receiver>>> { crate::platform::scap_screen_capture::scap_screen_sources( &self.0.borrow().common.foreground_executor, diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index d9bb665469002bd89e248a8593f56b12cfebcca1..1d2146cf73562beed6c26754396dc2c4c0c915f9 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -583,7 +583,7 @@ impl Platform for MacPlatform { #[cfg(feature = "screen-capture")] fn screen_capture_sources( &self, - ) -> oneshot::Receiver>>> { + ) -> oneshot::Receiver>>> { super::screen_capture::get_sources() } diff --git a/crates/gpui/src/platform/mac/screen_capture.rs b/crates/gpui/src/platform/mac/screen_capture.rs index af5e02fc06cbd6a82c4502a5f20e54237b5dc64d..4d4ffa6896520e465dfeb7b1ccc06e1149f9e25d 100644 --- a/crates/gpui/src/platform/mac/screen_capture.rs +++ b/crates/gpui/src/platform/mac/screen_capture.rs @@ -1,5 +1,5 @@ use crate::{ - DevicePixels, ForegroundExecutor, Size, + DevicePixels, ForegroundExecutor, SharedString, SourceMetadata, platform::{ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream}, size, }; @@ -7,8 +7,9 @@ use anyhow::{Result, anyhow}; use block::ConcreteBlock; use cocoa::{ base::{YES, id, nil}, - foundation::NSArray, + foundation::{NSArray, NSString}, }; +use collections::HashMap; use core_foundation::base::TCFType; use core_graphics::display::{ CGDirectDisplayID, CGDisplayCopyDisplayMode, CGDisplayModeGetPixelHeight, @@ -32,11 +33,13 @@ use super::NSStringExt; #[derive(Clone)] pub struct MacScreenCaptureSource { sc_display: id, + meta: Option, } pub struct MacScreenCaptureStream { sc_stream: id, sc_stream_output: id, + meta: SourceMetadata, } static mut DELEGATE_CLASS: *const Class = ptr::null(); @@ -47,19 +50,31 @@ const FRAME_CALLBACK_IVAR: &str = "frame_callback"; const SCStreamOutputTypeScreen: NSInteger = 0; impl ScreenCaptureSource for MacScreenCaptureSource { - fn resolution(&self) -> Result> { - unsafe { + fn metadata(&self) -> Result { + let (display_id, size) = unsafe { let display_id: CGDirectDisplayID = msg_send![self.sc_display, displayID]; let display_mode_ref = CGDisplayCopyDisplayMode(display_id); let width = CGDisplayModeGetPixelWidth(display_mode_ref); let height = CGDisplayModeGetPixelHeight(display_mode_ref); CGDisplayModeRelease(display_mode_ref); - Ok(size( - DevicePixels(width as i32), - DevicePixels(height as i32), - )) - } + ( + display_id, + size(DevicePixels(width as i32), DevicePixels(height as i32)), + ) + }; + let (label, is_main) = self + .meta + .clone() + .map(|meta| (meta.label, meta.is_main)) + .unzip(); + + Ok(SourceMetadata { + id: display_id as u64, + label, + is_main, + resolution: size, + }) } fn stream( @@ -89,9 +104,9 @@ impl ScreenCaptureSource for MacScreenCaptureSource { Box::into_raw(Box::new(frame_callback)) as *mut c_void, ); - let resolution = self.resolution().unwrap(); - let _: id = msg_send![configuration, setWidth: resolution.width.0 as i64]; - let _: id = msg_send![configuration, setHeight: resolution.height.0 as i64]; + let meta = self.metadata().unwrap(); + let _: id = msg_send![configuration, setWidth: meta.resolution.width.0 as i64]; + let _: id = msg_send![configuration, setHeight: meta.resolution.height.0 as i64]; let stream: id = msg_send![stream, initWithFilter:filter configuration:configuration delegate:delegate]; let (mut tx, rx) = oneshot::channel(); @@ -110,6 +125,7 @@ impl ScreenCaptureSource for MacScreenCaptureSource { move |error: id| { let result = if error == nil { let stream = MacScreenCaptureStream { + meta: meta.clone(), sc_stream: stream, sc_stream_output: output, }; @@ -138,7 +154,11 @@ impl Drop for MacScreenCaptureSource { } } -impl ScreenCaptureStream for MacScreenCaptureStream {} +impl ScreenCaptureStream for MacScreenCaptureStream { + fn metadata(&self) -> Result { + Ok(self.meta.clone()) + } +} impl Drop for MacScreenCaptureStream { fn drop(&mut self) { @@ -164,24 +184,74 @@ impl Drop for MacScreenCaptureStream { } } -pub(crate) fn get_sources() -> oneshot::Receiver>>> { +#[derive(Clone)] +struct ScreenMeta { + label: SharedString, + // Is this the screen with menu bar? + is_main: bool, +} + +unsafe fn screen_id_to_human_label() -> HashMap { + let screens: id = msg_send![class!(NSScreen), screens]; + let count: usize = msg_send![screens, count]; + let mut map = HashMap::default(); + let screen_number_key = unsafe { NSString::alloc(nil).init_str("NSScreenNumber") }; + for i in 0..count { + let screen: id = msg_send![screens, objectAtIndex: i]; + let device_desc: id = msg_send![screen, deviceDescription]; + if device_desc == nil { + continue; + } + + let nsnumber: id = msg_send![device_desc, objectForKey: screen_number_key]; + if nsnumber == nil { + continue; + } + + let screen_id: u32 = msg_send![nsnumber, unsignedIntValue]; + + let name: id = msg_send![screen, localizedName]; + if name != nil { + let cstr: *const std::os::raw::c_char = msg_send![name, UTF8String]; + let rust_str = unsafe { + std::ffi::CStr::from_ptr(cstr) + .to_string_lossy() + .into_owned() + }; + map.insert( + screen_id, + ScreenMeta { + label: rust_str.into(), + is_main: i == 0, + }, + ); + } + } + map +} + +pub(crate) fn get_sources() -> oneshot::Receiver>>> { unsafe { let (mut tx, rx) = oneshot::channel(); let tx = Rc::new(RefCell::new(Some(tx))); - + let screen_id_to_label = screen_id_to_human_label(); let block = ConcreteBlock::new(move |shareable_content: id, error: id| { let Some(mut tx) = tx.borrow_mut().take() else { return; }; + let result = if error == nil { let displays: id = msg_send![shareable_content, displays]; let mut result = Vec::new(); for i in 0..displays.count() { let display = displays.objectAtIndex(i); + let id: CGDirectDisplayID = msg_send![display, displayID]; + let meta = screen_id_to_label.get(&id).cloned(); let source = MacScreenCaptureSource { sc_display: msg_send![display, retain], + meta, }; - result.push(Box::new(source) as Box); + result.push(Rc::new(source) as Rc); } Ok(result) } else { diff --git a/crates/gpui/src/platform/scap_screen_capture.rs b/crates/gpui/src/platform/scap_screen_capture.rs index c5e2267a37c794aeab70bc06d88d849b64be1c6f..32041b655fdc20b046717291c623dcb5c4d5146c 100644 --- a/crates/gpui/src/platform/scap_screen_capture.rs +++ b/crates/gpui/src/platform/scap_screen_capture.rs @@ -1,10 +1,12 @@ //! Screen capture for Linux and Windows use crate::{ DevicePixels, ForegroundExecutor, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, - Size, size, + Size, SourceMetadata, size, }; use anyhow::{Context as _, Result, anyhow}; use futures::channel::oneshot; +use scap::Target; +use std::rc::Rc; use std::sync::Arc; use std::sync::atomic::{self, AtomicBool}; @@ -15,7 +17,7 @@ use std::sync::atomic::{self, AtomicBool}; #[allow(dead_code)] pub(crate) fn scap_screen_sources( foreground_executor: &ForegroundExecutor, -) -> oneshot::Receiver>>> { +) -> oneshot::Receiver>>> { let (sources_tx, sources_rx) = oneshot::channel(); get_screen_targets(sources_tx); to_dyn_screen_capture_sources(sources_rx, foreground_executor) @@ -29,14 +31,14 @@ pub(crate) fn scap_screen_sources( #[allow(dead_code)] pub(crate) fn start_scap_default_target_source( foreground_executor: &ForegroundExecutor, -) -> oneshot::Receiver>>> { +) -> oneshot::Receiver>>> { let (sources_tx, sources_rx) = oneshot::channel(); start_default_target_screen_capture(sources_tx); to_dyn_screen_capture_sources(sources_rx, foreground_executor) } struct ScapCaptureSource { - target: scap::Target, + target: scap::Display, size: Size, } @@ -52,7 +54,7 @@ fn get_screen_targets(sources_tx: oneshot::Sender> } }; let sources = targets - .iter() + .into_iter() .filter_map(|target| match target { scap::Target::Display(display) => { let size = Size { @@ -60,7 +62,7 @@ fn get_screen_targets(sources_tx: oneshot::Sender> height: DevicePixels(display.height as i32), }; Some(ScapCaptureSource { - target: target.clone(), + target: display, size, }) } @@ -72,8 +74,13 @@ fn get_screen_targets(sources_tx: oneshot::Sender> } impl ScreenCaptureSource for ScapCaptureSource { - fn resolution(&self) -> Result> { - Ok(self.size) + fn metadata(&self) -> Result { + Ok(SourceMetadata { + resolution: self.size, + label: Some(self.target.title.clone().into()), + is_main: None, + id: self.target.id as u64, + }) } fn stream( @@ -85,13 +92,15 @@ impl ScreenCaptureSource for ScapCaptureSource { let target = self.target.clone(); // Due to use of blocking APIs, a dedicated thread is used. - std::thread::spawn(move || match new_scap_capturer(Some(target)) { - Ok(mut capturer) => { - capturer.start_capture(); - run_capture(capturer, frame_callback, stream_tx); - } - Err(e) => { - stream_tx.send(Err(e)).ok(); + std::thread::spawn(move || { + match new_scap_capturer(Some(scap::Target::Display(target.clone()))) { + Ok(mut capturer) => { + capturer.start_capture(); + run_capture(capturer, target.clone(), frame_callback, stream_tx); + } + Err(e) => { + stream_tx.send(Err(e)).ok(); + } } }); @@ -107,6 +116,7 @@ struct ScapDefaultTargetCaptureSource { // Callback for frames. Box, )>, + target: scap::Display, size: Size, } @@ -123,33 +133,48 @@ fn start_default_target_screen_capture( .get_next_frame() .context("Failed to get first frame of screenshare to get the size.")?; let size = frame_size(&first_frame); - Ok((capturer, size)) + let target = capturer + .target() + .context("Unable to determine the target display.")?; + let target = target.clone(); + Ok((capturer, size, target)) }); match start_result { - Err(e) => { - sources_tx.send(Err(e)).ok(); - } - Ok((capturer, size)) => { + Ok((capturer, size, Target::Display(display))) => { let (stream_call_tx, stream_rx) = std::sync::mpsc::sync_channel(1); sources_tx .send(Ok(vec![ScapDefaultTargetCaptureSource { stream_call_tx, size, + target: display.clone(), }])) .ok(); let Ok((stream_tx, frame_callback)) = stream_rx.recv() else { return; }; - run_capture(capturer, frame_callback, stream_tx); + run_capture(capturer, display, frame_callback, stream_tx); + } + Err(e) => { + sources_tx.send(Err(e)).ok(); + } + _ => { + sources_tx + .send(Err(anyhow!("The screen capture source is not a display"))) + .ok(); } } }); } impl ScreenCaptureSource for ScapDefaultTargetCaptureSource { - fn resolution(&self) -> Result> { - Ok(self.size) + fn metadata(&self) -> Result { + Ok(SourceMetadata { + resolution: self.size, + label: None, + is_main: None, + id: self.target.id as u64, + }) } fn stream( @@ -189,12 +214,19 @@ fn new_scap_capturer(target: Option) -> Result, stream_tx: oneshot::Sender>, ) { let cancel_stream = Arc::new(AtomicBool::new(false)); + let size = Size { + width: DevicePixels(display.width as i32), + height: DevicePixels(display.height as i32), + }; let stream_send_result = stream_tx.send(Ok(ScapStream { cancel_stream: cancel_stream.clone(), + display, + size, })); if let Err(_) = stream_send_result { return; @@ -213,9 +245,20 @@ fn run_capture( struct ScapStream { cancel_stream: Arc, + display: scap::Display, + size: Size, } -impl ScreenCaptureStream for ScapStream {} +impl ScreenCaptureStream for ScapStream { + fn metadata(&self) -> Result { + Ok(SourceMetadata { + resolution: self.size, + label: Some(self.display.title.clone().into()), + is_main: None, + id: self.display.id as u64, + }) + } +} impl Drop for ScapStream { fn drop(&mut self) { @@ -237,12 +280,12 @@ fn frame_size(frame: &scap::frame::Frame) -> Size { } /// This is used by `get_screen_targets` and `start_default_target_screen_capture` to turn their -/// results into `Box`. They need to `Send` their capture source, and so -/// the capture source structs are used as `Box` is not `Send`. +/// results into `Rc`. They need to `Send` their capture source, and so +/// the capture source structs are used as `Rc` is not `Send`. fn to_dyn_screen_capture_sources( sources_rx: oneshot::Receiver>>, foreground_executor: &ForegroundExecutor, -) -> oneshot::Receiver>>> { +) -> oneshot::Receiver>>> { let (dyn_sources_tx, dyn_sources_rx) = oneshot::channel(); foreground_executor .spawn(async move { @@ -250,7 +293,7 @@ fn to_dyn_screen_capture_sources( Ok(Ok(results)) => dyn_sources_tx .send(Ok(results .into_iter() - .map(|source| Box::new(source) as Box) + .map(|source| Rc::new(source) as Rc) .collect::>())) .ok(), Ok(Err(err)) => dyn_sources_tx.send(Err(err)).ok(), diff --git a/crates/gpui/src/platform/test.rs b/crates/gpui/src/platform/test.rs index e4173b7c6ba2011bdea80514ac12fa862cade1e4..9227df5b63314b44a3c641835d00ba340aa909e8 100644 --- a/crates/gpui/src/platform/test.rs +++ b/crates/gpui/src/platform/test.rs @@ -8,4 +8,4 @@ pub(crate) use display::*; pub(crate) use platform::*; pub(crate) use window::*; -pub use platform::TestScreenCaptureSource; +pub use platform::{TestScreenCaptureSource, TestScreenCaptureStream}; diff --git a/crates/gpui/src/platform/test/platform.rs b/crates/gpui/src/platform/test/platform.rs index bef05399e52a6eb1a05552bb8693f5850274e98a..a26b65576cc49e290494762eed597d5bd8d0af26 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -2,7 +2,7 @@ use crate::{ AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels, ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, - Size, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams, size, + SourceMetadata, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams, size, }; use anyhow::Result; use collections::VecDeque; @@ -44,11 +44,17 @@ pub(crate) struct TestPlatform { /// A fake screen capture source, used for testing. pub struct TestScreenCaptureSource {} +/// A fake screen capture stream, used for testing. pub struct TestScreenCaptureStream {} impl ScreenCaptureSource for TestScreenCaptureSource { - fn resolution(&self) -> Result> { - Ok(size(DevicePixels(1), DevicePixels(1))) + fn metadata(&self) -> Result { + Ok(SourceMetadata { + id: 0, + is_main: None, + label: None, + resolution: size(DevicePixels(1), DevicePixels(1)), + }) } fn stream( @@ -64,7 +70,11 @@ impl ScreenCaptureSource for TestScreenCaptureSource { } } -impl ScreenCaptureStream for TestScreenCaptureStream {} +impl ScreenCaptureStream for TestScreenCaptureStream { + fn metadata(&self) -> Result { + TestScreenCaptureSource {}.metadata() + } +} struct TestPrompt { msg: String, @@ -271,13 +281,13 @@ impl Platform for TestPlatform { #[cfg(feature = "screen-capture")] fn screen_capture_sources( &self, - ) -> oneshot::Receiver>>> { + ) -> oneshot::Receiver>>> { let (mut tx, rx) = oneshot::channel(); tx.send(Ok(self .screen_capture_sources .borrow() .iter() - .map(|source| Box::new(source.clone()) as Box) + .map(|source| Rc::new(source.clone()) as Rc) .collect())) .ok(); rx diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index f69a802da07fab1636404e3aae0dfd8487d69479..401ecdeffecc1aefdab85ec1728aa6918a0e0857 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -440,7 +440,7 @@ impl Platform for WindowsPlatform { #[cfg(feature = "screen-capture")] fn screen_capture_sources( &self, - ) -> oneshot::Receiver>>> { + ) -> oneshot::Receiver>>> { crate::platform::scap_screen_capture::scap_screen_sources(&self.foreground_executor) } diff --git a/crates/livekit_client/src/livekit_client/playback.rs b/crates/livekit_client/src/livekit_client/playback.rs index 7e36314c12f24fcc696cb5d66f57717ed052a81b..c62b8853b4782055b919f6f46e95cc0c82693b33 100644 --- a/crates/livekit_client/src/livekit_client/playback.rs +++ b/crates/livekit_client/src/livekit_client/playback.rs @@ -326,11 +326,11 @@ pub(crate) async fn capture_local_video_track( capture_source: &dyn ScreenCaptureSource, cx: &mut gpui::AsyncApp, ) -> Result<(crate::LocalVideoTrack, Box)> { - let resolution = capture_source.resolution()?; + let metadata = capture_source.metadata()?; let track_source = gpui_tokio::Tokio::spawn(cx, async move { NativeVideoSource::new(VideoResolution { - width: resolution.width.0 as u32, - height: resolution.height.0 as u32, + width: metadata.resolution.width.0 as u32, + height: metadata.resolution.height.0 as u32, }) })? .await?; diff --git a/crates/livekit_client/src/mock_client/participant.rs b/crates/livekit_client/src/mock_client/participant.rs index 1f4168b8e04058f00af3b3117ba17dfa90947736..991d10bd5057014de3726ae4d1d0bc2c5b1b4661 100644 --- a/crates/livekit_client/src/mock_client/participant.rs +++ b/crates/livekit_client/src/mock_client/participant.rs @@ -5,7 +5,7 @@ use crate::{ }; use anyhow::Result; use collections::HashMap; -use gpui::{AsyncApp, ScreenCaptureSource, ScreenCaptureStream}; +use gpui::{AsyncApp, ScreenCaptureSource, ScreenCaptureStream, TestScreenCaptureStream}; #[derive(Clone, Debug)] pub struct LocalParticipant { @@ -119,7 +119,3 @@ impl RemoteParticipant { self.identity.clone() } } - -struct TestScreenCaptureStream; - -impl gpui::ScreenCaptureStream for TestScreenCaptureStream {} diff --git a/crates/title_bar/Cargo.toml b/crates/title_bar/Cargo.toml index 3c39e6b946a68824b8601a0978c702f51d06f5ec..8e95c6f79ff158a2f1dc4f03152a18d606b21727 100644 --- a/crates/title_bar/Cargo.toml +++ b/crates/title_bar/Cargo.toml @@ -27,6 +27,7 @@ test-support = [ ] [dependencies] +anyhow.workspace = true auto_update.workspace = true call.workspace = true chrono.workspace = true diff --git a/crates/title_bar/src/collab.rs b/crates/title_bar/src/collab.rs index b2a37a4f1c11c00139abe5c555f7ef254cc69f4c..1eebc0de0c7012231933619a432263ef911a8b7e 100644 --- a/crates/title_bar/src/collab.rs +++ b/crates/title_bar/src/collab.rs @@ -1,12 +1,20 @@ +use std::rc::Rc; use std::sync::Arc; use call::{ActiveCall, ParticipantLocation, Room}; use client::{User, proto::PeerId}; -use gpui::{AnyElement, Hsla, IntoElement, MouseButton, Path, Styled, canvas, point}; +use gpui::{ + AnyElement, Hsla, IntoElement, MouseButton, Path, ScreenCaptureSource, Styled, WeakEntity, + canvas, point, +}; use gpui::{App, Task, Window, actions}; use rpc::proto::{self}; use theme::ActiveTheme; -use ui::{Avatar, AvatarAudioStatusIndicator, Facepile, TintColor, Tooltip, prelude::*}; +use ui::{ + Avatar, AvatarAudioStatusIndicator, ContextMenu, ContextMenuItem, Facepile, PopoverMenu, + SplitButton, TintColor, Tooltip, prelude::*, +}; +use util::maybe; use workspace::notifications::DetachAndPromptErr; use crate::TitleBar; @@ -23,24 +31,49 @@ actions!( ] ); -fn toggle_screen_sharing(_: &ToggleScreenSharing, window: &mut Window, cx: &mut App) { +fn toggle_screen_sharing( + screen: Option>, + window: &mut Window, + cx: &mut App, +) { let call = ActiveCall::global(cx).read(cx); if let Some(room) = call.room().cloned() { let toggle_screen_sharing = room.update(cx, |room, cx| { - if room.is_screen_sharing() { + let clicked_on_currently_shared_screen = + room.shared_screen_id().is_some_and(|screen_id| { + Some(screen_id) + == screen + .as_deref() + .and_then(|s| s.metadata().ok().map(|meta| meta.id)) + }); + let should_unshare_current_screen = room.is_sharing_screen(); + let unshared_current_screen = should_unshare_current_screen.then(|| { telemetry::event!( "Screen Share Disabled", room_id = room.id(), channel_id = room.channel_id(), ); - Task::ready(room.unshare_screen(cx)) + room.unshare_screen(clicked_on_currently_shared_screen || screen.is_none(), cx) + }); + if let Some(screen) = screen { + if !should_unshare_current_screen { + telemetry::event!( + "Screen Share Enabled", + room_id = room.id(), + channel_id = room.channel_id(), + ); + } + cx.spawn(async move |room, cx| { + unshared_current_screen.transpose()?; + if !clicked_on_currently_shared_screen { + room.update(cx, |room, cx| room.share_screen(screen, cx))? + .await + } else { + Ok(()) + } + }) } else { - telemetry::event!( - "Screen Share Enabled", - room_id = room.id(), - channel_id = room.channel_id(), - ); - room.share_screen(cx) + Task::ready(Ok(())) } }); toggle_screen_sharing.detach_and_prompt_err("Sharing Screen Failed", window, cx, |e, _, _| Some(format!("{:?}\n\nPlease check that you have given Zed permissions to record your screen in Settings.", e))); @@ -303,7 +336,7 @@ impl TitleBar { let is_muted = room.is_muted(); let muted_by_user = room.muted_by_user(); let is_deafened = room.is_deafened().unwrap_or(false); - let is_screen_sharing = room.is_screen_sharing(); + let is_screen_sharing = room.is_sharing_screen(); let can_use_microphone = room.can_use_microphone(); let can_share_projects = room.can_share_projects(); let screen_sharing_supported = cx.is_screen_capture_supported(); @@ -428,21 +461,43 @@ impl TitleBar { ); if can_use_microphone && screen_sharing_supported { + let trigger = IconButton::new("screen-share", ui::IconName::Screen) + .style(ButtonStyle::Subtle) + .icon_size(IconSize::Small) + .toggle_state(is_screen_sharing) + .selected_style(ButtonStyle::Tinted(TintColor::Accent)) + .tooltip(Tooltip::text(if is_screen_sharing { + "Stop Sharing Screen" + } else { + "Share Screen" + })) + .on_click(move |_, window, cx| { + let should_share = ActiveCall::global(cx) + .read(cx) + .room() + .is_some_and(|room| !room.read(cx).is_sharing_screen()); + + window + .spawn(cx, async move |cx| { + let screen = if should_share { + cx.update(|_, cx| pick_default_screen(cx))?.await + } else { + None + }; + + cx.update(|window, cx| toggle_screen_sharing(screen, window, cx))?; + + Result::<_, anyhow::Error>::Ok(()) + }) + .detach(); + }); + children.push( - IconButton::new("screen-share", ui::IconName::Screen) - .style(ButtonStyle::Subtle) - .icon_size(IconSize::Small) - .toggle_state(is_screen_sharing) - .selected_style(ButtonStyle::Tinted(TintColor::Accent)) - .tooltip(Tooltip::text(if is_screen_sharing { - "Stop Sharing Screen" - } else { - "Share Screen" - })) - .on_click(move |_, window, cx| { - toggle_screen_sharing(&Default::default(), window, cx) - }) - .into_any_element(), + SplitButton::new( + trigger.render(window, cx), + self.render_screen_list().into_any_element(), + ) + .into_any_element(), ); } @@ -450,4 +505,89 @@ impl TitleBar { children } + + fn render_screen_list(&self) -> impl IntoElement { + PopoverMenu::new("screen-share-screen-list") + .with_handle(self.screen_share_popover_handle.clone()) + .trigger( + ui::ButtonLike::new_rounded_right("screen-share-screen-list-trigger") + .layer(ui::ElevationIndex::ModalSurface) + .size(ui::ButtonSize::None) + .child( + div() + .px_1() + .child(Icon::new(IconName::ChevronDownSmall).size(IconSize::XSmall)), + ) + .toggle_state(self.screen_share_popover_handle.is_deployed()), + ) + .menu(|window, cx| { + let screens = cx.screen_capture_sources(); + Some(ContextMenu::build(window, cx, |context_menu, _, cx| { + cx.spawn(async move |this: WeakEntity, cx| { + let screens = screens.await??; + this.update(cx, |this, cx| { + let active_screenshare_id = ActiveCall::global(cx) + .read(cx) + .room() + .and_then(|room| room.read(cx).shared_screen_id()); + for screen in screens { + let Ok(meta) = screen.metadata() else { + continue; + }; + + let label = meta + .label + .clone() + .unwrap_or_else(|| SharedString::from("Unknown screen")); + let resolution = SharedString::from(format!( + "{} × {}", + meta.resolution.width.0, meta.resolution.height.0 + )); + this.push_item(ContextMenuItem::CustomEntry { + entry_render: Box::new(move |_, _| { + h_flex() + .gap_2() + .child(Icon::new(IconName::Screen).when( + active_screenshare_id == Some(meta.id), + |this| this.color(Color::Accent), + )) + .child(Label::new(label.clone())) + .child( + Label::new(resolution.clone()) + .color(Color::Muted) + .size(LabelSize::Small), + ) + .into_any() + }), + selectable: true, + documentation_aside: None, + handler: Rc::new(move |_, window, cx| { + toggle_screen_sharing(Some(screen.clone()), window, cx); + }), + }); + } + }) + }) + .detach_and_log_err(cx); + context_menu + })) + }) + } +} + +/// Picks the screen to share when clicking on the main screen sharing button. +fn pick_default_screen(cx: &App) -> Task>> { + let source = cx.screen_capture_sources(); + cx.spawn(async move |_| { + let available_sources = maybe!(async move { source.await? }).await.ok()?; + available_sources + .iter() + .find(|it| { + it.as_ref() + .metadata() + .is_ok_and(|meta| meta.is_main.unwrap_or_default()) + }) + .or_else(|| available_sources.iter().next()) + .cloned() + }) } diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index c4fdb16f4f5d8b18b7e2b536198cc1ba61ec04d8..17c4c85b6d7ebfa4515748c89e7896a008d1c839 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -36,7 +36,7 @@ use theme::ActiveTheme; use title_bar_settings::TitleBarSettings; use ui::{ Avatar, Button, ButtonLike, ButtonStyle, Chip, ContextMenu, Icon, IconName, IconSize, - IconWithIndicator, Indicator, PopoverMenu, Tooltip, h_flex, prelude::*, + IconWithIndicator, Indicator, PopoverMenu, PopoverMenuHandle, Tooltip, h_flex, prelude::*, }; use util::ResultExt; use workspace::{Workspace, notifications::NotifyResultExt}; @@ -131,6 +131,7 @@ pub struct TitleBar { application_menu: Option>, _subscriptions: Vec, banner: Entity, + screen_share_popover_handle: PopoverMenuHandle, } impl Render for TitleBar { @@ -295,6 +296,7 @@ impl TitleBar { client, _subscriptions: subscriptions, banner, + screen_share_popover_handle: Default::default(), } } diff --git a/crates/ui/src/components/button/icon_button.rs b/crates/ui/src/components/button/icon_button.rs index 050db6addd2ba32535edffdd6fde066ac57ec644..e5d13e09cd804cdf50970c082d8b944e890f74db 100644 --- a/crates/ui/src/components/button/icon_button.rs +++ b/crates/ui/src/components/button/icon_button.rs @@ -178,7 +178,8 @@ impl VisibleOnHover for IconButton { } impl RenderOnce for IconButton { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + #[allow(refining_impl_trait)] + fn render(self, window: &mut Window, cx: &mut App) -> ButtonLike { let is_disabled = self.base.disabled; let is_selected = self.base.selected; let selected_style = self.base.selected_style; diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index 467dd226fbffb4fe54d7e7564c736431b8be094b..77468fd29596a2aae015e73ad2d618c82031128c 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -139,6 +139,8 @@ impl ContextMenuEntry { } } +impl FluentBuilder for ContextMenuEntry {} + impl From for ContextMenuItem { fn from(entry: ContextMenuEntry) -> Self { ContextMenuItem::Entry(entry) @@ -353,6 +355,10 @@ impl ContextMenu { self } + pub fn push_item(&mut self, item: impl Into) { + self.items.push(item.into()); + } + pub fn entry( mut self, label: impl Into,