1use gpui::{div, prelude::*, px, rgb, Context, Window};
4
5use crate::app::{Phase, SpeedApp};
6use crate::theme::*;
7
8const CHART_HEIGHT: f32 = 170.0;
9
10impl Render for SpeedApp {
11 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
12 div()
13 .flex()
14 .flex_col()
15 .size_full()
16 .bg(rgb(BG))
17 .text_color(rgb(TEXT))
18 .font_family("monospace")
19 .p_5()
20 .gap_4()
21 .child(self.header(cx))
22 .child(self.status_bar())
23 .child(
24 div()
25 .flex()
26 .flex_row()
27 .gap_4()
28 .w_full()
29 .child(chart_panel(
30 "⬇ DOWNLOAD",
31 self.download_mbps,
32 self.peak_download,
33 &self.download_samples,
34 download_color(),
35 self.phase == Phase::Download,
36 ))
37 .child(chart_panel(
38 "⬆ UPLOAD",
39 self.upload_mbps,
40 self.peak_upload,
41 &self.upload_samples,
42 upload_color(),
43 self.phase == Phase::Upload,
44 )),
45 )
46 .child(self.summary())
47 }
48}
49
50impl SpeedApp {
51 fn header(&self, cx: &mut Context<Self>) -> impl IntoElement {
52 let running = self.running;
53 let button_label = if running {
54 "RUNNING…"
55 } else {
56 "▶ RUN TEST"
57 };
58
59 div()
60 .flex()
61 .flex_row()
62 .items_center()
63 .justify_between()
64 .w_full()
65 .child(
66 div()
67 .flex()
68 .flex_row()
69 .items_center()
70 .gap_2()
71 .child(div().text_2xl().text_color(rgb(MAGENTA)).child("⟨⟨⟨"))
72 .child(div().text_2xl().text_color(rgb(CYAN)).child("NETRUNNER"))
73 .child(
74 div()
75 .text_sm()
76 .text_color(rgb(MUTED))
77 .child("// SPEED TEST"),
78 ),
79 )
80 .child(
81 div()
82 .id("run")
83 .px_4()
84 .py_2()
85 .rounded_md()
86 .border_1()
87 .border_color(rgb(if running { MUTED } else { GREEN }))
88 .text_color(rgb(if running { MUTED } else { GREEN }))
89 .bg(rgb(PANEL_BG))
90 .cursor_pointer()
91 .hover(|s| s.bg(rgb(PANEL_BORDER)))
92 .on_click(cx.listener(|app, _ev, _window, cx| app.start(cx)))
93 .child(button_label),
94 )
95 }
96
97 fn status_bar(&self) -> impl IntoElement {
98 div()
99 .flex()
100 .flex_row()
101 .items_center()
102 .gap_3()
103 .w_full()
104 .px_3()
105 .py_2()
106 .rounded_md()
107 .bg(rgb(PANEL_BG))
108 .border_1()
109 .border_color(rgb(PANEL_BORDER))
110 .child(
111 div()
112 .text_color(rgb(YELLOW))
113 .child(self.phase.label().to_string()),
114 )
115 .child(div().text_color(rgb(MUTED)).child("·"))
116 .child(div().text_color(rgb(TEXT)).child(self.status.to_string()))
117 }
118
119 fn summary(&self) -> impl IntoElement {
120 let location = self
121 .location
122 .as_ref()
123 .map(|s| s.to_string())
124 .unwrap_or_else(|| "—".to_string());
125 let isp = self
126 .isp
127 .as_ref()
128 .map(|s| s.to_string())
129 .unwrap_or_else(|| "—".to_string());
130 let server = self
131 .server
132 .as_ref()
133 .map(|s| s.to_string())
134 .unwrap_or_else(|| "—".to_string());
135
136 let quality = self
137 .result
138 .as_ref()
139 .map(|r| r.quality.to_string())
140 .unwrap_or_else(|| "—".to_string());
141 let quality_col = self
142 .result
143 .as_ref()
144 .map(|r| quality_color(r.quality))
145 .unwrap_or_else(|| rgb(MUTED));
146
147 div()
148 .flex()
149 .flex_col()
150 .gap_2()
151 .w_full()
152 .p_4()
153 .rounded_md()
154 .bg(rgb(PANEL_BG))
155 .border_1()
156 .border_color(rgb(MAGENTA))
157 .child(
158 div()
159 .flex()
160 .flex_row()
161 .gap_6()
162 .child(metric("Ping", format!("{:.0} ms", self.ping_ms), rgb(BLUE)))
163 .child(metric(
164 "Download",
165 format!("{:.1} Mbps", self.download_mbps),
166 rgb(GREEN),
167 ))
168 .child(metric(
169 "Upload",
170 format!("{:.1} Mbps", self.upload_mbps),
171 rgb(CYAN),
172 ))
173 .child(metric("Quality", quality, quality_col)),
174 )
175 .child(
176 div()
177 .flex()
178 .flex_row()
179 .gap_6()
180 .text_sm()
181 .child(kv("Location", location))
182 .child(kv("ISP", isp))
183 .child(kv("Server", server)),
184 )
185 }
186}
187
188fn chart_panel(
190 title: &str,
191 current: f32,
192 peak: f32,
193 samples: &[f32],
194 color: gpui::Rgba,
195 active: bool,
196) -> impl IntoElement {
197 let max = samples
198 .iter()
199 .copied()
200 .fold(1.0_f32, f32::max)
201 .max(peak)
202 .max(1.0);
203
204 let border = if active { color } else { rgb(PANEL_BORDER) };
205
206 let bars = samples.iter().map(move |&s| {
207 let h = (s / max * CHART_HEIGHT).clamp(2.0, CHART_HEIGHT);
208 div().w(px(5.)).h(px(h)).bg(color).rounded_t_sm()
209 });
210
211 div()
212 .flex()
213 .flex_col()
214 .flex_1()
215 .gap_2()
216 .p_4()
217 .rounded_md()
218 .bg(rgb(PANEL_BG))
219 .border_1()
220 .border_color(border)
221 .child(
222 div()
223 .flex()
224 .flex_row()
225 .items_center()
226 .justify_between()
227 .child(div().text_color(color).child(title.to_string()))
228 .child(
229 div()
230 .text_xs()
231 .text_color(rgb(MUTED))
232 .child(format!("peak {:.1}", peak)),
233 ),
234 )
235 .child(
236 div()
237 .text_2xl()
238 .text_color(rgb(TEXT))
239 .child(format!("{:.1} Mbps", current)),
240 )
241 .child(
242 div()
243 .flex()
244 .flex_row()
245 .items_end()
246 .gap(px(1.))
247 .h(px(CHART_HEIGHT))
248 .w_full()
249 .overflow_hidden()
250 .children(bars),
251 )
252}
253
254fn metric(label: &str, value: String, color: gpui::Rgba) -> impl IntoElement {
255 div()
256 .flex()
257 .flex_col()
258 .gap_1()
259 .child(
260 div()
261 .text_xs()
262 .text_color(rgb(MUTED))
263 .child(label.to_string()),
264 )
265 .child(div().text_xl().text_color(color).child(value))
266}
267
268fn kv(label: &str, value: String) -> impl IntoElement {
269 div()
270 .flex()
271 .flex_row()
272 .gap_2()
273 .child(div().text_color(rgb(MUTED)).child(format!("{label}:")))
274 .child(div().text_color(rgb(TEXT)).child(value))
275}