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 <kirill@zed.dev>
Co-authored-by: Cole Miller <cole@zed.dev>
This commit is contained in:
Piotr Osiewicz 2025-07-21 13:44:51 +02:00 committed by GitHub
parent 57ab09c2da
commit 88af35fe47
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 473 additions and 145 deletions

View file

@ -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!(

View file

@ -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()
}

View file

@ -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.

View file

@ -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,

View file

@ -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()
}

View file

@ -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 {

View file

@ -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(),

View file

@ -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};

View file

@ -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

View file

@ -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)
}