Add `ImageLoader::has_pending` and `wait_for_pending_images` (#7030)
With kittest it was difficult to wait for images to be loaded before taking a snapshot test. This PR adds `Harness::with_wait_for_pending_images` (true by default) which will cause `Harness::run` to sleep until all images are loaded (or `HarnessBuilder::with_max_steps` is exceeded). It also adds a new ImageLoader::has_pending and BytesLoader::has_pending, which should be implemented if things are loaded / decoded asynchronously. It reverts https://github.com/emilk/egui/pull/6901 which was my previous attempt to fix this (but this didn't work since only the tested crate is compiled with cfg(test) and not it's dependencies)
This commit is contained in:
parent
0fd6a805a4
commit
120d736cfc
|
|
@ -3546,6 +3546,14 @@ impl Context {
|
|||
pub fn loaders(&self) -> Arc<Loaders> {
|
||||
self.read(|this| this.loaders.clone())
|
||||
}
|
||||
|
||||
/// Returns `true` if any image is currently being loaded.
|
||||
pub fn has_pending_images(&self) -> bool {
|
||||
self.read(|this| {
|
||||
this.loaders.image.lock().iter().any(|i| i.has_pending())
|
||||
|| this.loaders.bytes.lock().iter().any(|i| i.has_pending())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// ## Viewports
|
||||
|
|
|
|||
|
|
@ -328,6 +328,11 @@ pub trait BytesLoader {
|
|||
|
||||
/// If the loader caches any data, this should return the size of that cache.
|
||||
fn byte_size(&self) -> usize;
|
||||
|
||||
/// Returns `true` if some data is currently being loaded.
|
||||
fn has_pending(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an image which is currently being loaded.
|
||||
|
|
@ -395,6 +400,13 @@ pub trait ImageLoader {
|
|||
|
||||
/// If the loader caches any data, this should return the size of that cache.
|
||||
fn byte_size(&self) -> usize;
|
||||
|
||||
/// Returns `true` if some image is currently being loaded.
|
||||
///
|
||||
/// NOTE: You probably also want to check [`BytesLoader::has_pending`].
|
||||
fn has_pending(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// A texture with a known size.
|
||||
|
|
|
|||
|
|
@ -125,4 +125,8 @@ impl BytesLoader for EhttpLoader {
|
|||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
fn has_pending(&self) -> bool {
|
||||
self.cache.lock().values().any(|entry| entry.is_pending())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,4 +128,8 @@ impl BytesLoader for FileLoader {
|
|||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
fn has_pending(&self) -> bool {
|
||||
self.cache.lock().values().any(|entry| entry.is_pending())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ use egui::{
|
|||
use image::ImageFormat;
|
||||
use std::{mem::size_of, path::Path, sync::Arc, task::Poll};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use std::thread;
|
||||
|
||||
type Entry = Poll<Result<Arc<ColorImage>, String>>;
|
||||
|
||||
#[derive(Default)]
|
||||
|
|
@ -73,7 +76,7 @@ impl ImageLoader for ImageCrateLoader {
|
|||
return Err(LoadError::NotSupported);
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_arch = "wasm32", test)))]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[expect(clippy::unnecessary_wraps)] // needed here to match other return types
|
||||
fn load_image(
|
||||
ctx: &egui::Context,
|
||||
|
|
@ -85,7 +88,7 @@ impl ImageLoader for ImageCrateLoader {
|
|||
cache.lock().insert(uri.clone(), Poll::Pending);
|
||||
|
||||
// Do the image parsing on a bg thread
|
||||
std::thread::Builder::new()
|
||||
thread::Builder::new()
|
||||
.name(format!("egui_extras::ImageLoader::load({uri:?})"))
|
||||
.spawn({
|
||||
let ctx = ctx.clone();
|
||||
|
|
@ -113,8 +116,7 @@ impl ImageLoader for ImageCrateLoader {
|
|||
Ok(ImagePoll::Pending { size: None })
|
||||
}
|
||||
|
||||
// Load images on the current thread for tests, so they are less flaky
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn load_image(
|
||||
_ctx: &egui::Context,
|
||||
uri: &str,
|
||||
|
|
@ -179,6 +181,10 @@ impl ImageLoader for ImageCrateLoader {
|
|||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
fn has_pending(&self) -> bool {
|
||||
self.cache.lock().values().any(|result| result.is_pending())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ document-features = { workspace = true, optional = true }
|
|||
[dev-dependencies]
|
||||
egui = { workspace = true, features = ["default_fonts"] }
|
||||
image = { workspace = true, features = ["png"] }
|
||||
egui_extras = { workspace = true, features = ["image"] }
|
||||
egui_extras = { workspace = true, features = ["image", "http"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ pub struct HarnessBuilder<State = ()> {
|
|||
pub(crate) step_dt: f32,
|
||||
pub(crate) state: PhantomData<State>,
|
||||
pub(crate) renderer: Box<dyn TestRenderer>,
|
||||
pub(crate) wait_for_pending_images: bool,
|
||||
}
|
||||
|
||||
impl<State> Default for HarnessBuilder<State> {
|
||||
|
|
@ -22,6 +23,7 @@ impl<State> Default for HarnessBuilder<State> {
|
|||
renderer: Box::new(LazyRenderer::default()),
|
||||
max_steps: 4,
|
||||
step_dt: 1.0 / 4.0,
|
||||
wait_for_pending_images: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -63,6 +65,19 @@ impl<State> HarnessBuilder<State> {
|
|||
self
|
||||
}
|
||||
|
||||
/// Should we wait for pending images?
|
||||
///
|
||||
/// If `true`, [`Harness::run`] and related methods will check if there are pending images
|
||||
/// (via [`egui::Context::has_pending_images`]) and sleep for [`Self::with_step_dt`] up to
|
||||
/// [`Self::with_max_steps`] times.
|
||||
///
|
||||
/// Default: `true`
|
||||
#[inline]
|
||||
pub fn with_wait_for_pending_images(mut self, wait_for_pending_images: bool) -> Self {
|
||||
self.wait_for_pending_images = wait_for_pending_images;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the [`TestRenderer`] to use for rendering.
|
||||
///
|
||||
/// By default, a [`LazyRenderer`] is used.
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ pub use renderer::*;
|
|||
use egui::{Modifiers, Pos2, Rect, RepaintCause, Vec2, ViewportId};
|
||||
use kittest::{Node, Queryable};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExceededMaxStepsError {
|
||||
pub max_steps: u64,
|
||||
pub repaint_causes: Vec<RepaintCause>,
|
||||
|
|
@ -66,6 +67,7 @@ pub struct Harness<'a, State = ()> {
|
|||
renderer: Box<dyn TestRenderer>,
|
||||
max_steps: u64,
|
||||
step_dt: f32,
|
||||
wait_for_pending_images: bool,
|
||||
}
|
||||
|
||||
impl<State> Debug for Harness<'_, State> {
|
||||
|
|
@ -88,6 +90,7 @@ impl<'a, State> Harness<'a, State> {
|
|||
step_dt,
|
||||
state: _,
|
||||
mut renderer,
|
||||
wait_for_pending_images,
|
||||
} = builder;
|
||||
let ctx = ctx.unwrap_or_default();
|
||||
ctx.enable_accesskit();
|
||||
|
|
@ -128,6 +131,7 @@ impl<'a, State> Harness<'a, State> {
|
|||
renderer,
|
||||
max_steps,
|
||||
step_dt,
|
||||
wait_for_pending_images,
|
||||
};
|
||||
// Run the harness until it is stable, ensuring that all Areas are shown and animations are done
|
||||
harness.run_ok();
|
||||
|
|
@ -293,10 +297,13 @@ impl<'a, State> Harness<'a, State> {
|
|||
loop {
|
||||
steps += 1;
|
||||
self.step();
|
||||
|
||||
let wait_for_images = self.wait_for_pending_images && self.ctx.has_pending_images();
|
||||
|
||||
// We only care about immediate repaints
|
||||
if self.root_viewport_output().repaint_delay != Duration::ZERO {
|
||||
if self.root_viewport_output().repaint_delay != Duration::ZERO && !wait_for_images {
|
||||
break;
|
||||
} else if sleep {
|
||||
} else if sleep || wait_for_images {
|
||||
std::thread::sleep(Duration::from_secs_f32(self.step_dt));
|
||||
}
|
||||
if steps > self.max_steps {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6298e67d099002808d51e7494e4174adee66d7ef2880728126c1d761d1432372
|
||||
size 2145
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
use egui::Modifiers;
|
||||
use egui::{include_image, Modifiers, Vec2};
|
||||
use egui_kittest::Harness;
|
||||
use kittest::{Key, Queryable as _};
|
||||
|
||||
|
|
@ -57,3 +57,29 @@ fn test_modifiers() {
|
|||
assert!(state.cmd_z_pressed, "Cmd+Z wasn't pressed");
|
||||
assert!(state.cmd_y_pressed, "Cmd+Y wasn't pressed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_wait_for_images() {
|
||||
let mut harness = Harness::builder()
|
||||
.with_size(Vec2::new(60.0, 120.0))
|
||||
.build_ui(|ui| {
|
||||
egui_extras::install_image_loaders(ui.ctx());
|
||||
let size = Vec2::splat(30.0);
|
||||
ui.label("Url:");
|
||||
ui.add_sized(
|
||||
size,
|
||||
egui::Image::new(
|
||||
"https://raw.githubusercontent.com\
|
||||
/emilk/egui/refs/heads/main/crates/eframe/data/icon.png",
|
||||
),
|
||||
);
|
||||
|
||||
ui.label("Include:");
|
||||
ui.add_sized(
|
||||
size,
|
||||
egui::Image::new(include_image!("../../eframe/data/icon.png")),
|
||||
);
|
||||
});
|
||||
|
||||
harness.snapshot("should_wait_for_images");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue