Files
Ely-GPUI-Components/src/messaging/incoming.rs
T
ZacharyZhang-NY dd465fedb7 Messaging · Calls: controls, tiles, a grid, a shared screen, a bar, a ringing dialog, a huddle
CallControls holds the microphone, camera and a shared screen as
toggles, then Leave. ParticipantTile shows a camera's frame from the
host or the person's initials, their name with a mark when muted, and a
ring while they speak. VideoCallGrid lays tiles in even rows as far as
they fit across its measured width; a short last row centers at the
full rows' width. ScreenShareView names who presents and offers Stop
presenting for your own share, the call's tiles beside or below.
VoiceCallBar tells where a call is, how long it has run and who is in
it, with the microphone and Leave. IncomingCallDialog rings with the
caller's picture; Accept or Decline answers each ring once, and Escape
declines. HuddleIndicator rings while a huddle is live; a press joins.

The recorder's device toggle and the camera's name chip now have one
home in media. People: member names sort whatever their case, a
status's words save trimmed, and a profile card's local time ticks.
2026-09-26 22:33:54 -04:00

113 lines
3.6 KiB
Rust

use std::{cell::Cell, rc::Rc};
use gpui::{
App, ElementId, ImageSource, InteractiveElement, IntoElement, ParentElement, RenderOnce,
SharedString, Styled, Window, div,
};
use crate::{
buttons::{Button, ButtonVariant},
data_display::Avatar,
forms::Run,
motion::Pulse,
overlays::Dialog,
primitives::IconName,
theme::AvatarSize,
};
/// A call coming in: the caller's picture ringing, their name and the kind of call, with Decline and Accept; one of the two answers each ring. Escape declines, a press on the scrim does not. Render it while the call rings.
#[derive(IntoElement)]
pub struct IncomingCallDialog {
id: ElementId,
caller: SharedString,
picture: Option<ImageSource>,
video: bool,
on_accept: Run,
on_decline: Run,
}
impl IncomingCallDialog {
pub fn new(
id: impl Into<ElementId>,
caller: impl Into<SharedString>,
on_accept: impl Fn(&mut Window, &mut App) + 'static,
on_decline: impl Fn(&mut Window, &mut App) + 'static,
) -> Self {
Self {
id: id.into(),
caller: caller.into(),
picture: None,
video: false,
on_accept: Rc::new(on_accept),
on_decline: Rc::new(on_decline),
}
}
pub fn picture(mut self, source: impl Into<ImageSource>) -> Self {
self.picture = Some(source.into());
self
}
/// A video call; a voice call otherwise.
pub fn video(mut self) -> Self {
self.video = true;
self
}
}
impl RenderOnce for IncomingCallDialog {
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
let avatar =
Avatar::new((self.id.clone(), "avatar"), self.caller.clone()).size(AvatarSize::Xl);
let avatar = match self.picture {
Some(picture) => avatar.image(picture),
None => avatar,
};
let (kind, icon) = if self.video {
("Incoming video call", IconName::Video)
} else {
("Incoming voice call", IconName::Phone)
};
let (id, caller) = (self.id.clone(), self.caller.clone());
let (decline, accept) = (self.on_decline, self.on_accept);
let told = caller.clone();
let (decline_id, accept_id) = (id.clone(), id.clone());
// Every way out runs this close; only Accept answers first.
let answered = Rc::new(Cell::new(false));
let answer = answered.clone();
Dialog::new(self.id, self.caller, move |window, cx| {
if !answered.get() {
log::info!("incoming call {caller}: declined");
decline(window, cx)
}
})
.detail(kind)
.held()
.child(
div()
.debug_selector(|| "incoming-call".into())
.flex()
.justify_center()
.py_4()
.child(Pulse::new((id, "ring")).rounded_full().child(avatar)),
)
.action(move |close| {
Button::new((decline_id, "decline"), "Decline")
.icon(IconName::PhoneOff)
.variant(ButtonVariant::Danger)
.on_click(move |_, window, cx| close(window, cx))
})
.action(move |close| {
Button::new((accept_id, "accept"), "Accept")
.icon(icon)
.variant(ButtonVariant::Success)
.on_click(move |_, window, cx| {
log::info!("incoming call {told}: accepted");
answer.set(true);
close(window, cx);
accept(window, cx)
})
})
}
}