Skip to main content

Module devtools

Module devtools 

Source
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 automation registry (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 InputFrame per 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 automation registry (stable dotted ids, component roles, values egui does not expose), with short refs (e12) that stay the same for an element across snapshots.

Structs§

OffscreenFrame
One frame for an Offscreen renderer: what the backend would paint, already tessellated.

Enums§

Outcome
What handle did with a request.

Constants§

DEVTOOLS
Frames the inspector and the debugger’s timers ask for.
METHODS
The methods handle answers.

Functions§

call
handle with 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.memory closure (they hold the context’s lock).
handles
Whether handle answers method.
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; handle does 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…). lines runs 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: only devtools.toggle and set_open).
set_offscreen
Render screenshots with render instead of asking the backend to capture a presented frame (None: the backend’s ViewportCommand::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-app installs one for its wgpu renderer.