1use std::rc::Rc;
2
3use call::{ActiveCall, Room};
4use channel::ChannelStore;
5use gpui::{AppContext, Entity, RenderOnce, WeakEntity};
6use project::Project;
7use ui::{
8 ActiveTheme, AnyElement, App, Avatar, Button, ButtonCommon, ButtonSize, ButtonStyle, Clickable,
9 Color, Context, ContextMenu, ContextMenuItem, Element, FluentBuilder, Icon, IconButton,
10 IconName, IconSize, IntoElement, Label, LabelCommon, LabelSize, ParentElement, PopoverMenu,
11 PopoverMenuHandle, Render, SelectableButton, SharedString, SplitButton, SplitButtonStyle,
12 Styled, StyledExt, TintColor, Toggleable, Tooltip, Window, div, h_flex, px, v_flex,
13};
14use workspace::Workspace;
15
16pub struct CallOverlay {
17 active_call: Entity<ActiveCall>,
18 channel_store: Entity<ChannelStore>,
19 project: Entity<Project>,
20 workspace: WeakEntity<Workspace>,
21 screen_share_popover_handle: PopoverMenuHandle<ContextMenu>,
22}
23
24impl CallOverlay {
25 pub(crate) fn render_call_controls(
26 &self,
27 window: &mut Window,
28 cx: &mut Context<Self>,
29 ) -> Vec<AnyElement> {
30 let Some(room) = self.active_call.read(cx).room() else {
31 return Vec::default();
32 };
33
34 let room = room.read(cx);
35 let project = self.project.read(cx);
36 let is_local = project.is_local() || project.is_via_remote_server();
37 let is_shared = is_local && project.is_shared();
38 let is_muted = room.is_muted();
39 let muted_by_user = room.muted_by_user();
40 let is_deafened = room.is_deafened().unwrap_or(false);
41 let is_screen_sharing = room.is_sharing_screen();
42 let can_use_microphone = room.can_use_microphone();
43 let can_share_projects = room.can_share_projects();
44 let screen_sharing_supported = cx.is_screen_capture_supported();
45 let is_connecting_to_project = self
46 .workspace
47 .update(cx, |workspace, cx| workspace.has_active_modal(window, cx))
48 .unwrap_or(false);
49
50 let mut children = Vec::new();
51
52 if can_use_microphone {
53 children.push(
54 IconButton::new(
55 "mute-microphone",
56 if is_muted {
57 IconName::MicMute
58 } else {
59 IconName::Mic
60 },
61 )
62 .tooltip(move |window, cx| {
63 if is_muted {
64 if is_deafened {
65 Tooltip::with_meta(
66 "Unmute Microphone",
67 None,
68 "Audio will be unmuted",
69 window,
70 cx,
71 )
72 } else {
73 Tooltip::simple("Unmute Microphone", cx)
74 }
75 } else {
76 Tooltip::simple("Mute Microphone", cx)
77 }
78 })
79 .style(ButtonStyle::Subtle)
80 .icon_size(IconSize::Small)
81 .toggle_state(is_muted)
82 .selected_icon_color(Color::Error)
83 .on_click(move |_, _window, cx| {
84 // toggle_mute(&Default::default(), cx);
85 // todo!()
86 })
87 .into_any_element(),
88 );
89 }
90
91 children.push(
92 IconButton::new(
93 "mute-sound",
94 if is_deafened {
95 IconName::AudioOff
96 } else {
97 IconName::AudioOn
98 },
99 )
100 .style(ButtonStyle::Subtle)
101 .selected_icon_color(Color::Error)
102 .icon_size(IconSize::Small)
103 .toggle_state(is_deafened)
104 .tooltip(move |window, cx| {
105 if is_deafened {
106 let label = "Unmute Audio";
107
108 if !muted_by_user {
109 Tooltip::with_meta(label, None, "Microphone will be unmuted", window, cx)
110 } else {
111 Tooltip::simple(label, cx)
112 }
113 } else {
114 let label = "Mute Audio";
115
116 if !muted_by_user {
117 Tooltip::with_meta(label, None, "Microphone will be muted", window, cx)
118 } else {
119 Tooltip::simple(label, cx)
120 }
121 }
122 })
123 .on_click(move |_, _, cx| {
124 // toggle_deafen(&Default::default(), cx))
125 // todo!()
126 })
127 .into_any_element(),
128 );
129
130 if can_use_microphone && screen_sharing_supported {
131 children.push(
132 IconButton::new("screen-share", IconName::Screen)
133 .style(ButtonStyle::Subtle)
134 .icon_size(IconSize::Small)
135 .toggle_state(is_screen_sharing)
136 .selected_icon_color(Color::Error)
137 .tooltip(Tooltip::text(if is_screen_sharing {
138 "Stop Sharing Screen"
139 } else {
140 "Share Screen"
141 }))
142 .on_click(move |_, window, cx| {
143 let should_share = ActiveCall::global(cx)
144 .read(cx)
145 .room()
146 .is_some_and(|room| !room.read(cx).is_sharing_screen());
147
148 // window
149 // .spawn(cx, async move |cx| {
150 // let screen = if should_share {
151 // // cx.update(|_, cx| {
152 // // // pick_default_screen(cx)}
153 // // // todo!()
154 // // })?
155 // // .await
156 // } else {
157 // Ok(None)
158 // };
159 // cx.update(|window, cx| {
160 // // toggle_screen_sharing(screen, window, cx)
161 // // todo!()
162 // })?;
163
164 // Result::<_, anyhow::Error>::Ok(())
165 // })
166 // .detach();
167 // self.render_screen_list().into_any_element(),
168 })
169 .into_any_element(),
170 );
171
172 // children.push(
173 // SplitButton::new(trigger.render(window, cx))
174 // .style(SplitButtonStyle::Transparent)
175 // .into_any_element(),
176 // );
177 }
178
179 children.push(div().pr_2().into_any_element());
180
181 children
182 }
183
184 fn render_screen_list(&self) -> impl IntoElement {
185 PopoverMenu::new("screen-share-screen-list")
186 .with_handle(self.screen_share_popover_handle.clone())
187 .trigger(
188 ui::ButtonLike::new_rounded_right("screen-share-screen-list-trigger")
189 .child(
190 h_flex()
191 .mx_neg_0p5()
192 .h_full()
193 .justify_center()
194 .child(Icon::new(IconName::ChevronDown).size(IconSize::XSmall)),
195 )
196 .toggle_state(self.screen_share_popover_handle.is_deployed()),
197 )
198 .menu(|window, cx| {
199 let screens = cx.screen_capture_sources();
200 Some(ContextMenu::build(window, cx, |context_menu, _, cx| {
201 cx.spawn(async move |this: WeakEntity<ContextMenu>, cx| {
202 let screens = screens.await??;
203 this.update(cx, |this, cx| {
204 let active_screenshare_id = ActiveCall::global(cx)
205 .read(cx)
206 .room()
207 .and_then(|room| room.read(cx).shared_screen_id());
208 for screen in screens {
209 let Ok(meta) = screen.metadata() else {
210 continue;
211 };
212
213 let label = meta
214 .label
215 .clone()
216 .unwrap_or_else(|| SharedString::from("Unknown screen"));
217 let resolution = SharedString::from(format!(
218 "{} × {}",
219 meta.resolution.width.0, meta.resolution.height.0
220 ));
221 this.push_item(ContextMenuItem::CustomEntry {
222 entry_render: Box::new(move |_, _| {
223 h_flex()
224 .gap_2()
225 .child(
226 Icon::new(IconName::Screen)
227 .size(IconSize::XSmall)
228 .map(|this| {
229 if active_screenshare_id == Some(meta.id) {
230 this.color(Color::Accent)
231 } else {
232 this.color(Color::Muted)
233 }
234 }),
235 )
236 .child(Label::new(label.clone()))
237 .child(
238 Label::new(resolution.clone())
239 .color(Color::Muted)
240 .size(LabelSize::Small),
241 )
242 .into_any()
243 }),
244 selectable: true,
245 documentation_aside: None,
246 handler: Rc::new(move |_, window, cx| {
247 // toggle_screen_sharing(Ok(Some(screen.clone())), window, cx);
248 }),
249 });
250 }
251 })
252 })
253 .detach_and_log_err(cx);
254 context_menu
255 }))
256 })
257 }
258}
259
260impl Render for CallOverlay {
261 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
262 let Some(room) = self.active_call.read(cx).room() else {
263 return gpui::Empty.into_any_element();
264 };
265
266 let title = if let Some(channel_id) = room.read(cx).channel_id()
267 && let Some(channel) = self.channel_store.read(cx).channel_for_id(channel_id)
268 {
269 channel.name.clone()
270 } else {
271 "Unknown".into()
272 };
273
274 div()
275 .p_1()
276 .child(
277 v_flex()
278 .elevation_3(cx)
279 .bg(cx.theme().colors().editor_background)
280 .p_2()
281 .w_full()
282 .gap_2()
283 .child(
284 h_flex()
285 .justify_between()
286 .child(
287 h_flex()
288 .gap_1()
289 .child(
290 Icon::new(IconName::Audio)
291 .color(Color::VersionControlAdded),
292 )
293 .child(Label::new(title)),
294 )
295 .child(Icon::new(IconName::ChevronDown)),
296 )
297 .child(
298 h_flex()
299 .justify_between()
300 .child(h_flex().children(self.render_call_controls(window, cx)))
301 .child(
302 h_flex()
303 .gap_1()
304 .child(
305 Button::new("leave-call", "Leave")
306 .icon(Some(IconName::Exit))
307 .label_size(LabelSize::Small)
308 .style(ButtonStyle::Tinted(TintColor::Error))
309 .tooltip(Tooltip::text("Leave Call"))
310 .icon_size(IconSize::Small)
311 .on_click(move |_, _window, cx| {
312 ActiveCall::global(cx)
313 .update(cx, |call, cx| call.hang_up(cx))
314 .detach_and_log_err(cx);
315 }),
316 )
317 .into_any_element(),
318 ),
319 ),
320 )
321 .into_any_element()
322 }
323}
324
325pub fn init(cx: &App) {
326 cx.observe_new(|workspace: &mut Workspace, _, cx| {
327 let dock = workspace.dock_at_position(workspace::dock::DockPosition::Left);
328 let handle = cx.weak_entity();
329 let project = workspace.project().clone();
330 dock.update(cx, |dock, cx| {
331 let overlay = cx.new(|cx| {
332 let active_call = ActiveCall::global(cx);
333 cx.observe(&active_call, |_, _, cx| cx.notify()).detach();
334 let channel_store = ChannelStore::global(cx);
335 CallOverlay {
336 channel_store,
337 active_call,
338 workspace: handle,
339 project,
340 screen_share_popover_handle: PopoverMenuHandle::default(),
341 }
342 });
343 dock.add_overlay(
344 cx,
345 Box::new(move |window, cx| {
346 overlay.update(cx, |overlay, cx| {
347 overlay.render(window, cx).into_any_element()
348 })
349 }),
350 )
351 });
352 })
353 .detach();
354}