Files
wordcraft/crates/ui-egui/examples/ui_shot.rs
8c67ef7c28 Start on File › Home when launched with nothing to open (#766) (#853)
* #766: start on File › Home (Start screen) when launched with nothing to open

Desktop and web start on Backstage Home (Blank document, templates, Recent)
when no file, --sample or --control is given; Esc/back leaves the empty blank
document. File › Options › General › Start-up checkbox (ui.startScreen, kept
between runs, default on). WordApp::new is unchanged; hosts opt in via
WordApp::start_screen. ui_shot: {"empty":true,"startScreen":true}.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* i18n: Hungarian and French strings for this change

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* #766: Bulgarian for the Start screen setting

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 10:00:28 -04:00

99 lines
4.4 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Headless UI screenshots: renders the whole WordCraft window offscreen (wgpu), so it works
//! with a locked screen or a hidden window.
//!
//! `cargo run -p wordcraft-ui-egui --example ui_shot -- script.jsonl`
//!
//! The script is JSON lines: control-channel requests (`{"method": …, "params": …}`, see
//! `docs/control-protocol.md`), `{"shot": "/abs/out.png"}` to save the window as PNG, or
//! `{"steps": n}` to run extra frames, or `{"wait": ms}` to keep running frames for a while (for
//! background work such as a Zotero session). The window is 1440×900 pt at 2× and opens the sample
//! document unless the first line is `{"empty": true}`; `{"empty": true, "startScreen": true}`
//! starts the way the desktop app does with nothing to open (File › Home, #766).
use wordcraft_engine::Session;
use wordcraft_ui_egui::{ControlRequest, Services, WordApp};
static READY: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
fn main() {
let path = std::env::args().nth(1).expect("usage: ui_shot script.jsonl");
let script = std::fs::read_to_string(&path).expect("read script");
let (tx, rx) = std::sync::mpsc::channel::<ControlRequest>();
let lines: Vec<serde_json::Value> =
script.lines().filter(|l| !l.trim().is_empty()).map(|l| serde_json::from_str(l).expect("json line")).collect();
let doc = if lines.first().is_some_and(|l| l.get("empty").is_some()) {
wordcraft_doc::Document::new()
} else {
wordcraft_engine::sample::sample_document()
};
let mut app = WordApp::new(Session::new(doc), Services::default()).with_control(rx);
app.integrated_titlebar = true;
if lines.first().is_some_and(|l| l.get("startScreen").is_some()) {
app.start_screen();
}
let w = lines.iter().find_map(|l| l.get("width").and_then(|v| v.as_f64())).unwrap_or(1440.0) as f32;
let h = lines.iter().find_map(|l| l.get("height").and_then(|v| v.as_f64())).unwrap_or(900.0) as f32;
let mut harness =
egui_kittest::Harness::builder().with_size(egui::vec2(w, h)).with_pixels_per_point(2.0).with_max_steps(1_000_000).wgpu().build_ui_state(
|ui, app: &mut WordApp| {
if !READY.load(std::sync::atomic::Ordering::Relaxed) {
return;
}
let ctx = ui.ctx().clone();
app.logic(&ctx);
app.ui(ui);
},
app,
);
harness.input_mut().max_texture_side = Some(8192);
READY.store(true, std::sync::atomic::Ordering::Relaxed);
step_n(&mut harness, 6);
for l in lines {
if let Some(p) = l.get("shot").and_then(|v| v.as_str()) {
step_n(&mut harness, 4);
match harness.render() {
Ok(img) => {
img.save(p).expect("save png");
println!("{p}");
}
Err(e) => eprintln!("render failed: {e}"),
}
} else if let Some(n) = l.get("steps").and_then(|v| v.as_u64()) {
step_n(&mut harness, n as usize);
} else if let Some(ms) = l.get("wait").and_then(|v| v.as_u64()) {
let end = std::time::Instant::now() + std::time::Duration::from_millis(ms);
while std::time::Instant::now() < end {
step(&mut harness);
std::thread::sleep(std::time::Duration::from_millis(16));
}
} else if let Some(m) = l.get("method").and_then(|v| v.as_str()) {
let (req, reply) = ControlRequest::new(m, l.get("params").cloned().unwrap_or_default());
tx.send(req).expect("send");
for _ in 0..60 {
step(&mut harness);
if let Ok(r) = reply.try_recv() {
let s = r.to_string();
// `UI_SHOT_FULL=1` prints whole replies (e.g. `document.inspect` for scripted checks).
let cut = if std::env::var_os("UI_SHOT_FULL").is_some() { s.len() } else { s.len().min(300) };
println!("{m}: {}", s.get(..cut).unwrap_or(&s));
break;
}
}
step_n(&mut harness, 2);
}
}
}
fn step(harness: &mut egui_kittest::Harness<'_, WordApp>) {
let mut raw = std::mem::take(harness.input_mut());
harness.state_mut().raw_input_hook(&mut raw);
*harness.input_mut() = raw;
harness.step();
}
fn step_n(harness: &mut egui_kittest::Harness<'_, WordApp>, n: usize) {
for _ in 0..n {
step(harness);
}
}