329 lines
10 KiB
Rust
329 lines
10 KiB
Rust
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<Run>,
|
|
}
|
|
|
|
impl CallControls {
|
|
pub fn new(id: impl Into<ElementId>) -> 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<Avatar>,
|
|
mic: Option<(bool, OnFlag)>,
|
|
on_leave: Option<Run>,
|
|
}
|
|
|
|
impl VoiceCallBar {
|
|
pub fn new(id: impl Into<ElementId>, place: impl Into<SharedString>, 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<Item = Avatar>) -> 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<Avatar>,
|
|
on_join: Option<Run>,
|
|
}
|
|
|
|
impl HuddleIndicator {
|
|
pub fn new(id: impl Into<ElementId>, people: impl IntoIterator<Item = Avatar>) -> Self {
|
|
let people: Vec<Avatar> = 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))
|
|
}
|
|
}
|