2026-08-05 18:05:04 -04:00
|
|
|
//! Driving egui headlessly from tests: build an input frame, synthesize a
|
|
|
|
|
//! click, read back what was painted.
|
|
|
|
|
|
|
|
|
|
/// A frame of input at `size`, carrying `events`.
|
|
|
|
|
///
|
2026-08-09 16:25:43 -04:00
|
|
|
/// The viewport size is load-bearing: a modal is centred in it, and a panel
|
2026-08-05 18:05:04 -04:00
|
|
|
/// that does not fit is simply not painted at all.
|
|
|
|
|
pub fn raw_input(size: egui::Vec2, events: Vec<egui::Event>) -> egui::RawInput {
|
|
|
|
|
egui::RawInput {
|
|
|
|
|
screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, size)),
|
|
|
|
|
events,
|
|
|
|
|
..Default::default()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-09 16:25:43 -04:00
|
|
|
/// A primary-button press and release at `pos`, preceded by the pointer
|
|
|
|
|
/// moving there: egui hit-tests against the pointer's *current* position,
|
|
|
|
|
/// so a press delivered without the move lands wherever the pointer was
|
|
|
|
|
/// last frame.
|
2026-08-05 18:05:04 -04:00
|
|
|
pub fn click_at(pos: egui::Pos2) -> Vec<egui::Event> {
|
|
|
|
|
let button = |pressed| egui::Event::PointerButton {
|
|
|
|
|
pos,
|
|
|
|
|
button: egui::PointerButton::Primary,
|
|
|
|
|
pressed,
|
|
|
|
|
modifiers: egui::Modifiers::NONE,
|
|
|
|
|
};
|
2026-08-09 16:25:43 -04:00
|
|
|
vec![egui::Event::PointerMoved(pos), button(true), button(false)]
|
2026-08-05 18:05:04 -04:00
|
|
|
}
|
|
|
|
|
|
2026-08-09 16:25:43 -04:00
|
|
|
/// A `Ui` from a real (headless) egui pass, so measuring helpers see the
|
|
|
|
|
/// same fonts the app paints with.
|
2026-08-05 19:17:11 -04:00
|
|
|
pub fn with_ui<R>(f: impl FnOnce(&mut egui::Ui) -> R) -> R {
|
|
|
|
|
let ctx = egui::Context::default();
|
|
|
|
|
let mut f = Some(f);
|
|
|
|
|
let mut out = None;
|
|
|
|
|
let _ = ctx.run(egui::RawInput::default(), |ctx| {
|
|
|
|
|
egui::CentralPanel::default().show(ctx, |ui| {
|
|
|
|
|
if let Some(f) = f.take() {
|
|
|
|
|
out = Some(f(ui));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
out.expect("the central panel ran")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Every text galley painted this frame, in paint order, each with the
|
|
|
|
|
/// rectangle it occupies.
|
|
|
|
|
fn painted_galleys(out: &egui::FullOutput) -> Vec<(&std::sync::Arc<egui::Galley>, egui::Rect)> {
|
|
|
|
|
fn walk<'a>(
|
|
|
|
|
shape: &'a egui::epaint::Shape,
|
|
|
|
|
into: &mut Vec<(&'a std::sync::Arc<egui::Galley>, egui::Rect)>,
|
|
|
|
|
) {
|
2026-08-05 18:05:04 -04:00
|
|
|
match shape {
|
2026-08-05 19:17:11 -04:00
|
|
|
egui::epaint::Shape::Text(t) => {
|
|
|
|
|
into.push((&t.galley, egui::Rect::from_min_size(t.pos, t.galley.size())))
|
|
|
|
|
}
|
2026-08-05 18:05:04 -04:00
|
|
|
egui::epaint::Shape::Vec(shapes) => {
|
|
|
|
|
for s in shapes {
|
|
|
|
|
walk(s, into);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-05 19:17:11 -04:00
|
|
|
let mut galleys = Vec::new();
|
2026-08-05 18:05:04 -04:00
|
|
|
for clipped in &out.shapes {
|
2026-08-05 19:17:11 -04:00
|
|
|
walk(&clipped.shape, &mut galleys);
|
2026-08-05 18:05:04 -04:00
|
|
|
}
|
2026-08-05 19:17:11 -04:00
|
|
|
galleys
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-09 16:25:43 -04:00
|
|
|
/// Every text galley painted this frame, each with the rectangle it
|
|
|
|
|
/// occupies. Labels carry no widget id worth recording, so reading the
|
|
|
|
|
/// shapes back is the only way to check the text a user actually sees.
|
2026-08-05 19:17:11 -04:00
|
|
|
pub fn painted(out: &egui::FullOutput) -> Vec<(String, egui::Rect)> {
|
|
|
|
|
painted_galleys(out)
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|(g, rect)| (g.text().to_string(), rect))
|
|
|
|
|
.collect()
|
2026-08-05 18:05:04 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Every string painted this frame, in paint order.
|
|
|
|
|
pub fn painted_text(out: &egui::FullOutput) -> Vec<String> {
|
|
|
|
|
painted(out).into_iter().map(|(text, _)| text).collect()
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-09 02:58:13 -04:00
|
|
|
/// Every styled *run* of text painted this frame with the color it was
|
2026-08-09 16:25:43 -04:00
|
|
|
/// painted in, in paint order. A galley can hold several colors at once;
|
|
|
|
|
/// runs are the layout job's own sections, so a single-color label yields
|
|
|
|
|
/// exactly one entry.
|
2026-08-09 02:58:13 -04:00
|
|
|
pub fn painted_spans(out: &egui::FullOutput) -> Vec<(String, egui::Color32)> {
|
|
|
|
|
painted_galleys(out)
|
|
|
|
|
.into_iter()
|
|
|
|
|
.flat_map(|(g, _)| {
|
|
|
|
|
g.job
|
|
|
|
|
.sections
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|s| (g.job.text[s.byte_range.clone()].to_string(), s.format.color))
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
})
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 19:26:18 -04:00
|
|
|
/// Every styled run painted this frame that has a background behind it, with
|
|
|
|
|
/// that background, in paint order.
|
|
|
|
|
///
|
|
|
|
|
/// The distinguishing mark of a highlighted match: the column headers and the
|
|
|
|
|
/// strong parts of a snippet are painted in the same *text* color, so
|
|
|
|
|
/// [`painted_spans`] alone cannot tell a match from a header.
|
|
|
|
|
pub fn painted_backgrounds(out: &egui::FullOutput) -> Vec<(String, egui::Color32)> {
|
|
|
|
|
painted_galleys(out)
|
|
|
|
|
.into_iter()
|
|
|
|
|
.flat_map(|(g, _)| {
|
|
|
|
|
g.job
|
|
|
|
|
.sections
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|s| s.format.background != egui::Color32::TRANSPARENT)
|
|
|
|
|
.map(|s| {
|
|
|
|
|
(
|
|
|
|
|
g.job.text[s.byte_range.clone()].to_string(),
|
|
|
|
|
s.format.background,
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
})
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-05 19:17:11 -04:00
|
|
|
/// Every *visible* row of every galley painted this frame, in paint order.
|
2026-08-09 16:25:43 -04:00
|
|
|
/// Not the same as [`painted_text`]: a galley's `text()` is the job it was
|
|
|
|
|
/// laid out from, including the rows epaint dropped at `wrap.max_rows` —
|
|
|
|
|
/// the laid-out rows are the only place a truncation is visible.
|
2026-08-05 19:17:11 -04:00
|
|
|
pub fn painted_rows(out: &egui::FullOutput) -> Vec<String> {
|
|
|
|
|
painted_galleys(out)
|
|
|
|
|
.into_iter()
|
|
|
|
|
.flat_map(|(g, _)| g.rows.iter().map(|r| r.text()).collect::<Vec<_>>())
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-09 16:25:43 -04:00
|
|
|
/// Every mesh painted this frame, in paint order. A mesh means a shape
|
|
|
|
|
/// assembled vertex by vertex — the only way to get a gradient out of egui
|
|
|
|
|
/// — and its color varies across the shape, so the vertices are what a
|
|
|
|
|
/// check has to read.
|
2026-08-07 20:44:07 -04:00
|
|
|
pub fn painted_meshes(out: &egui::FullOutput) -> Vec<&egui::Mesh> {
|
|
|
|
|
fn walk<'a>(shape: &'a egui::epaint::Shape, into: &mut Vec<&'a egui::Mesh>) {
|
|
|
|
|
match shape {
|
|
|
|
|
egui::epaint::Shape::Mesh(mesh) => into.push(mesh),
|
|
|
|
|
egui::epaint::Shape::Vec(shapes) => {
|
|
|
|
|
for s in shapes {
|
|
|
|
|
walk(s, into);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let mut meshes = Vec::new();
|
|
|
|
|
for clipped in &out.shapes {
|
|
|
|
|
walk(&clipped.shape, &mut meshes);
|
|
|
|
|
}
|
|
|
|
|
meshes
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-05 18:05:04 -04:00
|
|
|
/// The centre of `needle`'s galley, as a click target.
|
|
|
|
|
///
|
|
|
|
|
/// The *last* match wins, so a string painted both behind a modal and on it
|
|
|
|
|
/// resolves to the one on top — which is the one a click would reach.
|
|
|
|
|
pub fn painted_text_center(out: &egui::FullOutput, needle: &str) -> Option<egui::Pos2> {
|
|
|
|
|
painted(out)
|
|
|
|
|
.iter()
|
|
|
|
|
.rev()
|
|
|
|
|
.find(|(text, _)| text == needle)
|
|
|
|
|
.map(|(_, rect)| rect.center())
|
|
|
|
|
}
|