pub(crate) fn screen_sized(w: f32, h: f32) -> egui::RawInput {
egui::RawInput {
screen_rect: Some(egui::Rect::from_min_size(
egui::Pos2::ZERO,
egui::vec2(w, h),
)),
..Default::default()
}
}
pub(crate) fn screen() -> egui::RawInput {
screen_sized(1024.0, 4096.0)
}
pub(crate) type Painted = (String, egui::Rect);
pub(crate) fn collect(shape: &egui::Shape, out: &mut Vec<Painted>) {
let mut inked = Vec::new();
descend(shape, &mut inked);
out.extend(inked.into_iter().map(|(text, rect, _)| (text, rect)));
}
fn descend(shape: &egui::Shape, out: &mut Vec<(String, egui::Rect, egui::Color32)>) {
match shape {
egui::Shape::Text(t) => out.push((
visible(&t.galley),
t.galley.rect.translate(t.pos.to_vec2()),
ink(t),
)),
egui::Shape::Vec(shapes) => shapes.iter().for_each(|s| descend(s, out)),
_ => {}
}
}
fn ink(t: &egui::epaint::TextShape) -> egui::Color32 {
match t.galley.job.sections.first().map(|s| s.format.color) {
Some(colour) if colour != egui::Color32::PLACEHOLDER => colour,
_ => t.fallback_color,
}
}
fn visible(galley: &egui::Galley) -> String {
let mut out = String::new();
for row in &galley.rows {
out.extend(row.glyphs.iter().map(|g| g.chr));
if row.ends_with_newline {
out.push('\n');
}
}
out
}
pub(crate) fn collect_text(shape: &egui::Shape, out: &mut String) {
let mut painted = Vec::new();
collect(shape, &mut painted);
for (text, _) in painted {
out.push_str(&text);
out.push('\n');
}
}
pub(crate) fn text_of(output: &egui::FullOutput) -> String {
let mut text = String::new();
for clipped in &output.shapes {
collect_text(&clipped.shape, &mut text);
}
text
}
pub(crate) fn painted_of(output: &egui::FullOutput) -> Vec<Painted> {
let mut painted = Vec::new();
for clipped in &output.shapes {
collect(&clipped.shape, &mut painted);
}
painted
}
#[derive(Clone)]
pub(crate) struct Seen {
pub(crate) text: String,
pub(crate) laid: egui::Rect,
pub(crate) shown: egui::Rect,
pub(crate) ink: egui::Color32,
}
pub(crate) fn seen_of(output: &egui::FullOutput) -> Vec<Seen> {
let mut out = Vec::new();
for clipped in &output.shapes {
let mut here = Vec::new();
descend(&clipped.shape, &mut here);
out.extend(here.into_iter().filter_map(|(text, laid, ink)| {
let shown = laid.intersect(clipped.clip_rect);
(shown.width() > 0.5 && shown.height() > 0.5).then_some(Seen {
text,
laid,
shown,
ink,
})
}));
}
out
}
pub(crate) fn paint(mut body: impl FnMut(&mut egui::Ui)) -> String {
let ctx = egui::Context::default();
let output = ctx.run(screen(), |ctx| {
egui::CentralPanel::default().show(ctx, &mut body);
});
text_of(&output)
}
pub(crate) fn collect_fills(shape: &egui::Shape, out: &mut Vec<egui::Color32>) {
match shape {
egui::Shape::Rect(r) => out.push(r.fill),
egui::Shape::Vec(shapes) => shapes.iter().for_each(|s| collect_fills(s, out)),
_ => {}
}
}
pub(crate) fn paint_fills(mut body: impl FnMut(&mut egui::Ui)) -> Vec<egui::Color32> {
let ctx = egui::Context::default();
let output = ctx.run(screen(), |ctx| {
egui::CentralPanel::default().show(ctx, &mut body);
});
let mut out = Vec::new();
for clipped in &output.shapes {
collect_fills(&clipped.shape, &mut out);
}
out
}
fn settled_frame(w: f32, h: f32, mut body: impl FnMut(&mut egui::Ui)) -> egui::FullOutput {
let ctx = egui::Context::default();
let mut frame = || {
ctx.run(screen_sized(w, h), |ctx| {
egui::CentralPanel::default().show(ctx, &mut body);
})
};
let _ = frame();
frame()
}
pub(crate) fn paint_settled(w: f32, h: f32, body: impl FnMut(&mut egui::Ui)) -> String {
text_of(&settled_frame(w, h, body))
}
pub(crate) fn painted_settled(w: f32, h: f32, body: impl FnMut(&mut egui::Ui)) -> Vec<Painted> {
painted_of(&settled_frame(w, h, body))
}
pub(crate) fn span(painted: &[Painted]) -> (f32, f32) {
painted
.iter()
.fold((f32::MAX, f32::MIN), |(top, bottom), (_, rect)| {
(top.min(rect.top()), bottom.max(rect.bottom()))
})
}
#[cfg(test)]
mod tests {
use super::collect_text;
#[test]
fn collect_text_descends_shape_vec_and_ignores_non_text() {
use egui::{Color32, FontId, Pos2, Stroke};
let ctx = egui::Context::default();
let mut nested: Option<egui::Shape> = None;
let _ = ctx.run(egui::RawInput::default(), |ctx| {
let galley = ctx.fonts(|f| {
f.layout_no_wrap("nested\nrows".into(), FontId::default(), Color32::WHITE)
});
nested = Some(egui::Shape::Vec(vec![egui::Shape::Text(
egui::epaint::TextShape {
pos: Pos2::ZERO,
galley,
underline: Stroke::NONE,
fallback_color: Color32::WHITE,
override_text_color: None,
opacity_factor: 1.0,
angle: 0.0,
},
)]));
});
let mut out = String::new();
collect_text(nested.as_ref().unwrap(), &mut out);
collect_text(&egui::Shape::Noop, &mut out);
assert_eq!(out, "nested\nrows\n");
}
#[test]
fn collect_fills_descends_shape_vec_and_ignores_non_rect() {
let rect = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(3.0, 10.0));
let nested = egui::Shape::Vec(vec![egui::Shape::rect_filled(
rect,
0.0,
egui::Color32::RED,
)]);
let mut out = Vec::new();
super::collect_fills(&nested, &mut out);
super::collect_fills(&egui::Shape::Noop, &mut out);
assert_eq!(out, vec![egui::Color32::RED]);
}
}