From ad14bf2490c0d1e0960e0efc1143c8eeaf3eb0c6 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Fri, 1 Nov 2024 18:30:40 +0100 Subject: [PATCH] Add `Harness::new_ui`, `Harness::fit_contents` (#5301) This adds a `Harness::new_ui`, which accepts a Ui closure and shows the ui in a central panel. One big benefit is that this allows us to add a fit_contents method that can run the ui closure with a sizing pass and resize the "screen" based on the content size. I also used this to add a snapshot test for the rendering_test at different scales. --- Cargo.lock | 1 + .../egui_demo_lib/src/demo/widget_gallery.rs | 15 ++-- crates/egui_demo_lib/src/rendering_test.rs | 36 ++++++++- .../snapshots/rendering_test/dpi_1.00.png | 3 + .../snapshots/rendering_test/dpi_1.25.png | 3 + .../snapshots/rendering_test/dpi_1.50.png | 3 + .../snapshots/rendering_test/dpi_1.67.png | 3 + .../snapshots/rendering_test/dpi_1.75.png | 3 + .../snapshots/rendering_test/dpi_2.00.png | 3 + .../tests/snapshots/widget_gallery.png | 4 +- crates/egui_kittest/Cargo.toml | 1 + crates/egui_kittest/src/app_kind.rs | 74 ++++++++++++++++++ crates/egui_kittest/src/builder.rs | 36 +++++++-- crates/egui_kittest/src/lib.rs | 76 +++++++++++++++---- crates/egui_kittest/src/wgpu.rs | 2 +- .../tests/snapshots/test_shrink.png | 3 + crates/egui_kittest/tests/tests.rs | 14 ++++ 17 files changed, 242 insertions(+), 38 deletions(-) create mode 100644 crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png create mode 100644 crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png create mode 100644 crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png create mode 100644 crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png create mode 100644 crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png create mode 100644 crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png create mode 100644 crates/egui_kittest/src/app_kind.rs create mode 100644 crates/egui_kittest/tests/snapshots/test_shrink.png create mode 100644 crates/egui_kittest/tests/tests.rs diff --git a/Cargo.lock b/Cargo.lock index 6dd956c7ba3..000165018c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1364,6 +1364,7 @@ dependencies = [ "document-features", "egui", "egui-wgpu", + "egui_kittest", "image", "kittest", "pollster", diff --git a/crates/egui_demo_lib/src/demo/widget_gallery.rs b/crates/egui_demo_lib/src/demo/widget_gallery.rs index 60c9bccf0a9..c579576b85b 100644 --- a/crates/egui_demo_lib/src/demo/widget_gallery.rs +++ b/crates/egui_demo_lib/src/demo/widget_gallery.rs @@ -290,7 +290,7 @@ fn doc_link_label_with_crate<'a>( mod tests { use super::*; use crate::View; - use egui::{CentralPanel, Context, Vec2}; + use egui::Vec2; use egui_kittest::Harness; #[test] @@ -300,15 +300,12 @@ mod tests { date: Some(chrono::NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()), ..Default::default() }; - let app = |ctx: &Context| { - CentralPanel::default().show(ctx, |ui| { - demo.ui(ui); - }); - }; - let harness = Harness::builder() + let mut harness = Harness::builder() + .with_pixels_per_point(2.0) .with_size(Vec2::new(380.0, 550.0)) - .with_dpi(2.0) - .build(app); + .build_ui(|ui| demo.ui(ui)); + + harness.fit_contents(); harness.wgpu_snapshot("widget_gallery"); } diff --git a/crates/egui_demo_lib/src/rendering_test.rs b/crates/egui_demo_lib/src/rendering_test.rs index 3ae4f111808..f9ead5c584c 100644 --- a/crates/egui_demo_lib/src/rendering_test.rs +++ b/crates/egui_demo_lib/src/rendering_test.rs @@ -435,10 +435,7 @@ fn pixel_test_strokes(ui: &mut Ui) { let thickness_pixels = thickness_pixels as f32; let thickness_points = thickness_pixels / pixels_per_point; let num_squares = (pixels_per_point * 10.0).round().max(10.0) as u32; - let size_pixels = vec2( - ui.available_width(), - num_squares as f32 + thickness_pixels * 2.0, - ); + let size_pixels = vec2(ui.min_size().x, num_squares as f32 + thickness_pixels * 2.0); let size_points = size_pixels / pixels_per_point + Vec2::splat(2.0); let (response, painter) = ui.allocate_painter(size_points, Sense::hover()); @@ -680,3 +677,34 @@ fn mul_color_gamma(left: Color32, right: Color32) -> Color32 { (left.a() as f32 * right.a() as f32 / 255.0).round() as u8, ) } + +#[cfg(test)] +mod tests { + use crate::ColorTest; + use egui::vec2; + + #[test] + pub fn rendering_test() { + let mut errors = vec![]; + for dpi in [1.0, 1.25, 1.5, 1.75, 1.6666667, 2.0] { + let mut color_test = ColorTest::default(); + let mut harness = egui_kittest::Harness::builder() + .with_size(vec2(2000.0, 2000.0)) + .with_pixels_per_point(dpi) + .build_ui(|ui| { + color_test.ui(ui); + }); + + //harness.set_size(harness.ctx.used_size()); + + harness.fit_contents(); + + let result = harness.try_wgpu_snapshot(&format!("rendering_test/dpi_{dpi:.2}")); + if let Err(err) = result { + errors.push(err); + } + } + + assert!(errors.is_empty(), "Errors: {errors:#?}"); + } +} diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png new file mode 100644 index 00000000000..ae442978324 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:023eaa363b42ec24ae845dc2ca9ff271a0bd47217e625785d3716044ecfa7a64 +size 278444 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png new file mode 100644 index 00000000000..428160c867d --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d81f618e54176b1c43b710121f249e13ce29827fbea3451827ab62229006677e +size 378603 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png new file mode 100644 index 00000000000..003a08bd6e3 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d8eca6d5555ef779233175615b877fb91318b4a09a37e5cfbe71973d56f4caf +size 465907 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png new file mode 100644 index 00000000000..629edf05c1e --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4768804f57dfc54c5f6b84a2686038b8d630a28c7e928ae044d5b2ce8377e2cd +size 538775 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png new file mode 100644 index 00000000000..98ffb99034b --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fcee0e0302f33d681348d62bee3b548beb494c6dd1fa3454586986e0b699e162 +size 572403 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png new file mode 100644 index 00000000000..04d581799bd --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:254a8dff0b1d4b74971fd3bd4044c4ec0ce49412a95e98419a14dc55b32a4fc9 +size 663272 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery.png index 273b85a6303..914b3da9ead 100644 --- a/crates/egui_demo_lib/tests/snapshots/widget_gallery.png +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5dc632962f8894c4f20a48c9b9e57d60470f3f83ef7f19d05854dba718610a2f -size 161820 +oid sha256:c069ef4f86beeeafd8686f30fc914bedd7e7e7ec38fd96e9a46ac6b31308c43f +size 160883 diff --git a/crates/egui_kittest/Cargo.toml b/crates/egui_kittest/Cargo.toml index 5063b59658b..f9f98198c82 100644 --- a/crates/egui_kittest/Cargo.toml +++ b/crates/egui_kittest/Cargo.toml @@ -39,6 +39,7 @@ dify = { workspace = true, optional = true } document-features = { workspace = true, optional = true } [dev-dependencies] +egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] } wgpu = { workspace = true, features = ["metal"] } image = { workspace = true, features = ["png"] } egui = { workspace = true, features = ["default_fonts"] } diff --git a/crates/egui_kittest/src/app_kind.rs b/crates/egui_kittest/src/app_kind.rs new file mode 100644 index 00000000000..5adca264fb3 --- /dev/null +++ b/crates/egui_kittest/src/app_kind.rs @@ -0,0 +1,74 @@ +use egui::Frame; + +type AppKindContext<'a> = Box; +type AppKindUi<'a> = Box; + +pub(crate) enum AppKind<'a> { + Context(AppKindContext<'a>), + Ui(AppKindUi<'a>), +} + +// 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> AppKind<'a> { + pub fn run(&mut self, ctx: &egui::Context) -> Option { + match self { + AppKind::Context(f) => { + f(ctx); + None + } + AppKind::Ui(f) => Some(Self::run_ui(f, ctx, false)), + } + } + + pub(crate) fn run_sizing_pass(&mut self, ctx: &egui::Context) -> Option { + match self { + AppKind::Context(f) => { + f(ctx); + None + } + AppKind::Ui(f) => Some(Self::run_ui(f, ctx, true)), + } + } + + fn run_ui(f: &mut AppKindUi<'a>, ctx: &egui::Context, sizing_pass: bool) -> egui::Response { + egui::CentralPanel::default() + .frame(Frame::none()) + .show(ctx, |ui| { + let mut builder = egui::UiBuilder::new(); + if sizing_pass { + builder.sizing_pass = true; + } + ui.scope_builder(builder, |ui| { + Frame::central_panel(ui.style()) + .outer_margin(8.0) + .inner_margin(0.0) + .show(ui, |ui| f(ui)); + }) + .response + }) + .inner + } +} diff --git a/crates/egui_kittest/src/builder.rs b/crates/egui_kittest/src/builder.rs index bf1daf3622b..edf84b87f5a 100644 --- a/crates/egui_kittest/src/builder.rs +++ b/crates/egui_kittest/src/builder.rs @@ -1,17 +1,18 @@ +use crate::app_kind::AppKind; use crate::Harness; use egui::{Pos2, Rect, Vec2}; /// Builder for [`Harness`]. pub struct HarnessBuilder { pub(crate) screen_rect: Rect, - pub(crate) dpi: f32, + pub(crate) pixels_per_point: f32, } impl Default for HarnessBuilder { fn default() -> Self { Self { screen_rect: Rect::from_min_size(Pos2::ZERO, Vec2::new(800.0, 600.0)), - dpi: 1.0, + pixels_per_point: 1.0, } } } @@ -26,16 +27,18 @@ impl HarnessBuilder { self } - /// Set the DPI of the window. + /// Set the `pixels_per_point` of the window. #[inline] - pub fn with_dpi(mut self, dpi: f32) -> Self { - self.dpi = dpi; + pub fn with_pixels_per_point(mut self, pixels_per_point: f32) -> Self { + self.pixels_per_point = pixels_per_point; self } /// Create a new Harness with the given app closure. /// - /// The ui closure will immediately be called once to create the initial ui. + /// The app closure will immediately be called once to create the initial ui. + /// + /// If you don't need to create Windows / Panels, you can use [`HarnessBuilder::build_ui`] instead. /// /// # Example /// ```rust @@ -50,6 +53,25 @@ impl HarnessBuilder { /// }); /// ``` pub fn build<'a>(self, app: impl FnMut(&egui::Context) + 'a) -> Harness<'a> { - Harness::from_builder(&self, app) + Harness::from_builder(&self, AppKind::Context(Box::new(app))) + } + + /// Create a new Harness with the given ui closure. + /// + /// The ui closure will immediately be called once to create the initial ui. + /// + /// If you need to create Windows / Panels, you can use [`HarnessBuilder::build`] instead. + /// + /// # Example + /// ```rust + /// # use egui_kittest::Harness; + /// let mut harness = Harness::builder() + /// .with_size(egui::Vec2::new(300.0, 200.0)) + /// .build_ui(|ui| { + /// ui.label("Hello, world!"); + /// }); + /// ``` + pub fn build_ui<'a>(self, app: impl FnMut(&mut egui::Ui) + 'a) -> Harness<'a> { + Harness::from_builder(&self, AppKind::Ui(Box::new(app))) } } diff --git a/crates/egui_kittest/src/lib.rs b/crates/egui_kittest/src/lib.rs index f4f00ad652f..dbfde8748c8 100644 --- a/crates/egui_kittest/src/lib.rs +++ b/crates/egui_kittest/src/lib.rs @@ -11,6 +11,7 @@ mod snapshot; #[cfg(feature = "snapshot")] pub use snapshot::*; use std::fmt::{Debug, Formatter}; +mod app_kind; #[cfg(feature = "wgpu")] mod texture_to_image; #[cfg(feature = "wgpu")] @@ -19,6 +20,7 @@ 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}; @@ -32,8 +34,9 @@ pub struct Harness<'a> { kittest: kittest::State, output: egui::FullOutput, texture_deltas: Vec, - update_fn: Box, + app: AppKind<'a>, event_state: EventState, + response: Option, } impl<'a> Debug for Harness<'a> { @@ -43,10 +46,7 @@ impl<'a> Debug for Harness<'a> { } impl<'a> Harness<'a> { - pub(crate) fn from_builder( - builder: &HarnessBuilder, - mut app: impl FnMut(&egui::Context) + 'a, - ) -> Self { + pub(crate) fn from_builder(builder: &HarnessBuilder, mut app: AppKind<'a>) -> Self { let ctx = egui::Context::default(); ctx.enable_accesskit(); let mut input = egui::RawInput { @@ -54,14 +54,18 @@ impl<'a> Harness<'a> { ..Default::default() }; let viewport = input.viewports.get_mut(&ViewportId::ROOT).unwrap(); - viewport.native_pixels_per_point = Some(builder.dpi); + viewport.native_pixels_per_point = Some(builder.pixels_per_point); + + let mut response = None; // We need to run egui for a single frame so that the AccessKit state can be initialized // and users can immediately start querying for widgets. - let mut output = ctx.run(input.clone(), &mut app); + let mut output = ctx.run(input.clone(), |ctx| { + response = app.run(ctx); + }); let mut harness = Self { - update_fn: Box::new(app), + app, ctx, input, kittest: kittest::State::new( @@ -73,6 +77,7 @@ impl<'a> Harness<'a> { ), texture_deltas: vec![mem::take(&mut output.textures_delta)], output, + response, event_state: EventState::default(), }; // Run the harness until it is stable, ensuring that all Areas are shown and animations are done @@ -86,7 +91,9 @@ impl<'a> Harness<'a> { /// Create a new Harness with the given app closure. /// - /// The ui closure will immediately be called once to create the initial ui. + /// The app closure will immediately be called once to create the initial ui. + /// + /// If you don't need to create Windows / Panels, you can use [`Harness::new_ui`] instead. /// /// If you e.g. want to customize the size of the window, you can use [`Harness::builder`]. /// @@ -104,6 +111,25 @@ impl<'a> Harness<'a> { Self::builder().build(app) } + /// Create a new Harness with the given ui closure. + /// + /// The ui closure will immediately be called once to create the initial ui. + /// + /// If you need to create Windows / Panels, you can use [`Harness::new`] instead. + /// + /// If you e.g. want to customize the size of the ui, you can use [`Harness::builder`]. + /// + /// # Example + /// ```rust + /// # use egui_kittest::Harness; + /// let mut harness = Harness::new_ui(|ui| { + /// ui.label("Hello, world!"); + /// }); + /// ``` + pub fn new_ui(app: impl FnMut(&mut egui::Ui) + 'a) -> Self { + Self::builder().build_ui(app) + } + /// Set the size of the window. /// Note: If you only want to set the size once at the beginning, /// prefer using [`HarnessBuilder::with_size`]. @@ -113,25 +139,35 @@ impl<'a> Harness<'a> { self } - /// Set the DPI of the window. - /// Note: If you only want to set the DPI once at the beginning, - /// prefer using [`HarnessBuilder::with_dpi`]. + /// Set the `pixels_per_point` of the window. + /// Note: If you only want to set the `pixels_per_point` once at the beginning, + /// prefer using [`HarnessBuilder::with_pixels_per_point`]. #[inline] - pub fn set_dpi(&mut self, dpi: f32) -> &mut Self { - self.ctx.set_pixels_per_point(dpi); + pub fn set_pixels_per_point(&mut self, pixels_per_point: f32) -> &mut Self { + self.ctx.set_pixels_per_point(pixels_per_point); self } /// Run a frame. /// This will call the app closure with the current context and update the Harness. pub fn step(&mut self) { + self._step(false); + } + + fn _step(&mut self, sizing_pass: bool) { for event in self.kittest.take_events() { if let Some(event) = self.event_state.kittest_event_to_egui(event) { self.input.events.push(event); } } - let mut output = self.ctx.run(self.input.take(), self.update_fn.as_mut()); + let mut output = self.ctx.run(self.input.take(), |ctx| { + if sizing_pass { + self.response = self.app.run_sizing_pass(ctx); + } else { + self.response = self.app.run(ctx); + } + }); self.kittest.update( output .platform_output @@ -144,6 +180,16 @@ impl<'a> Harness<'a> { self.output = output; } + /// Resize the test harness to fit the contents. This only works when creating the Harness via + /// [`Harness::new_ui`] or [`HarnessBuilder::build_ui`]. + pub fn fit_contents(&mut self) { + self._step(true); + if let Some(response) = &self.response { + self.set_size(response.rect.size()); + } + self.run(); + } + /// Run a few frames. /// This will soon be changed to run the app until it is "stable", meaning /// - all animations are done diff --git a/crates/egui_kittest/src/wgpu.rs b/crates/egui_kittest/src/wgpu.rs index a979226ebc2..6d165c94bda 100644 --- a/crates/egui_kittest/src/wgpu.rs +++ b/crates/egui_kittest/src/wgpu.rs @@ -128,7 +128,7 @@ impl TestRenderer { view: &texture_view, resolve_target: None, ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), + load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), store: StoreOp::Store, }, })], diff --git a/crates/egui_kittest/tests/snapshots/test_shrink.png b/crates/egui_kittest/tests/snapshots/test_shrink.png new file mode 100644 index 00000000000..10967a3d52a --- /dev/null +++ b/crates/egui_kittest/tests/snapshots/test_shrink.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7008bdb595a19782c4f724bed363e51bd93121f5211186aa0e8014c8ba1007c2 +size 3005 diff --git a/crates/egui_kittest/tests/tests.rs b/crates/egui_kittest/tests/tests.rs new file mode 100644 index 00000000000..4978cbeb775 --- /dev/null +++ b/crates/egui_kittest/tests/tests.rs @@ -0,0 +1,14 @@ +use egui_kittest::Harness; + +#[test] +fn test_shrink() { + let mut harness = Harness::new_ui(|ui| { + ui.label("Hello, world!"); + ui.separator(); + ui.label("This is a test"); + }); + + harness.fit_contents(); + + harness.wgpu_snapshot("test_shrink"); +}