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

@ -27,6 +27,7 @@ test-support = [
]
[dependencies]
anyhow.workspace = true
auto_update.workspace = true
call.workspace = true
chrono.workspace = true

View file

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

View file

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