From 46b58e5bcca0bb34861b5671958be872419bee90 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 2 Jan 2025 17:48:39 +0100 Subject: [PATCH] Add `Harness::new_eframe` and `TestRenderer` trait (#5539) Co-authored-by: Andreas Reich --- Cargo.lock | 2 + crates/eframe/src/epi.rs | 67 +++++- crates/eframe/src/web/web_painter_wgpu.rs | 2 +- crates/egui-wgpu/src/lib.rs | 15 +- crates/egui-wgpu/src/winit.rs | 2 +- crates/egui_demo_app/Cargo.toml | 3 + crates/egui_demo_app/src/apps/image_viewer.rs | 5 +- crates/egui_demo_app/src/lib.rs | 2 +- crates/egui_demo_app/src/wrap_app.rs | 13 +- .../egui_demo_app/tests/snapshots/clock.png | 3 + .../tests/snapshots/custom3d.png | 3 + .../tests/snapshots/easymarkeditor.png | 3 + .../tests/snapshots/imageviewer.png | 3 + crates/egui_demo_app/tests/test_demo_app.rs | 78 +++++++ .../src/demo/demo_app_windows.rs | 2 +- crates/egui_demo_lib/src/demo/modals.rs | 8 +- .../egui_demo_lib/src/demo/widget_gallery.rs | 2 +- crates/egui_demo_lib/src/rendering_test.rs | 2 +- crates/egui_kittest/Cargo.toml | 6 +- crates/egui_kittest/README.md | 4 +- crates/egui_kittest/src/app_kind.rs | 43 ++-- crates/egui_kittest/src/builder.rs | 59 ++++- crates/egui_kittest/src/lib.rs | 49 ++++- crates/egui_kittest/src/renderer.rs | 81 +++++++ crates/egui_kittest/src/snapshot.rs | 101 +++++++-- crates/egui_kittest/src/wgpu.rs | 207 ++++++++++-------- crates/egui_kittest/tests/regression_tests.rs | 2 +- crates/egui_kittest/tests/tests.rs | 2 +- 28 files changed, 591 insertions(+), 178 deletions(-) create mode 100644 crates/egui_demo_app/tests/snapshots/clock.png create mode 100644 crates/egui_demo_app/tests/snapshots/custom3d.png create mode 100644 crates/egui_demo_app/tests/snapshots/easymarkeditor.png create mode 100644 crates/egui_demo_app/tests/snapshots/imageviewer.png create mode 100644 crates/egui_demo_app/tests/test_demo_app.rs create mode 100644 crates/egui_kittest/src/renderer.rs diff --git a/Cargo.lock b/Cargo.lock index 7e11391f1c2..009aeb73130 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1319,6 +1319,7 @@ dependencies = [ "egui", "egui_demo_lib", "egui_extras", + "egui_kittest", "ehttp", "env_logger", "image", @@ -1395,6 +1396,7 @@ version = "0.30.0" dependencies = [ "dify", "document-features", + "eframe", "egui", "egui-wgpu", "image", diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index 45d8335018e..7a7e9224444 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -109,6 +109,28 @@ impl HasDisplayHandle for CreationContext<'_> { } } +impl CreationContext<'_> { + /// Create a new empty [CreationContext] for testing [App]s in kittest. + #[doc(hidden)] + pub fn _new_kittest(egui_ctx: egui::Context) -> Self { + Self { + egui_ctx, + integration_info: IntegrationInfo::mock(), + storage: None, + #[cfg(feature = "glow")] + gl: None, + #[cfg(feature = "glow")] + get_proc_address: None, + #[cfg(feature = "wgpu")] + wgpu_render_state: None, + #[cfg(not(target_arch = "wasm32"))] + raw_window_handle: Err(HandleError::NotSupported), + #[cfg(not(target_arch = "wasm32"))] + raw_display_handle: Err(HandleError::NotSupported), + } + } +} + // ---------------------------------------------------------------------------- /// Implement this trait to write apps that can be compiled for both web/wasm and desktop/native using [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe). @@ -617,7 +639,8 @@ pub struct Frame { /// Can be used to manage GPU resources for custom rendering with WGPU using [`egui::PaintCallback`]s. #[cfg(feature = "wgpu")] - pub(crate) wgpu_render_state: Option, + #[doc(hidden)] + pub wgpu_render_state: Option, /// Raw platform window handle #[cfg(not(target_arch = "wasm32"))] @@ -651,6 +674,25 @@ impl HasDisplayHandle for Frame { } impl Frame { + /// Create a new empty [Frame] for testing [App]s in kittest. + #[doc(hidden)] + pub fn _new_kittest() -> Self { + Self { + #[cfg(feature = "glow")] + gl: None, + #[cfg(all(feature = "glow", not(target_arch = "wasm32")))] + glow_register_native_texture: None, + info: IntegrationInfo::mock(), + #[cfg(not(target_arch = "wasm32"))] + raw_display_handle: Err(HandleError::NotSupported), + #[cfg(not(target_arch = "wasm32"))] + raw_window_handle: Err(HandleError::NotSupported), + storage: None, + #[cfg(feature = "wgpu")] + wgpu_render_state: None, + } + } + /// True if you are in a web environment. /// /// Equivalent to `cfg!(target_arch = "wasm32")` @@ -794,6 +836,29 @@ pub struct IntegrationInfo { pub cpu_usage: Option, } +impl IntegrationInfo { + fn mock() -> Self { + Self { + #[cfg(target_arch = "wasm32")] + web_info: WebInfo { + user_agent: "kittest".to_owned(), + location: Location { + url: "http://localhost".to_owned(), + protocol: "http:".to_owned(), + host: "localhost".to_owned(), + hostname: "localhost".to_owned(), + port: "80".to_owned(), + hash: String::new(), + query: String::new(), + query_map: Default::default(), + origin: "http://localhost".to_owned(), + }, + }, + cpu_usage: None, + } + } +} + // ---------------------------------------------------------------------------- /// A place where you can store custom data in a way that persists when you restart the app. diff --git a/crates/eframe/src/web/web_painter_wgpu.rs b/crates/eframe/src/web/web_painter_wgpu.rs index 591d4224d3b..5e217180ce3 100644 --- a/crates/eframe/src/web/web_painter_wgpu.rs +++ b/crates/eframe/src/web/web_painter_wgpu.rs @@ -115,7 +115,7 @@ impl WebPainterWgpu { let render_state = RenderState::create( &options.wgpu_options, &instance, - &surface, + Some(&surface), depth_format, 1, options.dithering, diff --git a/crates/egui-wgpu/src/lib.rs b/crates/egui-wgpu/src/lib.rs index a91662f45b1..b90e520018c 100644 --- a/crates/egui-wgpu/src/lib.rs +++ b/crates/egui-wgpu/src/lib.rs @@ -24,7 +24,7 @@ pub use wgpu; mod renderer; pub use renderer::*; -use wgpu::{Adapter, Device, Instance, Queue}; +use wgpu::{Adapter, Device, Instance, Queue, TextureFormat}; /// Helpers for capturing screenshots of the UI. pub mod capture; @@ -91,7 +91,7 @@ impl RenderState { pub async fn create( config: &WgpuConfiguration, instance: &wgpu::Instance, - surface: &wgpu::Surface<'static>, + compatible_surface: Option<&wgpu::Surface<'static>>, depth_format: Option, msaa_samples: u32, dithering: bool, @@ -113,7 +113,7 @@ impl RenderState { instance .request_adapter(&wgpu::RequestAdapterOptions { power_preference, - compatible_surface: Some(surface), + compatible_surface, force_fallback_adapter: false, }) .await @@ -186,11 +186,14 @@ impl RenderState { } => (adapter, device, queue), }; - let capabilities = { + let surface_formats = { profiling::scope!("get_capabilities"); - surface.get_capabilities(&adapter).formats + compatible_surface.map_or_else( + || vec![TextureFormat::Rgba8Unorm], + |s| s.get_capabilities(&adapter).formats, + ) }; - let target_format = crate::preferred_framebuffer_format(&capabilities)?; + let target_format = crate::preferred_framebuffer_format(&surface_formats)?; let renderer = Renderer::new( &device, diff --git a/crates/egui-wgpu/src/winit.rs b/crates/egui-wgpu/src/winit.rs index dea2e7fa329..19be186f226 100644 --- a/crates/egui-wgpu/src/winit.rs +++ b/crates/egui-wgpu/src/winit.rs @@ -212,7 +212,7 @@ impl Painter { let render_state = RenderState::create( &self.configuration, &self.instance, - &surface, + Some(&surface), self.depth_format, self.msaa_samples, self.dithering, diff --git a/crates/egui_demo_app/Cargo.toml b/crates/egui_demo_app/Cargo.toml index c3083d11caf..3115c845e3d 100644 --- a/crates/egui_demo_app/Cargo.toml +++ b/crates/egui_demo_app/Cargo.toml @@ -93,3 +93,6 @@ rfd = { version = "0.15", optional = true } wasm-bindgen = "=0.2.95" wasm-bindgen-futures.workspace = true web-sys.workspace = true + +[dev-dependencies] +egui_kittest = { workspace = true, features = ["eframe", "snapshot", "wgpu"] } \ No newline at end of file diff --git a/crates/egui_demo_app/src/apps/image_viewer.rs b/crates/egui_demo_app/src/apps/image_viewer.rs index 80961915eb5..326718a235e 100644 --- a/crates/egui_demo_app/src/apps/image_viewer.rs +++ b/crates/egui_demo_app/src/apps/image_viewer.rs @@ -54,8 +54,9 @@ impl eframe::App for ImageViewer { fn update(&mut self, ctx: &egui::Context, _: &mut eframe::Frame) { egui::TopBottomPanel::new(TopBottomSide::Top, "url bar").show(ctx, |ui| { ui.horizontal_centered(|ui| { - ui.label("URI:"); - ui.text_edit_singleline(&mut self.uri_edit_text); + let label = ui.label("URI:"); + ui.text_edit_singleline(&mut self.uri_edit_text) + .labelled_by(label.id); if ui.small_button("✔").clicked() { ctx.forget_image(&self.current_uri); self.uri_edit_text = self.uri_edit_text.trim().to_owned(); diff --git a/crates/egui_demo_app/src/lib.rs b/crates/egui_demo_app/src/lib.rs index aa220f2b609..0eb34486dd1 100644 --- a/crates/egui_demo_app/src/lib.rs +++ b/crates/egui_demo_app/src/lib.rs @@ -6,7 +6,7 @@ mod backend_panel; mod frame_history; mod wrap_app; -pub use wrap_app::WrapApp; +pub use wrap_app::{Anchor, WrapApp}; /// Time of day as seconds since midnight. Used for clock in demo app. pub(crate) fn seconds_since_midnight() -> f64 { diff --git a/crates/egui_demo_app/src/wrap_app.rs b/crates/egui_demo_app/src/wrap_app.rs index cbed988fa68..117824e0174 100644 --- a/crates/egui_demo_app/src/wrap_app.rs +++ b/crates/egui_demo_app/src/wrap_app.rs @@ -38,6 +38,7 @@ impl eframe::App for DemoApp { #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct FractalClockApp { fractal_clock: crate::apps::FractalClock, + pub mock_time: Option, } impl eframe::App for FractalClockApp { @@ -46,7 +47,7 @@ impl eframe::App for FractalClockApp { .frame(egui::Frame::dark_canvas(&ctx.style())) .show(ctx, |ui| { self.fractal_clock - .ui(ui, Some(crate::seconds_since_midnight())); + .ui(ui, self.mock_time.or(Some(crate::seconds_since_midnight()))); }); } } @@ -77,7 +78,7 @@ impl eframe::App for ColorTestApp { #[derive(Copy, Clone, Debug, Eq, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -enum Anchor { +pub enum Anchor { Demo, EasyMarkEditor, @@ -161,7 +162,7 @@ pub struct State { http: crate::apps::HttpApp, #[cfg(feature = "image_viewer")] image_viewer: crate::apps::ImageViewer, - clock: FractalClockApp, + pub clock: FractalClockApp, rendering_test: ColorTestApp, selected_anchor: Anchor, @@ -170,7 +171,7 @@ pub struct State { /// Wraps many demo/test apps into one. pub struct WrapApp { - state: State, + pub state: State, #[cfg(any(feature = "glow", feature = "wgpu"))] custom3d: Option, @@ -203,7 +204,9 @@ impl WrapApp { slf } - fn apps_iter_mut(&mut self) -> impl Iterator { + pub fn apps_iter_mut( + &mut self, + ) -> impl Iterator { let mut vec = vec![ ( "✨ Demos", diff --git a/crates/egui_demo_app/tests/snapshots/clock.png b/crates/egui_demo_app/tests/snapshots/clock.png new file mode 100644 index 00000000000..51d271ddd49 --- /dev/null +++ b/crates/egui_demo_app/tests/snapshots/clock.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c05cc3d48242e46a391af34cb56f72de7933bf2cead009b6cd477c21867a84e +size 327802 diff --git a/crates/egui_demo_app/tests/snapshots/custom3d.png b/crates/egui_demo_app/tests/snapshots/custom3d.png new file mode 100644 index 00000000000..1e51e9f6aa0 --- /dev/null +++ b/crates/egui_demo_app/tests/snapshots/custom3d.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:61212e30fe1fecf5891ddad6ac795df510bfad76b21a7a8a13aa024fdad6d05e +size 93118 diff --git a/crates/egui_demo_app/tests/snapshots/easymarkeditor.png b/crates/egui_demo_app/tests/snapshots/easymarkeditor.png new file mode 100644 index 00000000000..23e1c59b00a --- /dev/null +++ b/crates/egui_demo_app/tests/snapshots/easymarkeditor.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7bcf6e2977bed682d7bdaa0b6a6786e528662dd0791d2e6f83cf1b4852035838 +size 182833 diff --git a/crates/egui_demo_app/tests/snapshots/imageviewer.png b/crates/egui_demo_app/tests/snapshots/imageviewer.png new file mode 100644 index 00000000000..1176b2d3304 --- /dev/null +++ b/crates/egui_demo_app/tests/snapshots/imageviewer.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e6cc6ff64eb73ddac89ecdacd07c2176f3ab952c0db4593fccf6d11f155ec392 +size 103100 diff --git a/crates/egui_demo_app/tests/test_demo_app.rs b/crates/egui_demo_app/tests/test_demo_app.rs new file mode 100644 index 00000000000..b242dad4316 --- /dev/null +++ b/crates/egui_demo_app/tests/test_demo_app.rs @@ -0,0 +1,78 @@ +use egui::accesskit::Role; +use egui::Vec2; +use egui_demo_app::{Anchor, WrapApp}; +use egui_kittest::kittest::Queryable; + +#[test] +fn test_demo_app() { + let mut harness = egui_kittest::Harness::builder() + .with_size(Vec2::new(900.0, 600.0)) + .wgpu() + .build_eframe(|cc| WrapApp::new(cc)); + + let app = harness.state_mut(); + + // Mock the fractal clock time so snapshots are consistent. + app.state.clock.mock_time = Some(36383.0); + + let apps = app + .apps_iter_mut() + .map(|(name, anchor, _)| (name, anchor)) + .collect::>(); + + #[cfg(feature = "wgpu")] + assert!( + apps.iter() + .any(|(_, anchor)| matches!(anchor, Anchor::Custom3d)), + "Expected to find the Custom3d app.", + ); + + let mut results = vec![]; + + for (name, anchor) in apps { + harness.get_by_role_and_label(Role::Button, name).click(); + + match anchor { + // The widget gallery demo shows the current date, so we can't use it for snapshot testing + Anchor::Demo => { + continue; + } + // This is already tested extensively elsewhere + Anchor::Rendering => { + continue; + } + // We don't want to rely on a network connection for tests + #[cfg(feature = "http")] + Anchor::Http => { + continue; + } + // Load a local image where we know it exists and loads quickly + #[cfg(feature = "image_viewer")] + Anchor::ImageViewer => { + harness.run(); + + harness + .get_by_role_and_label(Role::TextInput, "URI:") + .focus(); + harness.press_key_modifiers(egui::Modifiers::COMMAND, egui::Key::A); + + harness + .get_by_role_and_label(Role::TextInput, "URI:") + .type_text("file://../eframe/data/icon.png"); + + harness.get_by_role_and_label(Role::Button, "✔").click(); + } + _ => {} + } + + harness.run(); + + if let Err(e) = harness.try_snapshot(&anchor.to_string()) { + results.push(e); + } + } + + if let Some(error) = results.first() { + panic!("{error}"); + } +} diff --git a/crates/egui_demo_lib/src/demo/demo_app_windows.rs b/crates/egui_demo_lib/src/demo/demo_app_windows.rs index 7e4891e1f78..9cf01e9eac6 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -405,7 +405,7 @@ mod tests { options.threshold = 2.1; } - let result = harness.try_wgpu_snapshot_options(&format!("demos/{name}"), &options); + let result = harness.try_snapshot_options(&format!("demos/{name}"), &options); if let Err(err) = result { errors.push(err.to_string()); } diff --git a/crates/egui_demo_lib/src/demo/modals.rs b/crates/egui_demo_lib/src/demo/modals.rs index 989101b4d75..8d22c0cc008 100644 --- a/crates/egui_demo_lib/src/demo/modals.rs +++ b/crates/egui_demo_lib/src/demo/modals.rs @@ -235,21 +235,21 @@ mod tests { let mut results = Vec::new(); harness.run(); - results.push(harness.try_wgpu_snapshot("modals_1")); + results.push(harness.try_snapshot("modals_1")); harness.get_by_label("Save").click(); // TODO(lucasmerlin): Remove these extra runs once run checks for repaint requests harness.run(); harness.run(); harness.run(); - results.push(harness.try_wgpu_snapshot("modals_2")); + results.push(harness.try_snapshot("modals_2")); harness.get_by_label("Yes Please").click(); // TODO(lucasmerlin): Remove these extra runs once run checks for repaint requests harness.run(); harness.run(); harness.run(); - results.push(harness.try_wgpu_snapshot("modals_3")); + results.push(harness.try_snapshot("modals_3")); for result in results { result.unwrap(); @@ -282,6 +282,6 @@ mod tests { harness.run(); // This snapshots should show the progress bar modal on top of the save modal. - harness.wgpu_snapshot("modals_backdrop_should_prevent_focusing_lower_area"); + harness.snapshot("modals_backdrop_should_prevent_focusing_lower_area"); } } diff --git a/crates/egui_demo_lib/src/demo/widget_gallery.rs b/crates/egui_demo_lib/src/demo/widget_gallery.rs index d473d4b9d0e..cfe746899cb 100644 --- a/crates/egui_demo_lib/src/demo/widget_gallery.rs +++ b/crates/egui_demo_lib/src/demo/widget_gallery.rs @@ -307,6 +307,6 @@ mod tests { harness.fit_contents(); - harness.wgpu_snapshot("widget_gallery"); + harness.snapshot("widget_gallery"); } } diff --git a/crates/egui_demo_lib/src/rendering_test.rs b/crates/egui_demo_lib/src/rendering_test.rs index 011ca473623..9d4c7985f2d 100644 --- a/crates/egui_demo_lib/src/rendering_test.rs +++ b/crates/egui_demo_lib/src/rendering_test.rs @@ -703,7 +703,7 @@ mod tests { harness.fit_contents(); - let result = harness.try_wgpu_snapshot(&format!("rendering_test/dpi_{dpi:.2}")); + let result = harness.try_snapshot(&format!("rendering_test/dpi_{dpi:.2}")); if let Err(err) = result { errors.push(err); } diff --git a/crates/egui_kittest/Cargo.toml b/crates/egui_kittest/Cargo.toml index d93f0348368..4f52d4aef71 100644 --- a/crates/egui_kittest/Cargo.toml +++ b/crates/egui_kittest/Cargo.toml @@ -20,15 +20,19 @@ include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"] [features] # Adds a wgpu-based test renderer. -wgpu = ["dep:egui-wgpu", "dep:pollster", "dep:image"] +wgpu = ["dep:egui-wgpu", "dep:pollster", "dep:image", "eframe?/wgpu"] # Adds a dify-based image snapshot utility. snapshot = ["dep:dify", "dep:image", "image/png"] +# Allows testing eframe::App +eframe = ["dep:eframe", "eframe/accesskit"] + [dependencies] kittest.workspace = true egui = { workspace = true, features = ["accesskit"] } +eframe = { workspace = true, optional = true } # wgpu dependencies egui-wgpu = { workspace = true, optional = true } diff --git a/crates/egui_kittest/README.md b/crates/egui_kittest/README.md index c124fac3a62..99fe9be6543 100644 --- a/crates/egui_kittest/README.md +++ b/crates/egui_kittest/README.md @@ -29,13 +29,13 @@ fn main() { // You can even render the ui and do image snapshot tests #[cfg(all(feature = "wgpu", feature = "snapshot"))] - harness.wgpu_snapshot("readme_example"); + harness.snapshot("readme_example"); } ``` ## Snapshot testing There is a snapshot testing feature. To create snapshot tests, enable the `snapshot` and `wgpu` features. -Once enabled, you can call `Harness::wgpu_snapshot` to render the ui and save the image to the `tests/snapshots` directory. +Once enabled, you can call `Harness::snapshot` to render the ui and save the image to the `tests/snapshots` directory. To update the snapshots, run your tests with `UPDATE_SNAPSHOTS=true`, so e.g. `UPDATE_SNAPSHOTS=true cargo test`. Running with `UPDATE_SNAPSHOTS=true` will still cause the tests to fail, but on the next run, the tests should pass. diff --git a/crates/egui_kittest/src/app_kind.rs b/crates/egui_kittest/src/app_kind.rs index 8a180b3b93b..3b5cf9e734c 100644 --- a/crates/egui_kittest/src/app_kind.rs +++ b/crates/egui_kittest/src/app_kind.rs @@ -5,37 +5,22 @@ type AppKindUiState<'a, State> = Box; type AppKindContext<'a> = Box; type AppKindUi<'a> = Box; +/// In order to access the [`eframe::App`] trait from the generic `State`, we store a function pointer +/// here that will return the dyn trait from the struct. In the builder we have the correct where +/// clause to be able to create this. +/// Later we can use it anywhere to get the [`eframe::App`] from the `State`. +#[cfg(feature = "eframe")] +type AppKindEframe<'a, State> = (fn(&mut State) -> &mut dyn eframe::App, eframe::Frame); + pub(crate) enum AppKind<'a, State> { Context(AppKindContext<'a>), Ui(AppKindUi<'a>), ContextState(AppKindContextState<'a, State>), UiState(AppKindUiState<'a, State>), + #[cfg(feature = "eframe")] + Eframe(AppKindEframe<'a, State>), } -// TODO(lucasmerlin): These aren't working unfortunately :( -// I think they should work though: https://geo-ant.github.io/blog/2021/rust-traits-and-variadic-functions/ -// pub trait IntoAppKind<'a, UiKind> { -// fn into_harness_kind(self) -> AppKind<'a>; -// } -// -// impl<'a, F> IntoAppKind<'a, &egui::Context> for F -// where -// F: FnMut(&egui::Context) + 'a, -// { -// fn into_harness_kind(self) -> AppKind<'a> { -// AppKind::Context(Box::new(self)) -// } -// } -// -// impl<'a, F> IntoAppKind<'a, &mut egui::Ui> for F -// where -// F: FnMut(&mut egui::Ui) + 'a, -// { -// fn into_harness_kind(self) -> AppKind<'a> { -// AppKind::Ui(Box::new(self)) -// } -// } - impl<'a, State> AppKind<'a, State> { pub fn run( &mut self, @@ -54,6 +39,12 @@ impl<'a, State> AppKind<'a, State> { f(ctx, state); None } + #[cfg(feature = "eframe")] + AppKind::Eframe((get_app, frame)) => { + let app = get_app(state); + app.update(ctx, frame); + None + } kind_ui => Some(kind_ui.run_ui(ctx, state, sizing_pass)), } } @@ -78,7 +69,9 @@ impl<'a, State> AppKind<'a, State> { .show(ui, |ui| match self { AppKind::Ui(f) => f(ui), AppKind::UiState(f) => f(ui, state), - _ => unreachable!(), + _ => unreachable!( + "run_ui should only be called with AppKind::Ui or AppKind UiState" + ), }); }) .response diff --git a/crates/egui_kittest/src/builder.rs b/crates/egui_kittest/src/builder.rs index 65c4dfbf0f6..d0c219bb156 100644 --- a/crates/egui_kittest/src/builder.rs +++ b/crates/egui_kittest/src/builder.rs @@ -1,5 +1,6 @@ use crate::app_kind::AppKind; -use crate::Harness; +use crate::wgpu::WgpuTestRenderer; +use crate::{Harness, LazyRenderer, TestRenderer}; use egui::{Pos2, Rect, Vec2}; use std::marker::PhantomData; @@ -8,6 +9,7 @@ pub struct HarnessBuilder { pub(crate) screen_rect: Rect, pub(crate) pixels_per_point: f32, pub(crate) state: PhantomData, + pub(crate) renderer: Box, } impl Default for HarnessBuilder { @@ -16,6 +18,7 @@ impl Default for HarnessBuilder { screen_rect: Rect::from_min_size(Pos2::ZERO, Vec2::new(800.0, 600.0)), pixels_per_point: 1.0, state: PhantomData, + renderer: Box::new(LazyRenderer::default()), } } } @@ -37,6 +40,29 @@ impl HarnessBuilder { self } + /// Set the [`TestRenderer`] to use for rendering. + /// + /// By default, a [`LazyRenderer`] is used. + #[inline] + pub fn renderer(mut self, renderer: impl TestRenderer + 'static) -> Self { + self.renderer = Box::new(renderer); + self + } + + /// Enable wgpu rendering with a default setup suitable for testing. + /// + /// This sets up a [`WgpuTestRenderer`] with the default setup. + #[cfg(feature = "wgpu")] + pub fn wgpu(self) -> Self { + self.renderer(WgpuTestRenderer::default()) + } + + /// Enable wgpu rendering with the given setup. + #[cfg(feature = "wgpu")] + pub fn wgpu_setup(self, setup: egui_wgpu::WgpuSetup) -> Self { + self.renderer(WgpuTestRenderer::from_setup(setup)) + } + /// Create a new Harness with the given app closure and a state. /// /// The app closure will immediately be called once to create the initial ui. @@ -66,7 +92,7 @@ impl HarnessBuilder { app: impl FnMut(&egui::Context, &mut State) + 'a, state: State, ) -> Harness<'a, State> { - Harness::from_builder(&self, AppKind::ContextState(Box::new(app)), state) + Harness::from_builder(self, AppKind::ContextState(Box::new(app)), state, None) } /// Create a new Harness with the given ui closure and a state. @@ -95,7 +121,30 @@ impl HarnessBuilder { app: impl FnMut(&mut egui::Ui, &mut State) + 'a, state: State, ) -> Harness<'a, State> { - Harness::from_builder(&self, AppKind::UiState(Box::new(app)), state) + Harness::from_builder(self, AppKind::UiState(Box::new(app)), state, None) + } + + /// Create a new [Harness] from the given eframe creation closure. + /// The app can be accessed via the [`Harness::state`] / [`Harness::state_mut`] methods. + #[cfg(feature = "eframe")] + pub fn build_eframe<'a>( + self, + build: impl FnOnce(&mut eframe::CreationContext<'a>) -> State, + ) -> Harness<'a, State> + where + State: eframe::App, + { + let ctx = egui::Context::default(); + + let mut cc = eframe::CreationContext::_new_kittest(ctx.clone()); + let mut frame = eframe::Frame::_new_kittest(); + + self.renderer.setup_eframe(&mut cc, &mut frame); + + let app = build(&mut cc); + + let kind = AppKind::Eframe((|state| state, frame)); + Harness::from_builder(self, kind, app, Some(ctx)) } } @@ -119,7 +168,7 @@ impl HarnessBuilder { /// }); /// ``` pub fn build<'a>(self, app: impl FnMut(&egui::Context) + 'a) -> Harness<'a> { - Harness::from_builder(&self, AppKind::Context(Box::new(app)), ()) + Harness::from_builder(self, AppKind::Context(Box::new(app)), (), None) } /// Create a new Harness with the given ui closure. @@ -138,6 +187,6 @@ impl HarnessBuilder { /// }); /// ``` pub fn build_ui<'a>(self, app: impl FnMut(&mut egui::Ui) + 'a) -> Harness<'a> { - Harness::from_builder(&self, AppKind::Ui(Box::new(app)), ()) + Harness::from_builder(self, AppKind::Ui(Box::new(app)), (), None) } } diff --git a/crates/egui_kittest/src/lib.rs b/crates/egui_kittest/src/lib.rs index 03648aa16fe..83c6f548a38 100644 --- a/crates/egui_kittest/src/lib.rs +++ b/crates/egui_kittest/src/lib.rs @@ -12,18 +12,21 @@ mod snapshot; pub use snapshot::*; use std::fmt::{Debug, Formatter}; mod app_kind; +mod renderer; #[cfg(feature = "wgpu")] mod texture_to_image; #[cfg(feature = "wgpu")] pub mod wgpu; pub use kittest; -use std::mem; use crate::app_kind::AppKind; use crate::event::EventState; + pub use builder::*; -use egui::{Pos2, Rect, TexturesDelta, Vec2, ViewportId}; +pub use renderer::*; + +use egui::{Modifiers, Pos2, Rect, Vec2, ViewportId}; use kittest::{Node, Queryable}; /// The test Harness. This contains everything needed to run the test. @@ -37,11 +40,11 @@ pub struct Harness<'a, State = ()> { input: egui::RawInput, kittest: kittest::State, output: egui::FullOutput, - texture_deltas: Vec, app: AppKind<'a, State>, event_state: EventState, response: Option, state: State, + renderer: Box, } impl<'a, State> Debug for Harness<'a, State> { @@ -52,11 +55,12 @@ impl<'a, State> Debug for Harness<'a, State> { impl<'a, State> Harness<'a, State> { pub(crate) fn from_builder( - builder: &HarnessBuilder, + builder: HarnessBuilder, mut app: AppKind<'a, State>, mut state: State, + ctx: Option, ) -> Self { - let ctx = egui::Context::default(); + let ctx = ctx.unwrap_or_default(); ctx.enable_accesskit(); let mut input = egui::RawInput { screen_rect: Some(builder.screen_rect), @@ -73,6 +77,9 @@ impl<'a, State> Harness<'a, State> { response = app.run(ctx, &mut state, false); }); + let mut renderer = builder.renderer; + renderer.handle_delta(&output.textures_delta); + let mut harness = Self { app, ctx, @@ -84,11 +91,11 @@ impl<'a, State> Harness<'a, State> { .take() .expect("AccessKit was disabled"), ), - texture_deltas: vec![mem::take(&mut output.textures_delta)], output, response, event_state: EventState::default(), state, + renderer, }; // Run the harness until it is stable, ensuring that all Areas are shown and animations are done harness.run(); @@ -153,6 +160,15 @@ impl<'a, State> Harness<'a, State> { Self::builder().build_ui_state(app, state) } + /// Create a new [Harness] from the given eframe creation closure. + #[cfg(feature = "eframe")] + pub fn new_eframe(builder: impl FnOnce(&mut eframe::CreationContext<'a>) -> State) -> Self + where + State: eframe::App, + { + Self::builder().build_eframe(builder) + } + /// Set the size of the window. /// Note: If you only want to set the size once at the beginning, /// prefer using [`HarnessBuilder::with_size`]. @@ -194,8 +210,7 @@ impl<'a, State> Harness<'a, State> { .take() .expect("AccessKit was disabled"), ); - self.texture_deltas - .push(mem::take(&mut output.textures_delta)); + self.renderer.handle_delta(&output.textures_delta); self.output = output; } @@ -253,21 +268,35 @@ impl<'a, State> Harness<'a, State> { /// Press a key. /// This will create a key down event and a key up event. pub fn press_key(&mut self, key: egui::Key) { + self.press_key_modifiers(Modifiers::default(), key); + } + + /// Press a key with modifiers. + /// This will create a key down event and a key up event. + pub fn press_key_modifiers(&mut self, modifiers: Modifiers, key: egui::Key) { self.input.events.push(egui::Event::Key { key, pressed: true, - modifiers: Default::default(), + modifiers, repeat: false, physical_key: None, }); self.input.events.push(egui::Event::Key { key, pressed: false, - modifiers: Default::default(), + modifiers, repeat: false, physical_key: None, }); } + + /// Render the last output to an image. + /// + /// # Errors + /// Returns an error if the rendering fails. + pub fn render(&mut self) -> Result { + self.renderer.render(&self.ctx, &self.output) + } } /// Utilities for stateless harnesses. diff --git a/crates/egui_kittest/src/renderer.rs b/crates/egui_kittest/src/renderer.rs new file mode 100644 index 00000000000..cbd789a0c33 --- /dev/null +++ b/crates/egui_kittest/src/renderer.rs @@ -0,0 +1,81 @@ +use egui::{Context, FullOutput, TexturesDelta}; +use image::RgbaImage; + +pub trait TestRenderer { + /// We use this to pass the glow / wgpu render state to [`eframe::Frame`]. + #[cfg(feature = "eframe")] + fn setup_eframe(&self, _cc: &mut eframe::CreationContext<'_>, _frame: &mut eframe::Frame) {} + + /// Handle a [`TexturesDelta`] by updating the renderer's textures. + fn handle_delta(&mut self, delta: &TexturesDelta); + + /// Render the [`crate::Harness`] and return the resulting image. + /// + /// # Errors + /// Returns an error if the rendering fails. + fn render(&mut self, ctx: &Context, output: &FullOutput) -> Result; +} + +/// A lazy renderer that initializes the renderer on the first render call. +/// +/// By default, this will create a wgpu renderer if the wgpu feature is enabled. +pub enum LazyRenderer { + Uninitialized { + texture_ops: Vec, + builder: Option Box>>, + }, + Initialized { + renderer: Box, + }, +} + +impl Default for LazyRenderer { + fn default() -> Self { + #[cfg(feature = "wgpu")] + return Self::new(crate::wgpu::WgpuTestRenderer::new); + #[cfg(not(feature = "wgpu"))] + return Self::Uninitialized { + texture_ops: Vec::new(), + builder: None, + }; + } +} + +impl LazyRenderer { + pub fn new(create_renderer: impl FnOnce() -> T + 'static) -> Self { + Self::Uninitialized { + texture_ops: Vec::new(), + builder: Some(Box::new(move || Box::new(create_renderer()))), + } + } +} + +impl TestRenderer for LazyRenderer { + fn handle_delta(&mut self, delta: &TexturesDelta) { + match self { + Self::Uninitialized { texture_ops, .. } => texture_ops.push(delta.clone()), + Self::Initialized { renderer } => renderer.handle_delta(delta), + } + } + + fn render(&mut self, ctx: &Context, output: &FullOutput) -> Result { + match self { + Self::Uninitialized { + texture_ops, + builder: build, + } => { + let mut renderer = build.take().ok_or({ + "No default renderer available. \ + Enable the wgpu feature or set one via HarnessBuilder::renderer" + })?(); + for delta in texture_ops.drain(..) { + renderer.handle_delta(&delta); + } + let image = renderer.render(ctx, output)?; + *self = Self::Initialized { renderer }; + Ok(image) + } + Self::Initialized { renderer } => renderer.render(ctx, output), + } + } +} diff --git a/crates/egui_kittest/src/snapshot.rs b/crates/egui_kittest/src/snapshot.rs index f4d480b64f7..e56a2285f06 100644 --- a/crates/egui_kittest/src/snapshot.rs +++ b/crates/egui_kittest/src/snapshot.rs @@ -93,6 +93,12 @@ pub enum SnapshotError { /// The error that occurred err: ImageError, }, + + /// Error rendering the image + RenderError { + /// The error that occurred + err: String, + }, } const HOW_TO_UPDATE_SCREENSHOTS: &str = @@ -142,6 +148,9 @@ impl Display for SnapshotError { let path = std::path::absolute(path).unwrap_or(path.clone()); write!(f, "Error writing snapshot: {err:?}\nAt: {path:?}") } + Self::RenderError { err } => { + write!(f, "Error rendering image: {err:?}") + } } } } @@ -315,7 +324,7 @@ pub fn image_snapshot(current: &image::RgbaImage, name: &str) { #[cfg(feature = "wgpu")] impl Harness<'_, State> { - /// Render a image using a default [`crate::wgpu::TestRenderer`] and compare it to the snapshot + /// Render a image using the setup [`crate::TestRenderer`] and compare it to the snapshot /// with custom options. /// /// If you want to change the default options for your whole project, you could create an @@ -323,7 +332,7 @@ impl Harness<'_, State> { /// new `my_image_snapshot` function on the Harness that calls this function with the desired options. /// You could additionally use the /// [disallowed_methods](https://rust-lang.github.io/rust-clippy/master/#disallowed_methods) - /// lint to disable use of the [`Harness::wgpu_snapshot`] to prevent accidentally using the wrong defaults. + /// lint to disable use of the [`Harness::snapshot`] to prevent accidentally using the wrong defaults. /// /// The snapshot files will be saved under [`SnapshotOptions::output_path`]. /// The snapshot will be saved under `{output_path}/{name}.png`. @@ -331,31 +340,35 @@ impl Harness<'_, State> { /// If new image didn't match the snapshot, a diff image will be saved under `{output_path}/{name}.diff.png`. /// /// # Errors - /// Returns a [`SnapshotError`] if the image does not match the snapshot or if there was an error - /// reading or writing the snapshot. - pub fn try_wgpu_snapshot_options( - &self, + /// Returns a [`SnapshotError`] if the image does not match the snapshot, if there was an + /// error reading or writing the snapshot, if the rendering fails or if no default renderer is available. + pub fn try_snapshot_options( + &mut self, name: &str, options: &SnapshotOptions, ) -> Result<(), SnapshotError> { - let image = crate::wgpu::TestRenderer::new().render(self); + let image = self + .render() + .map_err(|err| SnapshotError::RenderError { err })?; try_image_snapshot_options(&image, name, options) } - /// Render a image using a default [`crate::wgpu::TestRenderer`] and compare it to the snapshot. + /// Render a image using the setup [`crate::TestRenderer`] and compare it to the snapshot. /// The snapshot will be saved under `tests/snapshots/{name}.png`. /// The new image from the last test run will be saved under `tests/snapshots/{name}.new.png`. /// If new image didn't match the snapshot, a diff image will be saved under `tests/snapshots/{name}.diff.png`. /// /// # Errors - /// Returns a [`SnapshotError`] if the image does not match the snapshot or if there was an error - /// reading or writing the snapshot. - pub fn try_wgpu_snapshot(&self, name: &str) -> Result<(), SnapshotError> { - let image = crate::wgpu::TestRenderer::new().render(self); + /// Returns a [`SnapshotError`] if the image does not match the snapshot, if there was an + /// error reading or writing the snapshot, if the rendering fails or if no default renderer is available. + pub fn try_snapshot(&mut self, name: &str) -> Result<(), SnapshotError> { + let image = self + .render() + .map_err(|err| SnapshotError::RenderError { err })?; try_image_snapshot(&image, name) } - /// Render a image using a default [`crate::wgpu::TestRenderer`] and compare it to the snapshot + /// Render a image using the setup [`crate::TestRenderer`] and compare it to the snapshot /// with custom options. /// /// If you want to change the default options for your whole project, you could create an @@ -363,7 +376,7 @@ impl Harness<'_, State> { /// new `my_image_snapshot` function on the Harness that calls this function with the desired options. /// You could additionally use the /// [disallowed_methods](https://rust-lang.github.io/rust-clippy/master/#disallowed_methods) - /// lint to disable use of the [`Harness::wgpu_snapshot`] to prevent accidentally using the wrong defaults. + /// lint to disable use of the [`Harness::snapshot`] to prevent accidentally using the wrong defaults. /// /// The snapshot files will be saved under [`SnapshotOptions::output_path`]. /// The snapshot will be saved under `{output_path}/{name}.png`. @@ -371,11 +384,11 @@ impl Harness<'_, State> { /// If new image didn't match the snapshot, a diff image will be saved under `{output_path}/{name}.diff.png`. /// /// # Panics - /// Panics if the image does not match the snapshot or if there was an error reading or writing the - /// snapshot. + /// Panics if the image does not match the snapshot, if there was an error reading or writing the + /// snapshot, if the rendering fails or if no default renderer is available. #[track_caller] - pub fn wgpu_snapshot_options(&self, name: &str, options: &SnapshotOptions) { - match self.try_wgpu_snapshot_options(name, options) { + pub fn snapshot_options(&mut self, name: &str, options: &SnapshotOptions) { + match self.try_snapshot_options(name, options) { Ok(_) => {} Err(err) => { panic!("{}", err); @@ -383,17 +396,17 @@ impl Harness<'_, State> { } } - /// Render a image using a default [`crate::wgpu::TestRenderer`] and compare it to the snapshot. + /// Render a image using the setup [`crate::TestRenderer`] and compare it to the snapshot. /// The snapshot will be saved under `tests/snapshots/{name}.png`. /// The new image from the last test run will be saved under `tests/snapshots/{name}.new.png`. /// If new image didn't match the snapshot, a diff image will be saved under `tests/snapshots/{name}.diff.png`. /// /// # Panics - /// Panics if the image does not match the snapshot or if there was an error reading or writing the - /// snapshot. + /// Panics if the image does not match the snapshot, if there was an error reading or writing the + /// snapshot, if the rendering fails or if no default renderer is available. #[track_caller] - pub fn wgpu_snapshot(&self, name: &str) { - match self.try_wgpu_snapshot(name) { + pub fn snapshot(&mut self, name: &str) { + match self.try_snapshot(name) { Ok(_) => {} Err(err) => { panic!("{}", err); @@ -401,3 +414,45 @@ impl Harness<'_, State> { } } } + +// Deprecated wgpu_snapshot functions +// TODO(lucasmerlin): Remove in 0.32 +#[allow(clippy::missing_errors_doc)] +#[cfg(feature = "wgpu")] +impl Harness<'_, State> { + #[deprecated( + since = "0.31.0", + note = "Use `try_snapshot_options` instead. This function will be removed in 0.32" + )] + pub fn try_wgpu_snapshot_options( + &mut self, + name: &str, + options: &SnapshotOptions, + ) -> Result<(), SnapshotError> { + self.try_snapshot_options(name, options) + } + + #[deprecated( + since = "0.31.0", + note = "Use `try_snapshot` instead. This function will be removed in 0.32" + )] + pub fn try_wgpu_snapshot(&mut self, name: &str) -> Result<(), SnapshotError> { + self.try_snapshot(name) + } + + #[deprecated( + since = "0.31.0", + note = "Use `snapshot_options` instead. This function will be removed in 0.32" + )] + pub fn wgpu_snapshot_options(&mut self, name: &str, options: &SnapshotOptions) { + self.snapshot_options(name, options); + } + + #[deprecated( + since = "0.31.0", + note = "Use `snapshot` instead. This function will be removed in 0.32" + )] + pub fn wgpu_snapshot(&mut self, name: &str) { + self.snapshot(name); + } +} diff --git a/crates/egui_kittest/src/wgpu.rs b/crates/egui_kittest/src/wgpu.rs index 4c3001fa72b..3f229763f7c 100644 --- a/crates/egui_kittest/src/wgpu.rs +++ b/crates/egui_kittest/src/wgpu.rs @@ -1,122 +1,152 @@ use crate::texture_to_image::texture_to_image; -use crate::Harness; -use egui_wgpu::wgpu::{Backends, InstanceDescriptor, StoreOp, TextureFormat}; -use egui_wgpu::{wgpu, ScreenDescriptor}; +use eframe::epaint::TextureId; +use egui::TexturesDelta; +use egui_wgpu::wgpu::{Backends, StoreOp, TextureFormat}; +use egui_wgpu::{wgpu, RenderState, ScreenDescriptor, WgpuSetup}; use image::RgbaImage; use std::iter::once; +use std::sync::Arc; use wgpu::Maintain; -/// Utility to render snapshots from a [`Harness`] using [`egui_wgpu`]. -pub struct TestRenderer { - device: wgpu::Device, - queue: wgpu::Queue, - dithering: bool, +// TODO(#5506): Replace this with the setup from https://github.com/emilk/egui/pull/5506 +pub fn default_wgpu_setup() -> egui_wgpu::WgpuSetup { + egui_wgpu::WgpuSetup::CreateNew { + supported_backends: Backends::all(), + device_descriptor: Arc::new(|_| wgpu::DeviceDescriptor::default()), + power_preference: wgpu::PowerPreference::default(), + } +} + +pub fn create_render_state(setup: WgpuSetup) -> egui_wgpu::RenderState { + let instance = match &setup { + WgpuSetup::Existing { instance, .. } => instance.clone(), + WgpuSetup::CreateNew { .. } => Default::default(), + }; + + pollster::block_on(egui_wgpu::RenderState::create( + &egui_wgpu::WgpuConfiguration { + wgpu_setup: setup, + ..Default::default() + }, + &instance, + None, + None, + 1, + false, + )) + .expect("Failed to create render state") } -impl Default for TestRenderer { +/// Utility to render snapshots from a [`crate::Harness`] using [`egui_wgpu`]. +pub struct WgpuTestRenderer { + render_state: RenderState, +} + +impl Default for WgpuTestRenderer { fn default() -> Self { Self::new() } } -impl TestRenderer { - /// Create a new [`TestRenderer`] using a default [`wgpu::Instance`]. +impl WgpuTestRenderer { + /// Create a new [`WgpuTestRenderer`] with the default setup. pub fn new() -> Self { - let instance = wgpu::Instance::new(InstanceDescriptor::default()); - - let adapters = instance.enumerate_adapters(Backends::all()); - let adapter = adapters.first().expect("No adapter found"); - - let (device, queue) = pollster::block_on(adapter.request_device( - &wgpu::DeviceDescriptor { - label: Some("Egui Device"), - memory_hints: Default::default(), - required_limits: Default::default(), - required_features: Default::default(), - }, - None, - )) - .expect("Failed to create device"); - - Self::create(device, queue) + Self { + render_state: create_render_state(default_wgpu_setup()), + } } - /// Create a new [`TestRenderer`] using the provided [`wgpu::Device`] and [`wgpu::Queue`]. - pub fn create(device: wgpu::Device, queue: wgpu::Queue) -> Self { + /// Create a new [`WgpuTestRenderer`] with the given setup. + pub fn from_setup(setup: WgpuSetup) -> Self { Self { - device, - queue, - dithering: false, + render_state: create_render_state(setup), } } - /// Enable or disable dithering. + /// Create a new [`WgpuTestRenderer`] from an existing [`RenderState`]. /// - /// Disabled by default. - #[inline] - pub fn with_dithering(mut self, dithering: bool) -> Self { - self.dithering = dithering; - self + /// # Panics + /// Panics if the [`RenderState`] has been used before. + pub fn from_render_state(render_state: RenderState) -> Self { + assert!( + render_state + .renderer + .read() + .texture(&TextureId::Managed(0)) + .is_none(), + "The RenderState passed in has been used before, pass in a fresh RenderState instead." + ); + Self { render_state } } +} - /// Render the [`Harness`] and return the resulting image. - pub fn render(&self, harness: &Harness<'_, State>) -> RgbaImage { - // We need to create a new renderer each time we render, since the renderer stores - // textures related to the Harnesses' egui Context. - // Calling the renderer from different Harnesses would cause problems if we store the renderer. - let mut renderer = egui_wgpu::Renderer::new( - &self.device, - TextureFormat::Rgba8Unorm, - None, - 1, - self.dithering, - ); +impl crate::TestRenderer for WgpuTestRenderer { + #[cfg(feature = "eframe")] + fn setup_eframe(&self, cc: &mut eframe::CreationContext<'_>, frame: &mut eframe::Frame) { + cc.wgpu_render_state = Some(self.render_state.clone()); + frame.wgpu_render_state = Some(self.render_state.clone()); + } - for delta in &harness.texture_deltas { - for (id, image_delta) in &delta.set { - renderer.update_texture(&self.device, &self.queue, *id, image_delta); - } + fn handle_delta(&mut self, delta: &TexturesDelta) { + let mut renderer = self.render_state.renderer.write(); + for (id, image) in &delta.set { + renderer.update_texture( + &self.render_state.device, + &self.render_state.queue, + *id, + image, + ); } + } - let mut encoder = self - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("Egui Command Encoder"), - }); - - let size = harness.ctx.screen_rect().size() * harness.ctx.pixels_per_point(); + /// Render the [`crate::Harness`] and return the resulting image. + fn render( + &mut self, + ctx: &egui::Context, + output: &egui::FullOutput, + ) -> Result { + let mut renderer = self.render_state.renderer.write(); + + let mut encoder = + self.render_state + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("Egui Command Encoder"), + }); + + let size = ctx.screen_rect().size() * ctx.pixels_per_point(); let screen = ScreenDescriptor { - pixels_per_point: harness.ctx.pixels_per_point(), + pixels_per_point: ctx.pixels_per_point(), size_in_pixels: [size.x.round() as u32, size.y.round() as u32], }; - let tessellated = harness.ctx.tessellate( - harness.output().shapes.clone(), - harness.ctx.pixels_per_point(), - ); + let tessellated = ctx.tessellate(output.shapes.clone(), ctx.pixels_per_point()); let user_buffers = renderer.update_buffers( - &self.device, - &self.queue, + &self.render_state.device, + &self.render_state.queue, &mut encoder, &tessellated, &screen, ); - let texture = self.device.create_texture(&wgpu::TextureDescriptor { - label: Some("Egui Texture"), - size: wgpu::Extent3d { - width: screen.size_in_pixels[0], - height: screen.size_in_pixels[1], - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: TextureFormat::Rgba8Unorm, - usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, - view_formats: &[], - }); + let texture = self + .render_state + .device + .create_texture(&wgpu::TextureDescriptor { + label: Some("Egui Texture"), + size: wgpu::Extent3d { + width: screen.size_in_pixels[0], + height: screen.size_in_pixels[1], + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: TextureFormat::Rgba8Unorm, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, + view_formats: &[], + }); let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default()); @@ -141,11 +171,16 @@ impl TestRenderer { renderer.render(&mut pass, &tessellated, &screen); } - self.queue + self.render_state + .queue .submit(user_buffers.into_iter().chain(once(encoder.finish()))); - self.device.poll(Maintain::Wait); + self.render_state.device.poll(Maintain::Wait); - texture_to_image(&self.device, &self.queue, &texture) + Ok(texture_to_image( + &self.render_state.device, + &self.render_state.queue, + &texture, + )) } } diff --git a/crates/egui_kittest/tests/regression_tests.rs b/crates/egui_kittest/tests/regression_tests.rs index 690ca86f6b6..32b412e6ed8 100644 --- a/crates/egui_kittest/tests/regression_tests.rs +++ b/crates/egui_kittest/tests/regression_tests.rs @@ -41,5 +41,5 @@ fn image_failed() { harness.fit_contents(); #[cfg(all(feature = "wgpu", feature = "snapshot"))] - harness.wgpu_snapshot("image_snapshots"); + harness.snapshot("image_snapshots"); } diff --git a/crates/egui_kittest/tests/tests.rs b/crates/egui_kittest/tests/tests.rs index 6799b9a3567..bf7d0bdb220 100644 --- a/crates/egui_kittest/tests/tests.rs +++ b/crates/egui_kittest/tests/tests.rs @@ -11,5 +11,5 @@ fn test_shrink() { harness.fit_contents(); #[cfg(all(feature = "snapshot", feature = "wgpu"))] - harness.wgpu_snapshot("test_shrink"); + harness.snapshot("test_shrink"); }