call_overlay.rs

  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}