main.rs

  1use gpui::prelude::*;
  2use gpui::{
  3    App, Bounds, Context, ElementId, SharedString, Task, Window, WindowBounds, WindowOptions, div,
  4    px, rgb, size,
  5};
  6
  7// ---------------------------------------------------------------------------
  8// Prime counting (intentionally brute-force so it hammers the CPU)
  9// ---------------------------------------------------------------------------
 10
 11fn is_prime(n: u64) -> bool {
 12    if n < 2 {
 13        return false;
 14    }
 15    if n < 4 {
 16        return true;
 17    }
 18    if n % 2 == 0 || n % 3 == 0 {
 19        return false;
 20    }
 21    let mut i = 5;
 22    while i * i <= n {
 23        if n % i == 0 || n % (i + 2) == 0 {
 24            return false;
 25        }
 26        i += 6;
 27    }
 28    true
 29}
 30
 31fn count_primes_in_range(start: u64, end: u64) -> u64 {
 32    let mut count = 0;
 33    for n in start..end {
 34        if is_prime(n) {
 35            count += 1;
 36        }
 37    }
 38    count
 39}
 40
 41// ---------------------------------------------------------------------------
 42// App state
 43// ---------------------------------------------------------------------------
 44
 45const NUM_CHUNKS: u64 = 12;
 46
 47#[derive(Clone, Copy, PartialEq, Eq)]
 48enum Preset {
 49    TenMillion,
 50    FiftyMillion,
 51    HundredMillion,
 52}
 53
 54impl Preset {
 55    fn label(self) -> &'static str {
 56        match self {
 57            Preset::TenMillion => "10 M",
 58            Preset::FiftyMillion => "50 M",
 59            Preset::HundredMillion => "100 M",
 60        }
 61    }
 62
 63    fn value(self) -> u64 {
 64        match self {
 65            Preset::TenMillion => 10_000_000,
 66            Preset::FiftyMillion => 50_000_000,
 67            Preset::HundredMillion => 100_000_000,
 68        }
 69    }
 70
 71    const ALL: [Preset; 3] = [
 72        Preset::TenMillion,
 73        Preset::FiftyMillion,
 74        Preset::HundredMillion,
 75    ];
 76}
 77
 78struct ChunkResult {
 79    count: u64,
 80}
 81
 82struct Run {
 83    limit: u64,
 84    chunks_done: u64,
 85    chunk_results: Vec<ChunkResult>,
 86    total: Option<u64>,
 87    elapsed: Option<f64>,
 88}
 89
 90struct HelloWeb {
 91    selected_preset: Preset,
 92    current_run: Option<Run>,
 93    history: Vec<SharedString>,
 94    _tasks: Vec<Task<()>>,
 95}
 96
 97impl HelloWeb {
 98    fn new(_cx: &mut Context<Self>) -> Self {
 99        Self {
100            selected_preset: Preset::TenMillion,
101            current_run: None,
102            history: Vec::new(),
103            _tasks: Vec::new(),
104        }
105    }
106
107    fn start_search(&mut self, cx: &mut Context<Self>) {
108        let limit = self.selected_preset.value();
109        let chunk_size = limit / NUM_CHUNKS;
110
111        self.current_run = Some(Run {
112            limit,
113            chunks_done: 0,
114            chunk_results: Vec::new(),
115            total: None,
116            elapsed: None,
117        });
118        self._tasks.clear();
119        cx.notify();
120
121        let start_time = web_time::Instant::now();
122
123        for i in 0..NUM_CHUNKS {
124            let range_start = i * chunk_size;
125            let range_end = if i == NUM_CHUNKS - 1 {
126                limit
127            } else {
128                range_start + chunk_size
129            };
130
131            let task = cx.spawn(async move |this, cx| {
132                let count = cx
133                    .background_spawn(async move { count_primes_in_range(range_start, range_end) })
134                    .await;
135
136                this.update(cx, |this, cx| {
137                    if let Some(run) = &mut this.current_run {
138                        run.chunk_results.push(ChunkResult { count });
139                        run.chunks_done += 1;
140
141                        if run.chunks_done == NUM_CHUNKS {
142                            let total: u64 = run.chunk_results.iter().map(|r| r.count).sum();
143                            let elapsed_ms = start_time.elapsed().as_secs_f64() * 1000.0;
144                            run.total = Some(total);
145                            run.elapsed = Some(elapsed_ms);
146                            this.history.push(
147                                format!(
148                                    "π({}) = {} ({:.0} ms, {} chunks)",
149                                    format_number(run.limit),
150                                    format_number(total),
151                                    elapsed_ms,
152                                    NUM_CHUNKS,
153                                )
154                                .into(),
155                            );
156                        }
157                        cx.notify();
158                    }
159                })
160                .ok();
161            });
162
163            self._tasks.push(task);
164        }
165    }
166}
167
168fn format_number(n: u64) -> String {
169    let s = n.to_string();
170    let mut result = String::new();
171    for (i, ch) in s.chars().rev().enumerate() {
172        if i > 0 && i % 3 == 0 {
173            result.push(',');
174        }
175        result.push(ch);
176    }
177    result.chars().rev().collect()
178}
179
180// ---------------------------------------------------------------------------
181// Render
182// ---------------------------------------------------------------------------
183
184const BG_BASE: u32 = 0x1e1e2e;
185const BG_SURFACE: u32 = 0x313244;
186const BG_OVERLAY: u32 = 0x45475a;
187const TEXT_PRIMARY: u32 = 0xcdd6f4;
188const TEXT_SECONDARY: u32 = 0xa6adc8;
189const TEXT_DIM: u32 = 0x6c7086;
190const ACCENT_YELLOW: u32 = 0xf9e2af;
191const ACCENT_GREEN: u32 = 0xa6e3a1;
192const ACCENT_BLUE: u32 = 0x89b4fa;
193const ACCENT_MAUVE: u32 = 0xcba6f7;
194
195impl Render for HelloWeb {
196    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
197        let is_running = self.current_run.as_ref().is_some_and(|r| r.total.is_none());
198
199        // -- Preset buttons --
200        let preset_row = Preset::ALL.iter().enumerate().fold(
201            div().flex().flex_row().gap_2(),
202            |row, (index, &preset)| {
203                let is_selected = preset == self.selected_preset;
204                let (bg, text_color) = if is_selected {
205                    (ACCENT_BLUE, BG_BASE)
206                } else {
207                    (BG_OVERLAY, TEXT_SECONDARY)
208                };
209                row.child(
210                    div()
211                        .id(ElementId::NamedInteger("preset".into(), index as u64))
212                        .px_3()
213                        .py_1()
214                        .rounded_md()
215                        .bg(rgb(bg))
216                        .text_color(rgb(text_color))
217                        .text_sm()
218                        .cursor_pointer()
219                        .when(!is_running, |this| {
220                            this.on_click(cx.listener(move |this, _event, _window, _cx| {
221                                this.selected_preset = preset;
222                            }))
223                        })
224                        .child(preset.label()),
225                )
226            },
227        );
228
229        // -- Go button --
230        let (go_bg, go_text, go_label) = if is_running {
231            (BG_OVERLAY, TEXT_DIM, "Running…")
232        } else {
233            (ACCENT_GREEN, BG_BASE, "Count Primes")
234        };
235        let go_button = div()
236            .id("go")
237            .px_4()
238            .py(px(6.))
239            .rounded_md()
240            .bg(rgb(go_bg))
241            .text_color(rgb(go_text))
242            .cursor_pointer()
243            .when(!is_running, |this| {
244                this.on_click(cx.listener(|this, _event, _window, cx| {
245                    this.start_search(cx);
246                }))
247            })
248            .child(go_label);
249
250        // -- Progress / result area --
251        let status_area = if let Some(run) = &self.current_run {
252            let progress_fraction = run.chunks_done as f32 / NUM_CHUNKS as f32;
253            let progress_pct = (progress_fraction * 100.0) as u32;
254
255            let status_text: SharedString = if let Some(total) = run.total {
256                format!(
257                    "Found {} primes below {} in {:.0} ms",
258                    format_number(total),
259                    format_number(run.limit),
260                    run.elapsed.unwrap_or(0.0),
261                )
262                .into()
263            } else {
264                format!(
265                    "Searching up to {}{}/{} chunks  ({}%)",
266                    format_number(run.limit),
267                    run.chunks_done,
268                    NUM_CHUNKS,
269                    progress_pct,
270                )
271                .into()
272            };
273
274            let bar_color = if run.total.is_some() {
275                ACCENT_GREEN
276            } else {
277                ACCENT_BLUE
278            };
279
280            let chunk_dots =
281                (0..NUM_CHUNKS as usize).fold(div().flex().flex_row().gap_1().mt_2(), |row, i| {
282                    let done = i < run.chunks_done as usize;
283                    let color = if done { ACCENT_MAUVE } else { BG_OVERLAY };
284                    row.child(div().size(px(10.)).rounded_sm().bg(rgb(color)))
285                });
286
287            div()
288                .flex()
289                .flex_col()
290                .w_full()
291                .gap_2()
292                .child(div().text_color(rgb(TEXT_PRIMARY)).child(status_text))
293                .child(
294                    div()
295                        .w_full()
296                        .h(px(8.))
297                        .rounded_sm()
298                        .bg(rgb(BG_OVERLAY))
299                        .child(
300                            div()
301                                .h_full()
302                                .rounded_sm()
303                                .bg(rgb(bar_color))
304                                .w(gpui::relative(progress_fraction)),
305                        ),
306                )
307                .child(chunk_dots)
308        } else {
309            div().flex().flex_col().w_full().child(
310                div()
311                    .text_color(rgb(TEXT_DIM))
312                    .child("Select a range and press Count Primes to begin."),
313            )
314        };
315
316        // -- History log --
317        let history_section = if self.history.is_empty() {
318            div()
319        } else {
320            self.history
321                .iter()
322                .rev()
323                .fold(div().flex().flex_col().gap_1(), |col, entry| {
324                    col.child(
325                        div()
326                            .text_sm()
327                            .text_color(rgb(TEXT_SECONDARY))
328                            .child(entry.clone()),
329                    )
330                })
331        };
332
333        // -- Layout --
334        div()
335            .flex()
336            .flex_col()
337            .size_full()
338            .bg(rgb(BG_BASE))
339            .justify_center()
340            .items_center()
341            .gap_4()
342            .p_4()
343            // Title
344            .child(
345                div()
346                    .text_xl()
347                    .text_color(rgb(TEXT_PRIMARY))
348                    .child("Prime Sieve — GPUI Web"),
349            )
350            .child(div().text_sm().text_color(rgb(TEXT_DIM)).child(format!(
351                "Background threads: {} · Chunks per run: {}",
352                std::thread::available_parallelism().map_or(2, |n| n.get().max(2)),
353                NUM_CHUNKS,
354            )))
355            // Controls
356            .child(
357                div()
358                    .flex()
359                    .flex_col()
360                    .items_center()
361                    .gap_3()
362                    .p_4()
363                    .w(px(500.))
364                    .rounded_lg()
365                    .bg(rgb(BG_SURFACE))
366                    .child(
367                        div()
368                            .text_sm()
369                            .text_color(rgb(ACCENT_YELLOW))
370                            .child("Count primes below:"),
371                    )
372                    .child(preset_row)
373                    .child(go_button),
374            )
375            // Status
376            .child(
377                div()
378                    .flex()
379                    .flex_col()
380                    .w(px(500.))
381                    .p_4()
382                    .rounded_lg()
383                    .bg(rgb(BG_SURFACE))
384                    .child(status_area),
385            )
386            // History
387            .when(!self.history.is_empty(), |this| {
388                this.child(
389                    div()
390                        .flex()
391                        .flex_col()
392                        .w(px(500.))
393                        .p_4()
394                        .rounded_lg()
395                        .bg(rgb(BG_SURFACE))
396                        .gap_2()
397                        .child(div().text_sm().text_color(rgb(TEXT_DIM)).child("History"))
398                        .child(history_section),
399                )
400            })
401    }
402}
403
404// ---------------------------------------------------------------------------
405// Entry point
406// ---------------------------------------------------------------------------
407
408fn main() {
409    gpui_platform::web_init();
410    gpui_platform::application().run(|cx: &mut App| {
411        let bounds = Bounds::centered(None, size(px(640.), px(560.)), cx);
412        cx.open_window(
413            WindowOptions {
414                window_bounds: Some(WindowBounds::Windowed(bounds)),
415                ..Default::default()
416            },
417            |_, cx| cx.new(HelloWeb::new),
418        )
419        .expect("failed to open window");
420        cx.activate(true);
421    });
422}