call_stats_modal.rs

  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}