diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 11b05bc0..9dc70c92 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -3546,6 +3546,14 @@ impl Context { pub fn loaders(&self) -> Arc { 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 diff --git a/crates/egui/src/load.rs b/crates/egui/src/load.rs index 6e30d4ca..2190992d 100644 --- a/crates/egui/src/load.rs +++ b/crates/egui/src/load.rs @@ -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. diff --git a/crates/egui_extras/src/loaders/ehttp_loader.rs b/crates/egui_extras/src/loaders/ehttp_loader.rs index 79c77694..22785eed 100644 --- a/crates/egui_extras/src/loaders/ehttp_loader.rs +++ b/crates/egui_extras/src/loaders/ehttp_loader.rs @@ -125,4 +125,8 @@ impl BytesLoader for EhttpLoader { }) .sum() } + + fn has_pending(&self) -> bool { + self.cache.lock().values().any(|entry| entry.is_pending()) + } } diff --git a/crates/egui_extras/src/loaders/file_loader.rs b/crates/egui_extras/src/loaders/file_loader.rs index 8b762887..9feaebf5 100644 --- a/crates/egui_extras/src/loaders/file_loader.rs +++ b/crates/egui_extras/src/loaders/file_loader.rs @@ -128,4 +128,8 @@ impl BytesLoader for FileLoader { }) .sum() } + + fn has_pending(&self) -> bool { + self.cache.lock().values().any(|entry| entry.is_pending()) + } } diff --git a/crates/egui_extras/src/loaders/image_loader.rs b/crates/egui_extras/src/loaders/image_loader.rs index 2528c2ae..7f472dcc 100644 --- a/crates/egui_extras/src/loaders/image_loader.rs +++ b/crates/egui_extras/src/loaders/image_loader.rs @@ -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, 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)] diff --git a/crates/egui_kittest/Cargo.toml b/crates/egui_kittest/Cargo.toml index d2434124..2638097e 100644 --- a/crates/egui_kittest/Cargo.toml +++ b/crates/egui_kittest/Cargo.toml @@ -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 diff --git a/crates/egui_kittest/src/builder.rs b/crates/egui_kittest/src/builder.rs index 4010b458..63f19c3c 100644 --- a/crates/egui_kittest/src/builder.rs +++ b/crates/egui_kittest/src/builder.rs @@ -11,6 +11,7 @@ pub struct HarnessBuilder { pub(crate) step_dt: f32, pub(crate) state: PhantomData, pub(crate) renderer: Box, + pub(crate) wait_for_pending_images: bool, } impl Default for HarnessBuilder { @@ -22,6 +23,7 @@ impl Default for HarnessBuilder { renderer: Box::new(LazyRenderer::default()), max_steps: 4, step_dt: 1.0 / 4.0, + wait_for_pending_images: true, } } } @@ -63,6 +65,19 @@ impl HarnessBuilder { 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. diff --git a/crates/egui_kittest/src/lib.rs b/crates/egui_kittest/src/lib.rs index 38521d10..0963a949 100644 --- a/crates/egui_kittest/src/lib.rs +++ b/crates/egui_kittest/src/lib.rs @@ -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, @@ -66,6 +67,7 @@ pub struct Harness<'a, State = ()> { renderer: Box, max_steps: u64, step_dt: f32, + wait_for_pending_images: bool, } impl 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 { diff --git a/crates/egui_kittest/tests/snapshots/should_wait_for_images.png b/crates/egui_kittest/tests/snapshots/should_wait_for_images.png new file mode 100644 index 00000000..c6ee32d1 --- /dev/null +++ b/crates/egui_kittest/tests/snapshots/should_wait_for_images.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6298e67d099002808d51e7494e4174adee66d7ef2880728126c1d761d1432372 +size 2145 diff --git a/crates/egui_kittest/tests/tests.rs b/crates/egui_kittest/tests/tests.rs index 9bf07063..2b223f45 100644 --- a/crates/egui_kittest/tests/tests.rs +++ b/crates/egui_kittest/tests/tests.rs @@ -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"); +}