use std::rc::Rc; use gpui::{ App, ElementId, IntoElement, MouseButton, ParentElement, RenderOnce, SharedString, Styled, Window, div, prelude::*, }; use web_time::Instant; use crate::{ buttons::{Button, ButtonVariant}, data_display::{Avatar, AvatarGroup}, feedback::Timer, forms::{OnFlag, Run}, media::device_toggle, motion::Pulse, primitives::{FocusRing, Icon, IconName}, theme::{ActiveTheme, AvatarSize, ControlSize, IconSize, Radius, TextSize}, typography::Ellipsis, }; fn leave(id: &ElementId, owner: &'static str, run: Run) -> Button { Button::new((id.clone(), "leave"), "Leave") .icon(IconName::PhoneOff) .variant(ButtonVariant::Danger) .size(ControlSize::Sm) .on_click(move |_, window, cx| { log::info!("{owner}: leave"); run(window, cx) }) } /// A call's controls in one pill: the microphone, the camera and a shared screen as toggles, then Leave, each only with its handler. #[derive(IntoElement)] pub struct CallControls { id: ElementId, mic: Option<(bool, OnFlag)>, camera: Option<(bool, OnFlag)>, share: Option<(bool, OnFlag)>, on_leave: Option, } impl CallControls { pub fn new(id: impl Into) -> Self { Self { id: id.into(), mic: None, camera: None, share: None, on_leave: None, } } /// Whether the microphone is on, and what gets the change. pub fn mic( mut self, on: bool, handler: impl Fn(bool, &mut Window, &mut App) + 'static, ) -> Self { self.mic = Some((on, Rc::new(handler))); self } /// Whether the camera is on, and what gets the change. pub fn camera( mut self, on: bool, handler: impl Fn(bool, &mut Window, &mut App) + 'static, ) -> Self { self.camera = Some((on, Rc::new(handler))); self } /// Whether the screen is shared, and what gets the change. pub fn share( mut self, on: bool, handler: impl Fn(bool, &mut Window, &mut App) + 'static, ) -> Self { self.share = Some((on, Rc::new(handler))); self } pub fn on_leave(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self { self.on_leave = Some(Rc::new(handler)); self } } impl RenderOnce for CallControls { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { let theme = cx.theme(); let colors = &theme.colors; let id = self.id.clone(); let toggle = |key, icons, tip, set: Option<(bool, OnFlag)>| { set.map(|set| device_toggle("call controls", &id, key, icons, tip, set)) }; let toggles = [ toggle( "mic", (IconName::Mic, IconName::MicOff), "Microphone", self.mic, ), toggle( "camera", (IconName::Video, IconName::VideoOff), "Camera", self.camera, ), toggle( "share", (IconName::ScreenShare, IconName::ScreenShareOff), "Share screen", self.share, ), ]; let some = toggles.iter().any(Option::is_some); let leave = self.on_leave.map(|run| leave(&id, "call controls", run)); div() .flex_none() .flex() .items_center() .gap_1() .p_1() .rounded_full() .border_1() .border_color(colors.border) .bg(colors.surface) .children(toggles.into_iter().flatten()) .when(some && leave.is_some(), |pill| { pill.child( div() .w_px() .h(theme.icon_size(IconSize::Md)) .bg(colors.border), ) }) .children(leave) } } /// A call under way in one bar: a live dot, where it is, how long it has run, who is in it, the microphone and Leave. The bar wraps by the header rule. #[derive(IntoElement)] pub struct VoiceCallBar { id: ElementId, place: SharedString, since: Instant, people: Vec, mic: Option<(bool, OnFlag)>, on_leave: Option, } impl VoiceCallBar { pub fn new(id: impl Into, place: impl Into, since: Instant) -> Self { Self { id: id.into(), place: place.into(), since, people: Vec::new(), mic: None, on_leave: None, } } /// Who is in the call; the first three show. pub fn people(mut self, people: impl IntoIterator) -> Self { self.people = people.into_iter().collect(); self } /// Whether the microphone is on, and what gets the change. pub fn mic( mut self, on: bool, handler: impl Fn(bool, &mut Window, &mut App) + 'static, ) -> Self { self.mic = Some((on, Rc::new(handler))); self } pub fn on_leave(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self { self.on_leave = Some(Rc::new(handler)); self } } impl RenderOnce for VoiceCallBar { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { let theme = cx.theme(); let colors = &theme.colors; let id = self.id.clone(); let dot = div() .size(theme.status_dot()) .rounded_full() .bg(colors.success); let mic = self.mic.map(|set| { device_toggle( "voice call", &id, "mic", (IconName::Mic, IconName::MicOff), "Microphone", set, ) }); let people = (!self.people.is_empty()) .then(|| AvatarGroup::new(self.people).max(3).size(AvatarSize::Xs)); div() .debug_selector(|| "voice-call-bar".into()) .flex() .flex_wrap() .items_center() .justify_between() .gap_2() .px_3() .py_2() .rounded(theme.radius(Radius::Lg)) .border_1() .border_color(colors.border) .bg(colors.surface) .text_size(theme.text_size(TextSize::Sm)) .child( div() .flex_1() .min_w(theme.label_width()) .flex() .items_center() .gap_2() .child( div() .flex_none() .child(Pulse::new((id.clone(), "live")).child(dot)), ) .child( div() .flex_1() .min_w_0() .text_color(colors.fg) .child(Ellipsis::new(self.place)), ) .child( div() .flex_none() .text_color(colors.fg_muted) .child(Timer::new((id.clone(), "time"), self.since).size(TextSize::Sm)), ), ) .child( div() .flex_none() .flex() .items_center() .gap_2() .children(people) .children(mic) .children(self.on_leave.map(|run| leave(&id, "voice call", run))), ) } } /// A huddle going on in a chat: headphones, how many are in, and their faces. With a handler a press joins it; its mark rings while live. #[derive(IntoElement)] pub struct HuddleIndicator { id: ElementId, people: Vec, on_join: Option, } impl HuddleIndicator { pub fn new(id: impl Into, people: impl IntoIterator) -> Self { let people: Vec = people.into_iter().collect(); assert!(!people.is_empty(), "a huddle holds someone"); Self { id: id.into(), people, on_join: None, } } pub fn on_join(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self { self.on_join = Some(Rc::new(handler)); self } } impl RenderOnce for HuddleIndicator { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { let theme = cx.theme(); let colors = &theme.colors; let count = self.people.len(); let (id, ring) = (self.id.clone(), (self.id.clone(), "ring")); div() .id(self.id) .debug_selector(|| "huddle".into()) .flex_none() .flex() .items_center() .gap_1p5() .pl_2() .pr_1() .py_0p5() .rounded_full() .border_1() .border_color(colors.success.alpha(0.4)) .bg(colors.success_subtle) .text_size(theme.text_size(TextSize::Sm)) .text_color(colors.success) .when_some(self.on_join, |chip, join| { chip.cursor_pointer() .tab_index(0) .focus_ring(cx) .on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default()) .on_click(move |_, window, cx| { log::info!("huddle {id}: join"); join(window, cx) }) }) .child( Pulse::new(ring).child( Icon::new(IconName::Headphones) .size(IconSize::Sm) .color(colors.success), ), ) .child(count.to_string()) .child(AvatarGroup::new(self.people).max(3).size(AvatarSize::Xs)) } }