1use call::{ActiveCall, Room, room};
2use gpui::{
3 DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, Render, Subscription,
4 Window,
5};
6use livekit_client::ConnectionQuality;
7use ui::prelude::*;
8use workspace::{ModalView, Workspace};
9use zed_actions::ShowCallStats;
10
11pub fn init(cx: &mut App) {
12 cx.observe_new(|workspace: &mut Workspace, _, _cx| {
13 workspace.register_action(|workspace, _: &ShowCallStats, window, cx| {
14 workspace.toggle_modal(window, cx, |_window, cx| CallStatsModal::new(cx));
15 });
16 })
17 .detach();
18}
19
20pub struct CallStatsModal {
21 focus_handle: FocusHandle,
22 _active_call_subscription: Option<Subscription>,
23 _diagnostics_subscription: Option<Subscription>,
24}
25
26impl CallStatsModal {
27 fn new(cx: &mut Context<Self>) -> Self {
28 let mut this = Self {
29 focus_handle: cx.focus_handle(),
30 _active_call_subscription: None,
31 _diagnostics_subscription: None,
32 };
33
34 if let Some(active_call) = ActiveCall::try_global(cx) {
35 this._active_call_subscription =
36 Some(cx.subscribe(&active_call, Self::handle_call_event));
37 this.observe_diagnostics(cx);
38 }
39
40 this
41 }
42
43 fn observe_diagnostics(&mut self, cx: &mut Context<Self>) {
44 let diagnostics = active_room(cx).and_then(|room| room.read(cx).diagnostics().cloned());
45
46 if let Some(diagnostics) = diagnostics {
47 self._diagnostics_subscription = Some(cx.observe(&diagnostics, |_, _, cx| cx.notify()));
48 } else {
49 self._diagnostics_subscription = None;
50 }
51 }
52
53 fn handle_call_event(
54 &mut self,
55 _: Entity<ActiveCall>,
56 event: &room::Event,
57 cx: &mut Context<Self>,
58 ) {
59 match event {
60 room::Event::RoomJoined { .. } => {
61 self.observe_diagnostics(cx);
62 }
63 room::Event::RoomLeft { .. } => {
64 self._diagnostics_subscription = None;
65 cx.notify();
66 }
67 _ => {}
68 }
69 }
70
71 fn dismiss(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
72 cx.emit(DismissEvent);
73 }
74}
75
76fn active_room(cx: &App) -> Option<Entity<Room>> {
77 ActiveCall::try_global(cx)?.read(cx).room().cloned()
78}
79
80fn quality_label(quality: Option<ConnectionQuality>) -> (&'static str, Color) {
81 match quality {
82 Some(ConnectionQuality::Excellent) => ("Excellent", Color::Success),
83 Some(ConnectionQuality::Good) => ("Good", Color::Success),
84 Some(ConnectionQuality::Poor) => ("Poor", Color::Warning),
85 Some(ConnectionQuality::Lost) => ("Lost", Color::Error),
86 None => ("—", Color::Muted),
87 }
88}
89
90fn metric_rating(label: &str, value_ms: f64) -> (&'static str, Color) {
91 match label {
92 "Latency" => {
93 if value_ms < 100.0 {
94 ("Normal", Color::Success)
95 } else if value_ms < 300.0 {
96 ("High", Color::Warning)
97 } else {
98 ("Poor", Color::Error)
99 }
100 }
101 "Jitter" => {
102 if value_ms < 30.0 {
103 ("Normal", Color::Success)
104 } else if value_ms < 75.0 {
105 ("High", Color::Warning)
106 } else {
107 ("Poor", Color::Error)
108 }
109 }
110 _ => ("Normal", Color::Success),
111 }
112}
113
114fn input_lag_rating(value_ms: f64) -> (&'static str, Color) {
115 if value_ms < 20.0 {
116 ("Normal", Color::Success)
117 } else if value_ms < 50.0 {
118 ("High", Color::Warning)
119 } else {
120 ("Poor", Color::Error)
121 }
122}
123
124fn packet_loss_rating(loss_pct: f64) -> (&'static str, Color) {
125 if loss_pct < 1.0 {
126 ("Normal", Color::Success)
127 } else if loss_pct < 5.0 {
128 ("High", Color::Warning)
129 } else {
130 ("Poor", Color::Error)
131 }
132}
133
134impl EventEmitter<DismissEvent> for CallStatsModal {}
135impl ModalView for CallStatsModal {}
136
137impl Focusable for CallStatsModal {
138 fn focus_handle(&self, _cx: &App) -> FocusHandle {
139 self.focus_handle.clone()
140 }
141}
142
143impl Render for CallStatsModal {
144 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
145 let room = active_room(cx);
146 let is_connected = room.is_some();
147 let stats = room
148 .and_then(|room| {
149 let diagnostics = room.read(cx).diagnostics()?;
150 Some(diagnostics.read(cx).stats().clone())
151 })
152 .unwrap_or_default();
153
154 let (quality_text, quality_color) = quality_label(stats.connection_quality);
155
156 v_flex()
157 .key_context("CallStatsModal")
158 .on_action(cx.listener(Self::dismiss))
159 .track_focus(&self.focus_handle)
160 .elevation_3(cx)
161 .w(rems(24.))
162 .p_4()
163 .gap_3()
164 .child(
165 h_flex()
166 .justify_between()
167 .child(Label::new("Call Diagnostics").size(LabelSize::Large))
168 .child(
169 Label::new(quality_text)
170 .size(LabelSize::Large)
171 .color(quality_color),
172 ),
173 )
174 .when(!is_connected, |this| {
175 this.child(
176 h_flex()
177 .justify_center()
178 .py_4()
179 .child(Label::new("Not in a call").color(Color::Muted)),
180 )
181 })
182 .when(is_connected, |this| {
183 this.child(
184 v_flex()
185 .gap_1()
186 .child(
187 h_flex()
188 .gap_2()
189 .child(Label::new("Network").weight(FontWeight::SEMIBOLD)),
190 )
191 .child(self.render_metric_row(
192 "Latency",
193 "Time for data to travel to the server",
194 stats.latency_ms,
195 |v| format!("{:.0}ms", v),
196 |v| metric_rating("Latency", v),
197 ))
198 .child(self.render_metric_row(
199 "Jitter",
200 "Variance or fluctuation in latency",
201 stats.jitter_ms,
202 |v| format!("{:.0}ms", v),
203 |v| metric_rating("Jitter", v),
204 ))
205 .child(self.render_metric_row(
206 "Packet loss",
207 "Amount of data lost during transfer",
208 stats.packet_loss_pct,
209 |v| format!("{:.1}%", v),
210 |v| packet_loss_rating(v),
211 ))
212 .child(self.render_metric_row(
213 "Input lag",
214 "Delay from audio capture to WebRTC",
215 stats.input_lag.map(|d| d.as_secs_f64() * 1000.0),
216 |v| format!("{:.1}ms", v),
217 |v| input_lag_rating(v),
218 )),
219 )
220 })
221 }
222}
223
224impl CallStatsModal {
225 fn render_metric_row(
226 &self,
227 title: &str,
228 description: &str,
229 value: Option<f64>,
230 format_value: impl Fn(f64) -> String,
231 rate: impl Fn(f64) -> (&'static str, Color),
232 ) -> impl IntoElement {
233 let (rating_text, rating_color, value_text) = match value {
234 Some(v) => {
235 let (rt, rc) = rate(v);
236 (rt, rc, format_value(v))
237 }
238 None => ("—", Color::Muted, "—".to_string()),
239 };
240
241 h_flex()
242 .px_2()
243 .py_1()
244 .rounded_md()
245 .justify_between()
246 .child(
247 v_flex()
248 .child(Label::new(title.to_string()).size(LabelSize::Default))
249 .child(
250 Label::new(description.to_string())
251 .size(LabelSize::Small)
252 .color(Color::Muted),
253 ),
254 )
255 .child(
256 v_flex()
257 .items_end()
258 .child(
259 Label::new(rating_text)
260 .size(LabelSize::Default)
261 .color(rating_color),
262 )
263 .child(
264 Label::new(value_text)
265 .size(LabelSize::Small)
266 .color(Color::Muted),
267 ),
268 )
269 }
270}