Skip to main content

netrunner_gui/
view.rs

1//! GPUI rendering for the netrunner desktop app.
2
3use 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
188/// A single labelled live throughput chart.
189fn 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}