logiguard fork v3: full patch set on verified 8c74db0 tree
Includes prior-session patches (carry forward so the app compiles): - crates/gpui/build.rs: cross-compile manifest fix - crates/gpui/src/platform.rs: PlatformWindow::activate_with_token trait method - crates/gpui/src/window.rs: Window::activate_with_token public API - crates/gpui_linux/src/linux/wayland/window.rs: WaylandWindow::activate_with_token + activate() keyboard-serial fix Plus the focus-serial fix: - serial.rs: SerialKind::KeyboardEnter - client.rs: store wl_keyboard.enter serial; latest_serial_of() Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
422
crates/gpui_web/examples/hello_web/main.rs
Normal file
422
crates/gpui_web/examples/hello_web/main.rs
Normal file
@@ -0,0 +1,422 @@
|
||||
use gpui::prelude::*;
|
||||
use gpui::{
|
||||
App, Bounds, Context, ElementId, SharedString, Task, Window, WindowBounds, WindowOptions, div,
|
||||
px, rgb, size,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Prime counting (intentionally brute-force so it hammers the CPU)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn is_prime(n: u64) -> bool {
|
||||
if n < 2 {
|
||||
return false;
|
||||
}
|
||||
if n < 4 {
|
||||
return true;
|
||||
}
|
||||
if n % 2 == 0 || n % 3 == 0 {
|
||||
return false;
|
||||
}
|
||||
let mut i = 5;
|
||||
while i * i <= n {
|
||||
if n % i == 0 || n % (i + 2) == 0 {
|
||||
return false;
|
||||
}
|
||||
i += 6;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn count_primes_in_range(start: u64, end: u64) -> u64 {
|
||||
let mut count = 0;
|
||||
for n in start..end {
|
||||
if is_prime(n) {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
count
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// App state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const NUM_CHUNKS: u64 = 12;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Preset {
|
||||
TenMillion,
|
||||
FiftyMillion,
|
||||
HundredMillion,
|
||||
}
|
||||
|
||||
impl Preset {
|
||||
fn label(self) -> &'static str {
|
||||
match self {
|
||||
Preset::TenMillion => "10 M",
|
||||
Preset::FiftyMillion => "50 M",
|
||||
Preset::HundredMillion => "100 M",
|
||||
}
|
||||
}
|
||||
|
||||
fn value(self) -> u64 {
|
||||
match self {
|
||||
Preset::TenMillion => 10_000_000,
|
||||
Preset::FiftyMillion => 50_000_000,
|
||||
Preset::HundredMillion => 100_000_000,
|
||||
}
|
||||
}
|
||||
|
||||
const ALL: [Preset; 3] = [
|
||||
Preset::TenMillion,
|
||||
Preset::FiftyMillion,
|
||||
Preset::HundredMillion,
|
||||
];
|
||||
}
|
||||
|
||||
struct ChunkResult {
|
||||
count: u64,
|
||||
}
|
||||
|
||||
struct Run {
|
||||
limit: u64,
|
||||
chunks_done: u64,
|
||||
chunk_results: Vec<ChunkResult>,
|
||||
total: Option<u64>,
|
||||
elapsed: Option<f64>,
|
||||
}
|
||||
|
||||
struct HelloWeb {
|
||||
selected_preset: Preset,
|
||||
current_run: Option<Run>,
|
||||
history: Vec<SharedString>,
|
||||
_tasks: Vec<Task<()>>,
|
||||
}
|
||||
|
||||
impl HelloWeb {
|
||||
fn new(_cx: &mut Context<Self>) -> Self {
|
||||
Self {
|
||||
selected_preset: Preset::TenMillion,
|
||||
current_run: None,
|
||||
history: Vec::new(),
|
||||
_tasks: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn start_search(&mut self, cx: &mut Context<Self>) {
|
||||
let limit = self.selected_preset.value();
|
||||
let chunk_size = limit / NUM_CHUNKS;
|
||||
|
||||
self.current_run = Some(Run {
|
||||
limit,
|
||||
chunks_done: 0,
|
||||
chunk_results: Vec::new(),
|
||||
total: None,
|
||||
elapsed: None,
|
||||
});
|
||||
self._tasks.clear();
|
||||
cx.notify();
|
||||
|
||||
let start_time = web_time::Instant::now();
|
||||
|
||||
for i in 0..NUM_CHUNKS {
|
||||
let range_start = i * chunk_size;
|
||||
let range_end = if i == NUM_CHUNKS - 1 {
|
||||
limit
|
||||
} else {
|
||||
range_start + chunk_size
|
||||
};
|
||||
|
||||
let task = cx.spawn(async move |this, cx| {
|
||||
let count = cx
|
||||
.background_spawn(async move { count_primes_in_range(range_start, range_end) })
|
||||
.await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
if let Some(run) = &mut this.current_run {
|
||||
run.chunk_results.push(ChunkResult { count });
|
||||
run.chunks_done += 1;
|
||||
|
||||
if run.chunks_done == NUM_CHUNKS {
|
||||
let total: u64 = run.chunk_results.iter().map(|r| r.count).sum();
|
||||
let elapsed_ms = start_time.elapsed().as_secs_f64() * 1000.0;
|
||||
run.total = Some(total);
|
||||
run.elapsed = Some(elapsed_ms);
|
||||
this.history.push(
|
||||
format!(
|
||||
"π({}) = {} ({:.0} ms, {} chunks)",
|
||||
format_number(run.limit),
|
||||
format_number(total),
|
||||
elapsed_ms,
|
||||
NUM_CHUNKS,
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
|
||||
self._tasks.push(task);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn format_number(n: u64) -> String {
|
||||
let s = n.to_string();
|
||||
let mut result = String::new();
|
||||
for (i, ch) in s.chars().rev().enumerate() {
|
||||
if i > 0 && i % 3 == 0 {
|
||||
result.push(',');
|
||||
}
|
||||
result.push(ch);
|
||||
}
|
||||
result.chars().rev().collect()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Render
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const BG_BASE: u32 = 0x1e1e2e;
|
||||
const BG_SURFACE: u32 = 0x313244;
|
||||
const BG_OVERLAY: u32 = 0x45475a;
|
||||
const TEXT_PRIMARY: u32 = 0xcdd6f4;
|
||||
const TEXT_SECONDARY: u32 = 0xa6adc8;
|
||||
const TEXT_DIM: u32 = 0x6c7086;
|
||||
const ACCENT_YELLOW: u32 = 0xf9e2af;
|
||||
const ACCENT_GREEN: u32 = 0xa6e3a1;
|
||||
const ACCENT_BLUE: u32 = 0x89b4fa;
|
||||
const ACCENT_MAUVE: u32 = 0xcba6f7;
|
||||
|
||||
impl Render for HelloWeb {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let is_running = self.current_run.as_ref().is_some_and(|r| r.total.is_none());
|
||||
|
||||
// -- Preset buttons --
|
||||
let preset_row = Preset::ALL.iter().enumerate().fold(
|
||||
div().flex().flex_row().gap_2(),
|
||||
|row, (index, &preset)| {
|
||||
let is_selected = preset == self.selected_preset;
|
||||
let (bg, text_color) = if is_selected {
|
||||
(ACCENT_BLUE, BG_BASE)
|
||||
} else {
|
||||
(BG_OVERLAY, TEXT_SECONDARY)
|
||||
};
|
||||
row.child(
|
||||
div()
|
||||
.id(ElementId::NamedInteger("preset".into(), index as u64))
|
||||
.px_3()
|
||||
.py_1()
|
||||
.rounded_md()
|
||||
.bg(rgb(bg))
|
||||
.text_color(rgb(text_color))
|
||||
.text_sm()
|
||||
.cursor_pointer()
|
||||
.when(!is_running, |this| {
|
||||
this.on_click(cx.listener(move |this, _event, _window, _cx| {
|
||||
this.selected_preset = preset;
|
||||
}))
|
||||
})
|
||||
.child(preset.label()),
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
// -- Go button --
|
||||
let (go_bg, go_text, go_label) = if is_running {
|
||||
(BG_OVERLAY, TEXT_DIM, "Running…")
|
||||
} else {
|
||||
(ACCENT_GREEN, BG_BASE, "Count Primes")
|
||||
};
|
||||
let go_button = div()
|
||||
.id("go")
|
||||
.px_4()
|
||||
.py(px(6.))
|
||||
.rounded_md()
|
||||
.bg(rgb(go_bg))
|
||||
.text_color(rgb(go_text))
|
||||
.cursor_pointer()
|
||||
.when(!is_running, |this| {
|
||||
this.on_click(cx.listener(|this, _event, _window, cx| {
|
||||
this.start_search(cx);
|
||||
}))
|
||||
})
|
||||
.child(go_label);
|
||||
|
||||
// -- Progress / result area --
|
||||
let status_area = if let Some(run) = &self.current_run {
|
||||
let progress_fraction = run.chunks_done as f32 / NUM_CHUNKS as f32;
|
||||
let progress_pct = (progress_fraction * 100.0) as u32;
|
||||
|
||||
let status_text: SharedString = if let Some(total) = run.total {
|
||||
format!(
|
||||
"Found {} primes below {} in {:.0} ms",
|
||||
format_number(total),
|
||||
format_number(run.limit),
|
||||
run.elapsed.unwrap_or(0.0),
|
||||
)
|
||||
.into()
|
||||
} else {
|
||||
format!(
|
||||
"Searching up to {} … {}/{} chunks ({}%)",
|
||||
format_number(run.limit),
|
||||
run.chunks_done,
|
||||
NUM_CHUNKS,
|
||||
progress_pct,
|
||||
)
|
||||
.into()
|
||||
};
|
||||
|
||||
let bar_color = if run.total.is_some() {
|
||||
ACCENT_GREEN
|
||||
} else {
|
||||
ACCENT_BLUE
|
||||
};
|
||||
|
||||
let chunk_dots =
|
||||
(0..NUM_CHUNKS as usize).fold(div().flex().flex_row().gap_1().mt_2(), |row, i| {
|
||||
let done = i < run.chunks_done as usize;
|
||||
let color = if done { ACCENT_MAUVE } else { BG_OVERLAY };
|
||||
row.child(div().size(px(10.)).rounded_sm().bg(rgb(color)))
|
||||
});
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.child(div().text_color(rgb(TEXT_PRIMARY)).child(status_text))
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.h(px(8.))
|
||||
.rounded_sm()
|
||||
.bg(rgb(BG_OVERLAY))
|
||||
.child(
|
||||
div()
|
||||
.h_full()
|
||||
.rounded_sm()
|
||||
.bg(rgb(bar_color))
|
||||
.w(gpui::relative(progress_fraction)),
|
||||
),
|
||||
)
|
||||
.child(chunk_dots)
|
||||
} else {
|
||||
div().flex().flex_col().w_full().child(
|
||||
div()
|
||||
.text_color(rgb(TEXT_DIM))
|
||||
.child("Select a range and press Count Primes to begin."),
|
||||
)
|
||||
};
|
||||
|
||||
// -- History log --
|
||||
let history_section = if self.history.is_empty() {
|
||||
div()
|
||||
} else {
|
||||
self.history
|
||||
.iter()
|
||||
.rev()
|
||||
.fold(div().flex().flex_col().gap_1(), |col, entry| {
|
||||
col.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(rgb(TEXT_SECONDARY))
|
||||
.child(entry.clone()),
|
||||
)
|
||||
})
|
||||
};
|
||||
|
||||
// -- Layout --
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.size_full()
|
||||
.bg(rgb(BG_BASE))
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.gap_4()
|
||||
.p_4()
|
||||
// Title
|
||||
.child(
|
||||
div()
|
||||
.text_xl()
|
||||
.text_color(rgb(TEXT_PRIMARY))
|
||||
.child("Prime Sieve — GPUI Web"),
|
||||
)
|
||||
.child(div().text_sm().text_color(rgb(TEXT_DIM)).child(format!(
|
||||
"Background threads: {} · Chunks per run: {}",
|
||||
std::thread::available_parallelism().map_or(2, |n| n.get().max(2)),
|
||||
NUM_CHUNKS,
|
||||
)))
|
||||
// Controls
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.gap_3()
|
||||
.p_4()
|
||||
.w(px(500.))
|
||||
.rounded_lg()
|
||||
.bg(rgb(BG_SURFACE))
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(rgb(ACCENT_YELLOW))
|
||||
.child("Count primes below:"),
|
||||
)
|
||||
.child(preset_row)
|
||||
.child(go_button),
|
||||
)
|
||||
// Status
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.w(px(500.))
|
||||
.p_4()
|
||||
.rounded_lg()
|
||||
.bg(rgb(BG_SURFACE))
|
||||
.child(status_area),
|
||||
)
|
||||
// History
|
||||
.when(!self.history.is_empty(), |this| {
|
||||
this.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.w(px(500.))
|
||||
.p_4()
|
||||
.rounded_lg()
|
||||
.bg(rgb(BG_SURFACE))
|
||||
.gap_2()
|
||||
.child(div().text_sm().text_color(rgb(TEXT_DIM)).child("History"))
|
||||
.child(history_section),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Entry point
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn main() {
|
||||
gpui_platform::web_init();
|
||||
gpui_platform::application().run(|cx: &mut App| {
|
||||
let bounds = Bounds::centered(None, size(px(640.), px(560.)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| cx.new(HelloWeb::new),
|
||||
)
|
||||
.expect("failed to open window");
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user