Add tests for layout and visuals of most egui widgets (#6752)
This is mostly in preparation for #5830 where I want to ensure that I don't introduce any regressions
This commit is contained in:
parent
0f1d6c2818
commit
501905b60d
10
Cargo.lock
10
Cargo.lock
|
|
@ -1423,6 +1423,16 @@ dependencies = [
|
||||||
"wgpu",
|
"wgpu",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "egui_tests"
|
||||||
|
version = "0.31.1"
|
||||||
|
dependencies = [
|
||||||
|
"egui",
|
||||||
|
"egui_extras",
|
||||||
|
"egui_kittest",
|
||||||
|
"image",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ehttp"
|
name = "ehttp"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
|
|
|
||||||
|
|
@ -274,6 +274,7 @@ impl<'a, State> Harness<'a, State> {
|
||||||
///
|
///
|
||||||
/// See also:
|
/// See also:
|
||||||
/// - [`Harness::try_run`].
|
/// - [`Harness::try_run`].
|
||||||
|
/// - [`Harness::try_run_realtime`].
|
||||||
/// - [`Harness::run_ok`].
|
/// - [`Harness::run_ok`].
|
||||||
/// - [`Harness::step`].
|
/// - [`Harness::step`].
|
||||||
/// - [`Harness::run_steps`].
|
/// - [`Harness::run_steps`].
|
||||||
|
|
@ -287,6 +288,27 @@ impl<'a, State> Harness<'a, State> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn _try_run(&mut self, sleep: bool) -> Result<u64, ExceededMaxStepsError> {
|
||||||
|
let mut steps = 0;
|
||||||
|
loop {
|
||||||
|
steps += 1;
|
||||||
|
self.step();
|
||||||
|
// We only care about immediate repaints
|
||||||
|
if self.root_viewport_output().repaint_delay != Duration::ZERO {
|
||||||
|
break;
|
||||||
|
} else if sleep {
|
||||||
|
std::thread::sleep(Duration::from_secs_f32(self.step_dt));
|
||||||
|
}
|
||||||
|
if steps > self.max_steps {
|
||||||
|
return Err(ExceededMaxStepsError {
|
||||||
|
max_steps: self.max_steps,
|
||||||
|
repaint_causes: self.ctx.repaint_causes(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(steps)
|
||||||
|
}
|
||||||
|
|
||||||
/// Run until
|
/// Run until
|
||||||
/// - all animations are done
|
/// - all animations are done
|
||||||
/// - no more repaints are requested
|
/// - no more repaints are requested
|
||||||
|
|
@ -302,23 +324,9 @@ impl<'a, State> Harness<'a, State> {
|
||||||
/// - [`Harness::run_ok`].
|
/// - [`Harness::run_ok`].
|
||||||
/// - [`Harness::step`].
|
/// - [`Harness::step`].
|
||||||
/// - [`Harness::run_steps`].
|
/// - [`Harness::run_steps`].
|
||||||
|
/// - [`Harness::try_run_realtime`].
|
||||||
pub fn try_run(&mut self) -> Result<u64, ExceededMaxStepsError> {
|
pub fn try_run(&mut self) -> Result<u64, ExceededMaxStepsError> {
|
||||||
let mut steps = 0;
|
self._try_run(false)
|
||||||
loop {
|
|
||||||
steps += 1;
|
|
||||||
self.step();
|
|
||||||
// We only care about immediate repaints
|
|
||||||
if self.root_viewport_output().repaint_delay != Duration::ZERO {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if steps > self.max_steps {
|
|
||||||
return Err(ExceededMaxStepsError {
|
|
||||||
max_steps: self.max_steps,
|
|
||||||
repaint_causes: self.ctx.repaint_causes(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(steps)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run until
|
/// Run until
|
||||||
|
|
@ -333,10 +341,34 @@ impl<'a, State> Harness<'a, State> {
|
||||||
/// - [`Harness::try_run`].
|
/// - [`Harness::try_run`].
|
||||||
/// - [`Harness::step`].
|
/// - [`Harness::step`].
|
||||||
/// - [`Harness::run_steps`].
|
/// - [`Harness::run_steps`].
|
||||||
|
/// - [`Harness::try_run_realtime`].
|
||||||
pub fn run_ok(&mut self) -> Option<u64> {
|
pub fn run_ok(&mut self) -> Option<u64> {
|
||||||
self.try_run().ok()
|
self.try_run().ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Run multiple frames, sleeping for [`HarnessBuilder::with_step_dt`] between frames.
|
||||||
|
///
|
||||||
|
/// This is useful to e.g. wait for an async operation to complete (e.g. loading of images).
|
||||||
|
/// Runs until
|
||||||
|
/// - all animations are done
|
||||||
|
/// - no more repaints are requested
|
||||||
|
/// - the maximum number of steps is reached (See [`HarnessBuilder::with_max_steps`])
|
||||||
|
///
|
||||||
|
/// Returns the number of steps that were run.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns an error if the maximum number of steps is exceeded.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
/// - [`Harness::run`].
|
||||||
|
/// - [`Harness::run_ok`].
|
||||||
|
/// - [`Harness::step`].
|
||||||
|
/// - [`Harness::run_steps`].
|
||||||
|
/// - [`Harness::try_run`].
|
||||||
|
pub fn try_run_realtime(&mut self) -> Result<u64, ExceededMaxStepsError> {
|
||||||
|
self._try_run(true)
|
||||||
|
}
|
||||||
|
|
||||||
/// Run a number of steps.
|
/// Run a number of steps.
|
||||||
/// Equivalent to calling [`Harness::step`] x times.
|
/// Equivalent to calling [`Harness::step`] x times.
|
||||||
pub fn run_steps(&mut self, steps: usize) {
|
pub fn run_steps(&mut self, steps: usize) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
[package]
|
||||||
|
name = "egui_tests"
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
version.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
egui = { workspace = true, default-features = true }
|
||||||
|
egui_kittest = { workspace = true, features = ["snapshot", "wgpu"] }
|
||||||
|
egui_extras = { workspace = true, features = ["image"]}
|
||||||
|
image = { workspace = true, features = ["png"] }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:ccd7bdd86e587bcf0577c92e10ed7c3c35195e37df109a84554ceb30a434768d
|
||||||
|
size 315482
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:975c279d6da2a2cb000df72bf5d9f3bdd200bb20adc00e29e8fd9ed4d2c6f6b1
|
||||||
|
size 340923
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:ad14068e60fa678ee749925dd3713ee2b12a83ec1bca9c413bdeb9bc27d8ac20
|
||||||
|
size 407795
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:996e02c1c10a0c76fa295160d117aceb764ef506608b151bafbdf263106dbe57
|
||||||
|
size 385129
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:aaf9b032037d0708894e568cc8e256b32be9cfb586eaffdc6167143b85562b37
|
||||||
|
size 415016
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:043be3ece0697ea7114b7bd743e5c958610ae38ac359b6f8120886edff8541d8
|
||||||
|
size 239522
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:0f7fbeeba8ae9e34c5400727690ac7941e2711f72f2dc23e3342cb06904e4a35
|
||||||
|
size 335775
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:96ae7be40161b0b42959b44c8f72b62fd2cd4b3b463fc7d5bcd02ead445edca1
|
||||||
|
size 355550
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:4991fdf58542ca14162cbd7f59b6a30d6c3d752a1215cc1890359bc3a1eb23c9
|
||||||
|
size 388912
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:4ac8cbcdeed098d52009be77c8815931553d979f5aaf0baf0a9296daf6373605
|
||||||
|
size 402699
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:605091767a73a934981d10d0ed59ff561772ed61e7691303b75b35ae01163ecc
|
||||||
|
size 336722
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:465e34d94bf734a2a7a1e8e4a71ce64c908c737a7c4fa2a6f812351f2aaa6808
|
||||||
|
size 233018
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:99f64e581b97df6694cb7c85ee7728a955e3c1a851ab660e8b6091eee1885bbe
|
||||||
|
size 9719
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:d39ec25b91f5f5d68305d2cb7cc0285d715fe30ccbd66369efbe7327d1899b52
|
||||||
|
size 10753
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:46d86987ba895ead9b28efcc37e1b4374f34eedebac83d1db9eaa8e5a3202ee3
|
||||||
|
size 13203
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:09c5904877c8895d3ad41b7082019ef87db40c6a91ad47401bb9b8ac79a62bdc
|
||||||
|
size 12914
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:1cd5e9ad416c3a0b6824debc343f196e6db90509fd201c60c7c1f9b022f37c1d
|
||||||
|
size 12322
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:e03cf99a3d28f73d4a72c0e616dc54198663b94bf5cffda694cf4eb4dee01be8
|
||||||
|
size 13445
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:e86a37c7b259a6bad61897545d927d75e8307916dc78d256e4d33c410fcd6876
|
||||||
|
size 7306
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:583fa78f79b39522a44c871642114ead9ed1d177bb8a3807d2c9e2cd89bf0b44
|
||||||
|
size 11076
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:d1a172cfadc91467529e5546e686673be73ba0071a55d55abc7a41fb1d07214d
|
||||||
|
size 11700
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:eed80e11dd3ba478217cf004654934214b522ea666074e023dda9a323473615a
|
||||||
|
size 12452
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:0ef91dfedc74cae59099bce32b2e42cb04649e84442e8010282a9c1ff2a7f2c8
|
||||||
|
size 12469
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:1892358a4552af3f529141d314cd18e4cf55a629d870798278a5470e3e0a8a94
|
||||||
|
size 11030
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:7300a0b88d4fdb6c1e543bfaf50e8964b2f84aaaf8197267b671d0cf3c8da30a
|
||||||
|
size 7033
|
||||||
|
|
@ -0,0 +1,370 @@
|
||||||
|
use egui::load::SizedTexture;
|
||||||
|
use egui::{
|
||||||
|
include_image, Align, Button, Color32, ColorImage, Direction, DragValue, Event, Grid, Layout,
|
||||||
|
PointerButton, Pos2, Response, Slider, Stroke, StrokeKind, TextWrapMode, TextureHandle,
|
||||||
|
TextureOptions, Ui, UiBuilder, Vec2, Widget,
|
||||||
|
};
|
||||||
|
use egui_kittest::kittest::{by, Node, Queryable};
|
||||||
|
use egui_kittest::{Harness, SnapshotResult, SnapshotResults};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn widget_tests() {
|
||||||
|
let mut results = SnapshotResults::new();
|
||||||
|
|
||||||
|
test_widget("button", |ui| ui.button("Button"), &mut results);
|
||||||
|
test_widget(
|
||||||
|
"button_image",
|
||||||
|
|ui| {
|
||||||
|
Button::image_and_text(
|
||||||
|
include_image!("../../../crates/eframe/data/icon.png"),
|
||||||
|
"Button",
|
||||||
|
)
|
||||||
|
.ui(ui)
|
||||||
|
},
|
||||||
|
&mut results,
|
||||||
|
);
|
||||||
|
test_widget(
|
||||||
|
"button_image_shortcut",
|
||||||
|
|ui| {
|
||||||
|
Button::image_and_text(
|
||||||
|
include_image!("../../../crates/eframe/data/icon.png"),
|
||||||
|
"Open",
|
||||||
|
)
|
||||||
|
.shortcut_text("⌘O")
|
||||||
|
.ui(ui)
|
||||||
|
},
|
||||||
|
&mut results,
|
||||||
|
);
|
||||||
|
results.add(VisualTests::test("button_image_shortcut_selected", |ui| {
|
||||||
|
Button::image_and_text(
|
||||||
|
include_image!("../../../crates/eframe/data/icon.png"),
|
||||||
|
"Open",
|
||||||
|
)
|
||||||
|
.shortcut_text("⌘O")
|
||||||
|
.selected(true)
|
||||||
|
.ui(ui)
|
||||||
|
}));
|
||||||
|
|
||||||
|
test_widget(
|
||||||
|
"selectable_value",
|
||||||
|
|ui| ui.selectable_label(false, "Selectable"),
|
||||||
|
&mut results,
|
||||||
|
);
|
||||||
|
test_widget(
|
||||||
|
"selectable_value_selected",
|
||||||
|
|ui| ui.selectable_label(true, "Selectable"),
|
||||||
|
&mut results,
|
||||||
|
);
|
||||||
|
|
||||||
|
test_widget(
|
||||||
|
"checkbox",
|
||||||
|
|ui| ui.checkbox(&mut false, "Checkbox"),
|
||||||
|
&mut results,
|
||||||
|
);
|
||||||
|
test_widget(
|
||||||
|
"checkbox_checked",
|
||||||
|
|ui| ui.checkbox(&mut true, "Checkbox"),
|
||||||
|
&mut results,
|
||||||
|
);
|
||||||
|
test_widget("radio", |ui| ui.radio(false, "Radio"), &mut results);
|
||||||
|
test_widget("radio_checked", |ui| ui.radio(true, "Radio"), &mut results);
|
||||||
|
|
||||||
|
test_widget(
|
||||||
|
"drag_value",
|
||||||
|
|ui| DragValue::new(&mut 12.0).ui(ui),
|
||||||
|
&mut results,
|
||||||
|
);
|
||||||
|
|
||||||
|
test_widget(
|
||||||
|
"text_edit",
|
||||||
|
|ui| {
|
||||||
|
ui.spacing_mut().text_edit_width = 45.0;
|
||||||
|
ui.text_edit_singleline(&mut "Hi!".to_owned())
|
||||||
|
},
|
||||||
|
&mut results,
|
||||||
|
);
|
||||||
|
|
||||||
|
test_widget(
|
||||||
|
"slider",
|
||||||
|
|ui| {
|
||||||
|
ui.spacing_mut().slider_width = 45.0;
|
||||||
|
Slider::new(&mut 12.0, 0.0..=100.0).ui(ui)
|
||||||
|
},
|
||||||
|
&mut results,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_widget(name: &str, mut w: impl FnMut(&mut Ui) -> Response, results: &mut SnapshotResults) {
|
||||||
|
results.add(test_widget_layout(name, &mut w));
|
||||||
|
results.add(VisualTests::test(name, &mut w));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_widget_layout(name: &str, mut w: impl FnMut(&mut Ui) -> Response) -> SnapshotResult {
|
||||||
|
let test_size = Vec2::new(110.0, 45.0);
|
||||||
|
|
||||||
|
struct Row {
|
||||||
|
main_dir: Direction,
|
||||||
|
main_align: Align,
|
||||||
|
main_justify: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Col {
|
||||||
|
cross_align: Align,
|
||||||
|
cross_justify: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut rows = Vec::new();
|
||||||
|
let mut cols = Vec::new();
|
||||||
|
|
||||||
|
for main_justify in [false, true] {
|
||||||
|
for main_dir in [
|
||||||
|
Direction::LeftToRight,
|
||||||
|
Direction::TopDown,
|
||||||
|
Direction::RightToLeft,
|
||||||
|
Direction::BottomUp,
|
||||||
|
] {
|
||||||
|
for main_align in [Align::Min, Align::Center, Align::Max] {
|
||||||
|
rows.push(Row {
|
||||||
|
main_dir,
|
||||||
|
main_align,
|
||||||
|
main_justify,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for cross_justify in [false, true] {
|
||||||
|
for cross_align in [Align::Min, Align::Center, Align::Max] {
|
||||||
|
cols.push(Col {
|
||||||
|
cross_align,
|
||||||
|
cross_justify,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut harness = Harness::builder().build_ui(|ui| {
|
||||||
|
egui_extras::install_image_loaders(ui.ctx());
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut wrap_test_size = test_size;
|
||||||
|
wrap_test_size.x /= 3.0;
|
||||||
|
ui.heading("Wrapping");
|
||||||
|
|
||||||
|
let modes = [
|
||||||
|
TextWrapMode::Extend,
|
||||||
|
TextWrapMode::Truncate,
|
||||||
|
TextWrapMode::Wrap,
|
||||||
|
];
|
||||||
|
Grid::new("wrapping")
|
||||||
|
.spacing(Vec2::new(test_size.x / 2.0, 4.0))
|
||||||
|
.show(ui, |ui| {
|
||||||
|
for mode in &modes {
|
||||||
|
ui.label(format!("{mode:?}"));
|
||||||
|
}
|
||||||
|
ui.end_row();
|
||||||
|
|
||||||
|
for mode in &modes {
|
||||||
|
let (_, rect) = ui.allocate_space(wrap_test_size);
|
||||||
|
|
||||||
|
let mut child_ui = ui.new_child(UiBuilder::new().max_rect(rect));
|
||||||
|
child_ui.style_mut().wrap_mode = Some(*mode);
|
||||||
|
w(&mut child_ui);
|
||||||
|
|
||||||
|
ui.painter().rect_stroke(
|
||||||
|
rect,
|
||||||
|
0.0,
|
||||||
|
Stroke::new(1.0, Color32::WHITE),
|
||||||
|
StrokeKind::Outside,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.heading("Layout");
|
||||||
|
Grid::new("layout").striped(true).show(ui, |ui| {
|
||||||
|
ui.label("");
|
||||||
|
for col in &cols {
|
||||||
|
ui.label(format!(
|
||||||
|
"cross_align: {:?}\ncross_justify:{:?}",
|
||||||
|
col.cross_align, col.cross_justify
|
||||||
|
));
|
||||||
|
}
|
||||||
|
ui.end_row();
|
||||||
|
|
||||||
|
for row in &rows {
|
||||||
|
ui.label(format!(
|
||||||
|
"main_dir: {:?}\nmain_align: {:?}\nmain_justify: {:?}",
|
||||||
|
row.main_dir, row.main_align, row.main_justify
|
||||||
|
));
|
||||||
|
for col in &cols {
|
||||||
|
let layout = Layout {
|
||||||
|
main_dir: row.main_dir,
|
||||||
|
main_align: row.main_align,
|
||||||
|
main_justify: row.main_justify,
|
||||||
|
cross_align: col.cross_align,
|
||||||
|
cross_justify: col.cross_justify,
|
||||||
|
main_wrap: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (_, rect) = ui.allocate_space(test_size);
|
||||||
|
|
||||||
|
let mut child_ui = ui.new_child(UiBuilder::new().layout(layout).max_rect(rect));
|
||||||
|
w(&mut child_ui);
|
||||||
|
|
||||||
|
ui.painter().rect_stroke(
|
||||||
|
rect,
|
||||||
|
0.0,
|
||||||
|
Stroke::new(1.0, Color32::WHITE),
|
||||||
|
StrokeKind::Outside,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.end_row();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
harness.fit_contents();
|
||||||
|
harness.try_snapshot(&format!("layout/{name}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Utility to create a snapshot test of the different states of a egui widget.
|
||||||
|
/// This renders each state to a texture to work around the fact only a single widget can be
|
||||||
|
/// hovered / pressed / focused at a time.
|
||||||
|
struct VisualTests<'a> {
|
||||||
|
name: String,
|
||||||
|
w: &'a mut dyn FnMut(&mut Ui) -> Response,
|
||||||
|
results: Vec<(String, ColorImage)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> VisualTests<'a> {
|
||||||
|
pub fn test(name: &str, mut w: impl FnMut(&mut Ui) -> Response) -> SnapshotResult {
|
||||||
|
let mut vis = VisualTests::new(name, &mut w);
|
||||||
|
vis.add_default_states();
|
||||||
|
vis.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(name: &str, w: &'a mut dyn FnMut(&mut Ui) -> Response) -> Self {
|
||||||
|
Self {
|
||||||
|
name: name.to_owned(),
|
||||||
|
w,
|
||||||
|
results: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_default_states(&mut self) {
|
||||||
|
self.add("idle", |_| {});
|
||||||
|
self.add_node("hover", |node| {
|
||||||
|
node.hover();
|
||||||
|
});
|
||||||
|
self.add("pressed", |harness| {
|
||||||
|
harness.get_next().hover();
|
||||||
|
let rect = harness.get_next().bounding_box().unwrap();
|
||||||
|
let pos = Pos2::new(
|
||||||
|
((rect.x0 + rect.x1) / 2.0) as f32,
|
||||||
|
((rect.y0 + rect.y1) / 2.0) as f32,
|
||||||
|
);
|
||||||
|
harness.input_mut().events.push(Event::PointerButton {
|
||||||
|
button: PointerButton::Primary,
|
||||||
|
pos,
|
||||||
|
pressed: true,
|
||||||
|
modifiers: Default::default(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
self.add_node("focussed", |node| {
|
||||||
|
node.focus();
|
||||||
|
});
|
||||||
|
self.add_disabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn single_test(&mut self, f: impl FnOnce(&mut Harness<'_>), enabled: bool) -> ColorImage {
|
||||||
|
let mut harness = Harness::builder().with_step_dt(0.05).build_ui(|ui| {
|
||||||
|
egui_extras::install_image_loaders(ui.ctx());
|
||||||
|
ui.add_enabled_ui(enabled, |ui| {
|
||||||
|
(self.w)(ui);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
harness.fit_contents();
|
||||||
|
|
||||||
|
// Wait for images to load
|
||||||
|
harness.try_run_realtime().ok();
|
||||||
|
|
||||||
|
f(&mut harness);
|
||||||
|
|
||||||
|
harness.step();
|
||||||
|
|
||||||
|
let image = harness.render().expect("Failed to render harness");
|
||||||
|
|
||||||
|
ColorImage::from_rgba_unmultiplied(
|
||||||
|
[image.width() as usize, image.height() as usize],
|
||||||
|
image.as_ref(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add(&mut self, name: &str, test: impl FnOnce(&mut Harness<'_>)) {
|
||||||
|
let image = self.single_test(test, true);
|
||||||
|
self.results.push((name.to_owned(), image));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_disabled(&mut self) {
|
||||||
|
let image = self.single_test(|_| {}, false);
|
||||||
|
self.results.push(("disabled".to_owned(), image));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_node(&mut self, name: &str, test: impl FnOnce(&Node<'_>)) {
|
||||||
|
self.add(name, |harness| {
|
||||||
|
let node = harness.get_next();
|
||||||
|
test(&node);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(self) -> SnapshotResult {
|
||||||
|
let mut results = Some(self.results);
|
||||||
|
let mut images: Option<Vec<(String, TextureHandle, SizedTexture)>> = None;
|
||||||
|
|
||||||
|
let mut harness = Harness::new_ui(|ui| {
|
||||||
|
let results = images.get_or_insert_with(|| {
|
||||||
|
results
|
||||||
|
.take()
|
||||||
|
.unwrap()
|
||||||
|
.into_iter()
|
||||||
|
.map(|(name, image)| {
|
||||||
|
let size = Vec2::new(image.width() as f32, image.height() as f32);
|
||||||
|
let texture_handle =
|
||||||
|
ui.ctx()
|
||||||
|
.load_texture(name.clone(), image, TextureOptions::default());
|
||||||
|
let texture = SizedTexture::new(texture_handle.id(), size);
|
||||||
|
(name.clone(), texture_handle, texture)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
});
|
||||||
|
|
||||||
|
Grid::new("results").show(ui, |ui| {
|
||||||
|
for (name, _, image) in results {
|
||||||
|
ui.label(&*name);
|
||||||
|
|
||||||
|
ui.scope(|ui| {
|
||||||
|
ui.image(*image);
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.end_row();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
harness.fit_contents();
|
||||||
|
|
||||||
|
harness.try_snapshot(&format!("visuals/{}", self.name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trait HarnessExt {
|
||||||
|
fn get_next(&self) -> Node<'_>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HarnessExt for Harness<'_> {
|
||||||
|
fn get_next(&self) -> Node<'_> {
|
||||||
|
self.get_all(by()).next().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue