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}