Expand description
The UI debugger: drive any egui app the way a browser automation tool drives a web page.
Snapshot the UI as an element tree with short refs (e12), act on refs (click, fill, press,
drag…), wait for conditions, read values, take screenshots, and an in-app inspector (F12).
Everything lives in an egui plugin on the context, so it works in any egui app,
with or without ashlar-app’s runtime: the host only passes control requests in.
- The element tree is egui’s own AccessKit output (so plain egui widgets show up with their
role, label, value and state) merged with ashlar’s
automationregistry (dotted ids and component roles). The first request turns egui’s AccessKit output on for the rest of the session. - Refs are stable: the same widget keeps its ref across snapshots while it exists. A ref whose
element is gone fails with
stale_ref. - Input is injected one frame at a time; a request is answered once the frames that carry it have run. Waits are checked on frames the app runs anyway, plus one wake at their deadline: nothing repaints in a loop.
- The inspector costs nothing while closed: no frames, no allocations.
The wire protocol is docs/control-protocol.md (methods METHODS).
§In an app without ashlar-app
Install the plugin once, and hand every request your control dispatcher does not answer
itself to handle (from any thread). The reply arrives on the request’s channel once the
frames it needs have run, so keep your event loop running as usual.
use ashlar_control::{Reply, Request};
use ashlar_ui::devtools::{self, Outcome};
let ctx = egui::Context::default();
devtools::install(&ctx);
// in your control dispatcher:
let (req, reply) = Request::new("ui.snapshot", serde_json::json!({"interactive": true}));
match devtools::handle(&ctx, req) {
Outcome::Accepted => {} // answered on a later frame
Outcome::NotMine(req) => req.unknown(), // or your own methods
}
// your event loop runs frames; the debugger answers from them
for _ in 0..3 {
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| {
if ui.button("Save").clicked() { /* … */ }
});
out.textures_delta.clear();
}
let Ok(Reply::Ok(snap)) = reply.try_recv() else { panic!("no snapshot") };
assert!(snap["tree"].as_str().unwrap().contains(r#"button "Save""#));Optional hooks: set_busy (your own pending work, for ui.wait {for: "idle"}),
section (extra inspector panels: logs, undo history…), set_hotkey.
Modules§
- image
- Screenshot encoding: crop a captured frame to a rect in points and encode it as PNG; base64 for the wire (pictures travel inline, never through a predictable temp file).
- input
- Injected input: one
InputFrameper frame, built from request parameters. Every number that comes from a request is hostile: non-finite values are refused, the rest clamped. - model
- The element tree the debugger works on: egui’s own AccessKit output (every egui widget: role,
label, value, state, bounds) merged with ashlar’s
automationregistry (stable dotted ids, component roles, values egui does not expose), with short refs (e12) that stay the same for an element across snapshots.
Structs§
- Offscreen
Frame - One frame for an
Offscreenrenderer: what the backend would paint, already tessellated.
Enums§
Constants§
Functions§
- call
handlewith a fresh request: the receiver gets the reply.- handle
- Take a control request: a debugger method is queued for the next frame (which this asks
for) and answered there or later; anything else is handed back. Callable from any thread, but
not from inside a
ctx.input/ctx.memoryclosure (they hold the context’s lock). - handles
- Whether
handleanswersmethod. - has_
pending - Whether debugger work waits for frames: requests not yet read, injected input still playing, waits and screenshots. An app whose window is hidden (eframe then runs no UI pass) runs one itself while this holds, so agents can drive a covered or minimized window.
- install
- Add the debugger (and the automation registry) to a context. Idempotent;
handledoes it too, but installing before the first frame saves one frame on the first request. - is_open
- Whether the inspector is open.
- section
- Show a panel of text lines in the inspector (logs, the undo history…).
linesruns only while the inspector is open, so calling this every frame costs nothing when it is closed. Call it each frame before the frame ends (sections are not kept). - set_
busy - Tell
ui.wait {for: "idle"}whether the app has work of its own pending (background jobs, a save in flight). Cheap: call it every frame. - set_
hotkey - The key that opens and closes the inspector (F12 by default;
None: onlydevtools.toggleandset_open). - set_
offscreen - Render screenshots with
renderinstead of asking the backend to capture a presented frame (None: the backend’sViewportCommand::Screenshot). - set_
open - Open or close the inspector.
Type Aliases§
- Offscreen
- Renders a frame without presenting it: screenshots then work for a window that is covered,
in the background or on a locked screen.
ashlar-appinstalls one for its wgpu renderer.