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  Release Notes: - Added screen selector dropdown to screen share button --------- Co-authored-by: Kirill Bulatov <kirill@zed.dev> Co-authored-by: Cole Miller <cole@zed.dev>
This commit is contained in:
parent
57ab09c2da
commit
88af35fe47
26 changed files with 473 additions and 145 deletions
|
@ -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]
|
||||
|
|
6
Cargo.lock
generated
6
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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<u64> {
|
||||
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<Self>) -> Task<Result<()>> {
|
||||
pub fn share_screen(
|
||||
&mut self,
|
||||
source: Rc<dyn ScreenCaptureSource>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
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<Self>) -> Result<()> {
|
||||
pub fn unshare_screen(&mut self, play_sound: bool, cx: &mut Context<Self>) -> 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<livekit::Room>,
|
||||
screen_track: LocalTrack,
|
||||
microphone_track: LocalTrack,
|
||||
screen_track: LocalTrack<dyn ScreenCaptureStream>,
|
||||
microphone_track: LocalTrack<AudioStream>,
|
||||
/// 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<Stream: ?Sized> {
|
||||
None,
|
||||
Pending {
|
||||
publish_id: usize,
|
||||
},
|
||||
Published {
|
||||
track_publication: LocalTrackPublication,
|
||||
_stream: Box<dyn Any>,
|
||||
_stream: Box<Stream>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for LocalTrack {
|
||||
impl<T: ?Sized> Default for LocalTrack<T> {
|
||||
fn default() -> Self {
|
||||
Self::None
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -696,7 +696,7 @@ impl App {
|
|||
/// Returns a list of available screen capture sources.
|
||||
pub fn screen_capture_sources(
|
||||
&self,
|
||||
) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>> {
|
||||
) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
|
||||
self.platform.screen_capture_sources()
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Result<Vec<Box<dyn ScreenCaptureSource>>>>;
|
||||
fn screen_capture_sources(&self)
|
||||
-> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>>;
|
||||
#[cfg(not(feature = "screen-capture"))]
|
||||
fn screen_capture_sources(
|
||||
&self,
|
||||
) -> oneshot::Receiver<anyhow::Result<Vec<Box<dyn ScreenCaptureSource>>>> {
|
||||
) -> oneshot::Receiver<anyhow::Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
|
||||
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<SharedString>,
|
||||
/// Whether this source is the main display.
|
||||
pub is_main: Option<bool>,
|
||||
/// Video resolution of this source.
|
||||
pub resolution: Size<DevicePixels>,
|
||||
}
|
||||
|
||||
/// 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<Size<DevicePixels>>;
|
||||
/// Returns metadata for this source.
|
||||
fn metadata(&self) -> Result<SourceMetadata>;
|
||||
|
||||
/// 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<SourceMetadata>;
|
||||
}
|
||||
|
||||
/// A frame of video captured from a screen.
|
||||
pub struct ScreenCaptureFrame(pub PlatformScreenCaptureFrame);
|
||||
|
|
|
@ -73,7 +73,7 @@ impl LinuxClient for HeadlessClient {
|
|||
#[cfg(feature = "screen-capture")]
|
||||
fn screen_capture_sources(
|
||||
&self,
|
||||
) -> futures::channel::oneshot::Receiver<anyhow::Result<Vec<Box<dyn crate::ScreenCaptureSource>>>>
|
||||
) -> futures::channel::oneshot::Receiver<anyhow::Result<Vec<Rc<dyn crate::ScreenCaptureSource>>>>
|
||||
{
|
||||
let (mut tx, rx) = futures::channel::oneshot::channel();
|
||||
tx.send(Err(anyhow::anyhow!(
|
||||
|
|
|
@ -56,7 +56,7 @@ pub trait LinuxClient {
|
|||
#[cfg(feature = "screen-capture")]
|
||||
fn screen_capture_sources(
|
||||
&self,
|
||||
) -> oneshot::Receiver<Result<Vec<Box<dyn crate::ScreenCaptureSource>>>>;
|
||||
) -> oneshot::Receiver<Result<Vec<Rc<dyn crate::ScreenCaptureSource>>>>;
|
||||
|
||||
fn open_window(
|
||||
&self,
|
||||
|
@ -245,7 +245,7 @@ impl<P: LinuxClient + 'static> Platform for P {
|
|||
#[cfg(feature = "screen-capture")]
|
||||
fn screen_capture_sources(
|
||||
&self,
|
||||
) -> oneshot::Receiver<Result<Vec<Box<dyn crate::ScreenCaptureSource>>>> {
|
||||
) -> oneshot::Receiver<Result<Vec<Rc<dyn crate::ScreenCaptureSource>>>> {
|
||||
self.screen_capture_sources()
|
||||
}
|
||||
|
||||
|
|
|
@ -672,7 +672,7 @@ impl LinuxClient for WaylandClient {
|
|||
#[cfg(feature = "screen-capture")]
|
||||
fn screen_capture_sources(
|
||||
&self,
|
||||
) -> futures::channel::oneshot::Receiver<anyhow::Result<Vec<Box<dyn crate::ScreenCaptureSource>>>>
|
||||
) -> futures::channel::oneshot::Receiver<anyhow::Result<Vec<Rc<dyn crate::ScreenCaptureSource>>>>
|
||||
{
|
||||
// TODO: Get screen capture working on wayland. Be sure to try window resizing as that may
|
||||
// be tricky.
|
||||
|
|
|
@ -1448,7 +1448,7 @@ impl LinuxClient for X11Client {
|
|||
#[cfg(feature = "screen-capture")]
|
||||
fn screen_capture_sources(
|
||||
&self,
|
||||
) -> futures::channel::oneshot::Receiver<anyhow::Result<Vec<Box<dyn crate::ScreenCaptureSource>>>>
|
||||
) -> futures::channel::oneshot::Receiver<anyhow::Result<Vec<Rc<dyn crate::ScreenCaptureSource>>>>
|
||||
{
|
||||
crate::platform::scap_screen_capture::scap_screen_sources(
|
||||
&self.0.borrow().common.foreground_executor,
|
||||
|
|
|
@ -583,7 +583,7 @@ impl Platform for MacPlatform {
|
|||
#[cfg(feature = "screen-capture")]
|
||||
fn screen_capture_sources(
|
||||
&self,
|
||||
) -> oneshot::Receiver<Result<Vec<Box<dyn crate::ScreenCaptureSource>>>> {
|
||||
) -> oneshot::Receiver<Result<Vec<Rc<dyn crate::ScreenCaptureSource>>>> {
|
||||
super::screen_capture::get_sources()
|
||||
}
|
||||
|
||||
|
|
|
@ -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<ScreenMeta>,
|
||||
}
|
||||
|
||||
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<Size<DevicePixels>> {
|
||||
unsafe {
|
||||
fn metadata(&self) -> Result<SourceMetadata> {
|
||||
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<SourceMetadata> {
|
||||
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<Result<Vec<Box<dyn ScreenCaptureSource>>>> {
|
||||
#[derive(Clone)]
|
||||
struct ScreenMeta {
|
||||
label: SharedString,
|
||||
// Is this the screen with menu bar?
|
||||
is_main: bool,
|
||||
}
|
||||
|
||||
unsafe fn screen_id_to_human_label() -> HashMap<CGDirectDisplayID, ScreenMeta> {
|
||||
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<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
|
||||
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<dyn ScreenCaptureSource>);
|
||||
result.push(Rc::new(source) as Rc<dyn ScreenCaptureSource>);
|
||||
}
|
||||
Ok(result)
|
||||
} else {
|
||||
|
|
|
@ -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<Result<Vec<Box<dyn ScreenCaptureSource>>>> {
|
||||
) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
|
||||
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<Result<Vec<Box<dyn ScreenCaptureSource>>>> {
|
||||
) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
|
||||
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<DevicePixels>,
|
||||
}
|
||||
|
||||
|
@ -52,7 +54,7 @@ fn get_screen_targets(sources_tx: oneshot::Sender<Result<Vec<ScapCaptureSource>>
|
|||
}
|
||||
};
|
||||
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<Result<Vec<ScapCaptureSource>>
|
|||
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<Result<Vec<ScapCaptureSource>>
|
|||
}
|
||||
|
||||
impl ScreenCaptureSource for ScapCaptureSource {
|
||||
fn resolution(&self) -> Result<Size<DevicePixels>> {
|
||||
Ok(self.size)
|
||||
fn metadata(&self) -> Result<SourceMetadata> {
|
||||
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<dyn Fn(ScreenCaptureFrame) + Send>,
|
||||
)>,
|
||||
target: scap::Display,
|
||||
size: Size<DevicePixels>,
|
||||
}
|
||||
|
||||
|
@ -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<Size<DevicePixels>> {
|
||||
Ok(self.size)
|
||||
fn metadata(&self) -> Result<SourceMetadata> {
|
||||
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<scap::Target>) -> Result<scap::capturer::Cap
|
|||
|
||||
fn run_capture(
|
||||
mut capturer: scap::capturer::Capturer,
|
||||
display: scap::Display,
|
||||
frame_callback: Box<dyn Fn(ScreenCaptureFrame) + Send>,
|
||||
stream_tx: oneshot::Sender<Result<ScapStream>>,
|
||||
) {
|
||||
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<AtomicBool>,
|
||||
display: scap::Display,
|
||||
size: Size<DevicePixels>,
|
||||
}
|
||||
|
||||
impl ScreenCaptureStream for ScapStream {}
|
||||
impl ScreenCaptureStream for ScapStream {
|
||||
fn metadata(&self) -> Result<SourceMetadata> {
|
||||
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<DevicePixels> {
|
|||
}
|
||||
|
||||
/// This is used by `get_screen_targets` and `start_default_target_screen_capture` to turn their
|
||||
/// results into `Box<dyn ScreenCaptureSource>`. They need to `Send` their capture source, and so
|
||||
/// the capture source structs are used as `Box<dyn ScreenCaptureSource>` is not `Send`.
|
||||
/// results into `Rc<dyn ScreenCaptureSource>`. They need to `Send` their capture source, and so
|
||||
/// the capture source structs are used as `Rc<dyn ScreenCaptureSource>` is not `Send`.
|
||||
fn to_dyn_screen_capture_sources<T: ScreenCaptureSource + 'static>(
|
||||
sources_rx: oneshot::Receiver<Result<Vec<T>>>,
|
||||
foreground_executor: &ForegroundExecutor,
|
||||
) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>> {
|
||||
) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
|
||||
let (dyn_sources_tx, dyn_sources_rx) = oneshot::channel();
|
||||
foreground_executor
|
||||
.spawn(async move {
|
||||
|
@ -250,7 +293,7 @@ fn to_dyn_screen_capture_sources<T: ScreenCaptureSource + 'static>(
|
|||
Ok(Ok(results)) => dyn_sources_tx
|
||||
.send(Ok(results
|
||||
.into_iter()
|
||||
.map(|source| Box::new(source) as Box<dyn ScreenCaptureSource>)
|
||||
.map(|source| Rc::new(source) as Rc<dyn ScreenCaptureSource>)
|
||||
.collect::<Vec<_>>()))
|
||||
.ok(),
|
||||
Ok(Err(err)) => dyn_sources_tx.send(Err(err)).ok(),
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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<Size<DevicePixels>> {
|
||||
Ok(size(DevicePixels(1), DevicePixels(1)))
|
||||
fn metadata(&self) -> Result<SourceMetadata> {
|
||||
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<SourceMetadata> {
|
||||
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<Result<Vec<Box<dyn ScreenCaptureSource>>>> {
|
||||
) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
|
||||
let (mut tx, rx) = oneshot::channel();
|
||||
tx.send(Ok(self
|
||||
.screen_capture_sources
|
||||
.borrow()
|
||||
.iter()
|
||||
.map(|source| Box::new(source.clone()) as Box<dyn ScreenCaptureSource>)
|
||||
.map(|source| Rc::new(source.clone()) as Rc<dyn ScreenCaptureSource>)
|
||||
.collect()))
|
||||
.ok();
|
||||
rx
|
||||
|
|
|
@ -440,7 +440,7 @@ impl Platform for WindowsPlatform {
|
|||
#[cfg(feature = "screen-capture")]
|
||||
fn screen_capture_sources(
|
||||
&self,
|
||||
) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>> {
|
||||
) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
|
||||
crate::platform::scap_screen_capture::scap_screen_sources(&self.foreground_executor)
|
||||
}
|
||||
|
||||
|
|
|
@ -326,11 +326,11 @@ pub(crate) async fn capture_local_video_track(
|
|||
capture_source: &dyn ScreenCaptureSource,
|
||||
cx: &mut gpui::AsyncApp,
|
||||
) -> Result<(crate::LocalVideoTrack, Box<dyn ScreenCaptureStream>)> {
|
||||
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?;
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -27,6 +27,7 @@ test-support = [
|
|||
]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
auto_update.workspace = true
|
||||
call.workspace = true
|
||||
chrono.workspace = true
|
||||
|
|
|
@ -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<Rc<dyn ScreenCaptureSource>>,
|
||||
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<ContextMenu>, 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<Option<Rc<dyn ScreenCaptureSource>>> {
|
||||
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()
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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<Entity<ApplicationMenu>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
banner: Entity<OnboardingBanner>,
|
||||
screen_share_popover_handle: PopoverMenuHandle<ContextMenu>,
|
||||
}
|
||||
|
||||
impl Render for TitleBar {
|
||||
|
@ -295,6 +296,7 @@ impl TitleBar {
|
|||
client,
|
||||
_subscriptions: subscriptions,
|
||||
banner,
|
||||
screen_share_popover_handle: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -139,6 +139,8 @@ impl ContextMenuEntry {
|
|||
}
|
||||
}
|
||||
|
||||
impl FluentBuilder for ContextMenuEntry {}
|
||||
|
||||
impl From<ContextMenuEntry> 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<ContextMenuItem>) {
|
||||
self.items.push(item.into());
|
||||
}
|
||||
|
||||
pub fn entry(
|
||||
mut self,
|
||||
label: impl Into<SharedString>,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue