From c9210c3be1d6e0f20406aa3770985d5c59cd4384 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 25 Sep 2025 14:26:45 -0600 Subject: [PATCH] WIP. New in-call controls Co-authored-by: Matt Miller --- assets/icons/audio.svg | 8 + assets/themes/one/one.json | 4 +- crates/collab_ui/src/call_overlay.rs | 335 ++++++++++++++++++++++++++- crates/icons/src/icons.rs | 1 + 4 files changed, 341 insertions(+), 7 deletions(-) create mode 100644 assets/icons/audio.svg diff --git a/assets/icons/audio.svg b/assets/icons/audio.svg new file mode 100644 index 0000000000000000000000000000000000000000..7948b046160e92eb9e0d3cce3e28e3c007ce0a83 --- /dev/null +++ b/assets/icons/audio.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/themes/one/one.json b/assets/themes/one/one.json index 7cc8c96a23f32aab69596722188e3c5ec87aba08..1e4e2cc486c05ffb9fe094ef582771c6bb95e37e 100644 --- a/assets/themes/one/one.json +++ b/assets/themes/one/one.json @@ -13,7 +13,7 @@ "border.selected": "#293b5bff", "border.transparent": "#00000000", "border.disabled": "#414754ff", - "elevated_surface.background": "#2f343eff", + "elevated_surface.background": "#3F4550FF", "surface.background": "#2f343eff", "background": "#3b414dff", "element.background": "#2e343eff", @@ -414,7 +414,7 @@ "border.selected": "#cbcdf6ff", "border.transparent": "#00000000", "border.disabled": "#d3d3d4ff", - "elevated_surface.background": "#ebebecff", + "elevated_surface.background": "#ffffffff", "surface.background": "#ebebecff", "background": "#dcdcddff", "element.background": "#ebebecff", diff --git a/crates/collab_ui/src/call_overlay.rs b/crates/collab_ui/src/call_overlay.rs index 9fbe3fb4f5881affb9a26ad14f30d80ed167bbdc..5c1611749e0047797d3e19c3fd8f483c06cabb52 100644 --- a/crates/collab_ui/src/call_overlay.rs +++ b/crates/collab_ui/src/call_overlay.rs @@ -1,20 +1,345 @@ -use gpui::AppContext; -use ui::{App, Context, IntoElement, Render, Styled, Window, div, px}; +use std::rc::Rc; + +use call::{ActiveCall, Room}; +use channel::ChannelStore; +use gpui::{AppContext, Entity, RenderOnce, WeakEntity}; +use project::Project; +use ui::{ + ActiveTheme, AnyElement, App, Avatar, Button, ButtonCommon, ButtonSize, ButtonStyle, Clickable, + Color, Context, ContextMenu, ContextMenuItem, Element, FluentBuilder, Icon, IconButton, + IconName, IconSize, IntoElement, Label, LabelCommon, LabelSize, ParentElement, PopoverMenu, + PopoverMenuHandle, Render, SelectableButton, SharedString, SplitButton, SplitButtonStyle, + Styled, StyledExt, TintColor, Toggleable, Tooltip, Window, div, h_flex, px, v_flex, +}; use workspace::Workspace; -pub struct CallOverlay {} +pub struct CallOverlay { + active_call: Entity, + channel_store: Entity, + project: Entity, + workspace: WeakEntity, + screen_share_popover_handle: PopoverMenuHandle, +} + +impl CallOverlay { + pub(crate) fn render_call_controls( + &self, + window: &mut Window, + cx: &mut Context, + ) -> Vec { + let Some(room) = self.active_call.read(cx).room() else { + return Vec::default(); + }; + + let room = room.read(cx); + let project = self.project.read(cx); + let is_local = project.is_local() || project.is_via_remote_server(); + let is_shared = is_local && project.is_shared(); + 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_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(); + let is_connecting_to_project = self + .workspace + .update(cx, |workspace, cx| workspace.has_active_modal(window, cx)) + .unwrap_or(false); + + let mut children = Vec::new(); + + if can_use_microphone { + children.push( + IconButton::new( + "mute-microphone", + if is_muted { + IconName::MicMute + } else { + IconName::Mic + }, + ) + .tooltip(move |window, cx| { + if is_muted { + if is_deafened { + Tooltip::with_meta( + "Unmute Microphone", + None, + "Audio will be unmuted", + window, + cx, + ) + } else { + Tooltip::simple("Unmute Microphone", cx) + } + } else { + Tooltip::simple("Mute Microphone", cx) + } + }) + .style(ButtonStyle::Subtle) + .icon_size(IconSize::Small) + .toggle_state(is_muted) + .selected_icon_color(Color::Error) + .on_click(move |_, _window, cx| { + // toggle_mute(&Default::default(), cx); + // todo!() + }) + .into_any_element(), + ); + } + + children.push( + IconButton::new( + "mute-sound", + if is_deafened { + IconName::AudioOff + } else { + IconName::AudioOn + }, + ) + .style(ButtonStyle::Subtle) + .selected_icon_color(Color::Error) + .icon_size(IconSize::Small) + .toggle_state(is_deafened) + .tooltip(move |window, cx| { + if is_deafened { + let label = "Unmute Audio"; + + if !muted_by_user { + Tooltip::with_meta(label, None, "Microphone will be unmuted", window, cx) + } else { + Tooltip::simple(label, cx) + } + } else { + let label = "Mute Audio"; + + if !muted_by_user { + Tooltip::with_meta(label, None, "Microphone will be muted", window, cx) + } else { + Tooltip::simple(label, cx) + } + } + }) + .on_click(move |_, _, cx| { + // toggle_deafen(&Default::default(), cx)) + // todo!() + }) + .into_any_element(), + ); + + if can_use_microphone && screen_sharing_supported { + children.push( + IconButton::new("screen-share", IconName::Screen) + .style(ButtonStyle::Subtle) + .icon_size(IconSize::Small) + .toggle_state(is_screen_sharing) + .selected_icon_color(Color::Error) + .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)} + // // // todo!() + // // })? + // // .await + // } else { + // Ok(None) + // }; + // cx.update(|window, cx| { + // // toggle_screen_sharing(screen, window, cx) + // // todo!() + // })?; + + // Result::<_, anyhow::Error>::Ok(()) + // }) + // .detach(); + // self.render_screen_list().into_any_element(), + }) + .into_any_element(), + ); + + // children.push( + // SplitButton::new(trigger.render(window, cx)) + // .style(SplitButtonStyle::Transparent) + // .into_any_element(), + // ); + } + + children.push(div().pr_2().into_any_element()); + + 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") + .child( + h_flex() + .mx_neg_0p5() + .h_full() + .justify_center() + .child(Icon::new(IconName::ChevronDown).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) + .size(IconSize::XSmall) + .map(|this| { + if active_screenshare_id == Some(meta.id) { + this.color(Color::Accent) + } else { + this.color(Color::Muted) + } + }), + ) + .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(Ok(Some(screen.clone())), window, cx); + }), + }); + } + }) + }) + .detach_and_log_err(cx); + context_menu + })) + }) + } +} impl Render for CallOverlay { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - div().w(px(100.)).h(px(100.)).bg(gpui::blue()) + let Some(room) = self.active_call.read(cx).room() else { + return gpui::Empty.into_any_element(); + }; + + let title = if let Some(channel_id) = room.read(cx).channel_id() + && let Some(channel) = self.channel_store.read(cx).channel_for_id(channel_id) + { + channel.name.clone() + } else { + "Unknown".into() + }; + + div() + .p_1() + .child( + v_flex() + .elevation_3(cx) + .bg(cx.theme().colors().editor_background) + .p_2() + .w_full() + .gap_2() + .child( + h_flex() + .justify_between() + .child( + h_flex() + .gap_1() + .child( + Icon::new(IconName::Audio) + .color(Color::VersionControlAdded), + ) + .child(Label::new(title)), + ) + .child(Icon::new(IconName::ChevronDown)), + ) + .child( + h_flex() + .justify_between() + .child(h_flex().children(self.render_call_controls(window, cx))) + .child( + h_flex() + .gap_1() + .child( + Button::new("leave-call", "Leave") + .icon(Some(IconName::Exit)) + .label_size(LabelSize::Small) + .style(ButtonStyle::Tinted(TintColor::Error)) + .tooltip(Tooltip::text("Leave Call")) + .icon_size(IconSize::Small) + .on_click(move |_, _window, cx| { + ActiveCall::global(cx) + .update(cx, |call, cx| call.hang_up(cx)) + .detach_and_log_err(cx); + }), + ) + .into_any_element(), + ), + ), + ) + .into_any_element() } } pub fn init(cx: &App) { cx.observe_new(|workspace: &mut Workspace, _, cx| { let dock = workspace.dock_at_position(workspace::dock::DockPosition::Left); + let handle = cx.weak_entity(); + let project = workspace.project().clone(); dock.update(cx, |dock, cx| { - let overlay = cx.new(|_| CallOverlay {}); + let overlay = cx.new(|cx| { + let active_call = ActiveCall::global(cx); + cx.observe(&active_call, |_, _, cx| cx.notify()).detach(); + let channel_store = ChannelStore::global(cx); + CallOverlay { + channel_store, + active_call, + workspace: handle, + project, + screen_share_popover_handle: PopoverMenuHandle::default(), + } + }); dock.add_overlay( cx, Box::new(move |window, cx| { diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index 0f05e58c27c48c37043fe90f64b4f03968b22752..90a252b6a8aeb4cfafeed88a4e9de77038d8e343 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -35,6 +35,7 @@ pub enum IconName { ArrowUp, ArrowUpRight, Attach, + Audio, AudioOff, AudioOn, Backspace,