Files
designcraft/crates/ui-egui/examples/ui_shot.rs
T
Brandon ThomasandClaude Opus 5.5 9b8983ada2 M1: ruler guides (drag out/move/delete, Create Guides); fix task-bar deadlock while dragging
- engine: guide.add/move/delete/deleteAll/list, layout.createGuides (rows/columns with gutters,
  margins or page) (+ test)
- UI: drag a guide out of either ruler (page guide, or spread guide on the pasteboard), drag one
  with the selection tools to move it, drop it on a ruler to delete it; spread guides cross the
  pasteboard; Layout › Create Guides…, View › Grids & Guides › Delete All Guides on Spread
- fix: the Contextual Task Bar called is_pointer_over_egui inside ctx.input — a deadlock that
  froze the app while dragging with a selection
- ui_shot injects synthetic input (ui.click/ui.drag/ui.key now work offscreen)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 00:39:44 -04:00

98 lines
4.0 KiB
Rust
Raw 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 DesignCraft window offscreen (wgpu) — works with a
//! locked screen or a hidden window, unlike `ui.screenshot`.
//!
//! `cargo run -p designcraft-ui-egui --example ui_shot -- script.jsonl`
//!
//! The script is JSON lines. Each line is a control-channel request (`{"method": …, "params": …}`,
//! see `docs/control-protocol.md`), or `{"shot": "/abs/out.png"}` to save the window as PNG, or
//! `{"steps": n}` to run extra frames. The window is 1440×900 pt at 2× and opens `file.newSample`
//! (unless the first line is `{"empty": true}`).
use designcraft_engine::Session;
use designcraft_ui_egui::{ControlRequest, DesignApp, Services};
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 services = Services {
write: Some(Box::new(|p: &str, b: &[u8]| std::fs::write(p, b).map_err(|e| e.to_string()))),
read: Some(Box::new(|p: &str| std::fs::read(p).map_err(|e| e.to_string()))),
..Default::default()
};
let (tx, rx) = std::sync::mpsc::channel::<ControlRequest>();
let mut app = DesignApp::new(Session::new(), services).with_control(rx);
app.integrated_titlebar = true;
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();
if lines.first().is_none_or(|l| l.get("empty").is_none()) {
let _ = app.run("file.newSample", serde_json::json!({}));
}
let mut harness = egui_kittest::Harness::builder()
.with_size(egui::vec2(1440.0, 900.0))
.with_pixels_per_point(2.0)
.with_max_steps(1_000_000)
.wgpu()
.build_ui_state(
|ui, app: &mut DesignApp| {
// The builder runs frames before `max_texture_side` can be raised: wait for it.
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 std::env::var_os("UI_SHOT_TRACE").is_some() {
eprintln!("> {l}");
}
if let Some(p) = l.get("shot").and_then(|v| v.as_str()) {
step_n(&mut harness, 3);
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(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");
// Pointer/keyboard input is injected one event per frame.
for _ in 0..40 {
step(&mut harness);
if let Ok(r) = reply.try_recv() {
let s = r.to_string();
println!("{m}: {}", &s[..s.len().min(300)]);
break;
}
}
step_n(&mut harness, 2);
}
}
}
/// One frame, with the app's synthetic input (`ui.click`, `ui.drag`, `ui.key`) injected like the
/// windowed app's raw-input hook does.
fn step(harness: &mut egui_kittest::Harness<'_, DesignApp>) {
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<'_, DesignApp>, n: usize) {
for _ in 0..n {
step(harness);
}
}