From 3c7ad0ee12a794fa81882e8364e3f25e5d8c11d6 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 6 Nov 2024 14:43:41 +0100 Subject: [PATCH] egui_kittest: Allow passing state to the app closure (#5313) The allows us to pass any state to the ui closure. While it is possible to just store state in the closure itself, accessing that state after the harness was created to e.g. read or modify it would require interior mutability. With this change there are new `Harness::new_state`, `Harness::run_state`, ... methods that allow passing state on each run. This builds on top of #5301, which should be merged first --------- Co-authored-by: Emil Ernerfeldt --- crates/egui_demo_lib/src/demo/text_edit.rs | 16 ++- crates/egui_kittest/src/app_kind.rs | 42 ++++--- crates/egui_kittest/src/builder.rs | 80 +++++++++++-- crates/egui_kittest/src/lib.rs | 129 ++++++++++++++++----- crates/egui_kittest/src/snapshot.rs | 2 +- crates/egui_kittest/src/wgpu.rs | 2 +- 6 files changed, 215 insertions(+), 56 deletions(-) diff --git a/crates/egui_demo_lib/src/demo/text_edit.rs b/crates/egui_demo_lib/src/demo/text_edit.rs index 96763625..5ce5c93b 100644 --- a/crates/egui_demo_lib/src/demo/text_edit.rs +++ b/crates/egui_demo_lib/src/demo/text_edit.rs @@ -121,12 +121,15 @@ mod tests { #[test] pub fn should_type() { - let mut text = "Hello, world!".to_owned(); - let mut harness = Harness::new(move |ctx| { - CentralPanel::default().show(ctx, |ui| { - ui.text_edit_singleline(&mut text); - }); - }); + let text = "Hello, world!".to_owned(); + let mut harness = Harness::new_state( + move |ctx, text| { + CentralPanel::default().show(ctx, |ui| { + ui.text_edit_singleline(text); + }); + }, + text, + ); harness.run(); @@ -144,5 +147,6 @@ mod tests { harness.run(); let text_edit = harness.get_by_role(accesskit::Role::TextInput); assert_eq!(text_edit.value().as_deref(), Some("Hi there!")); + assert_eq!(harness.state(), "Hi there!"); } } diff --git a/crates/egui_kittest/src/app_kind.rs b/crates/egui_kittest/src/app_kind.rs index 5adca264..8a180b3b 100644 --- a/crates/egui_kittest/src/app_kind.rs +++ b/crates/egui_kittest/src/app_kind.rs @@ -1,11 +1,15 @@ use egui::Frame; +type AppKindContextState<'a, State> = Box; +type AppKindUiState<'a, State> = Box; type AppKindContext<'a> = Box; type AppKindUi<'a> = Box; -pub(crate) enum AppKind<'a> { +pub(crate) enum AppKind<'a, State> { Context(AppKindContext<'a>), Ui(AppKindUi<'a>), + ContextState(AppKindContextState<'a, State>), + UiState(AppKindUiState<'a, State>), } // TODO(lucasmerlin): These aren't working unfortunately :( @@ -32,28 +36,34 @@ pub(crate) enum AppKind<'a> { // } // } -impl<'a> AppKind<'a> { - pub fn run(&mut self, ctx: &egui::Context) -> Option { +impl<'a, State> AppKind<'a, State> { + pub fn run( + &mut self, + ctx: &egui::Context, + state: &mut State, + sizing_pass: bool, + ) -> Option { match self { AppKind::Context(f) => { + debug_assert!(!sizing_pass, "Context closures cannot do a sizing pass"); 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); + AppKind::ContextState(f) => { + debug_assert!(!sizing_pass, "Context closures cannot do a sizing pass"); + f(ctx, state); None } - AppKind::Ui(f) => Some(Self::run_ui(f, ctx, true)), + kind_ui => Some(kind_ui.run_ui(ctx, state, sizing_pass)), } } - fn run_ui(f: &mut AppKindUi<'a>, ctx: &egui::Context, sizing_pass: bool) -> egui::Response { + fn run_ui( + &mut self, + ctx: &egui::Context, + state: &mut State, + sizing_pass: bool, + ) -> egui::Response { egui::CentralPanel::default() .frame(Frame::none()) .show(ctx, |ui| { @@ -65,7 +75,11 @@ impl<'a> AppKind<'a> { Frame::central_panel(ui.style()) .outer_margin(8.0) .inner_margin(0.0) - .show(ui, |ui| f(ui)); + .show(ui, |ui| match self { + AppKind::Ui(f) => f(ui), + AppKind::UiState(f) => f(ui, state), + _ => unreachable!(), + }); }) .response }) diff --git a/crates/egui_kittest/src/builder.rs b/crates/egui_kittest/src/builder.rs index edf84b87..45ad00e8 100644 --- a/crates/egui_kittest/src/builder.rs +++ b/crates/egui_kittest/src/builder.rs @@ -1,23 +1,26 @@ use crate::app_kind::AppKind; use crate::Harness; use egui::{Pos2, Rect, Vec2}; +use std::marker::PhantomData; /// Builder for [`Harness`]. -pub struct HarnessBuilder { +pub struct HarnessBuilder { pub(crate) screen_rect: Rect, pub(crate) pixels_per_point: f32, + pub(crate) state: PhantomData, } -impl Default for HarnessBuilder { +impl Default for HarnessBuilder { fn default() -> Self { Self { screen_rect: Rect::from_min_size(Pos2::ZERO, Vec2::new(800.0, 600.0)), pixels_per_point: 1.0, + state: PhantomData, } } } -impl HarnessBuilder { +impl HarnessBuilder { /// Set the size of the window. #[inline] pub fn with_size(mut self, size: impl Into) -> Self { @@ -34,6 +37,69 @@ impl HarnessBuilder { self } + /// 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. + /// + /// If you don't need to create Windows / Panels, you can use [`HarnessBuilder::build_ui`] instead. + /// + /// # Example + /// ```rust + /// # use egui::CentralPanel; + /// # use egui_kittest::{Harness, kittest::Queryable}; + /// let checked = false; + /// let mut harness = Harness::builder() + /// .with_size(egui::Vec2::new(300.0, 200.0)) + /// .build_state(|ctx, checked| { + /// CentralPanel::default().show(ctx, |ui| { + /// ui.checkbox(checked, "Check me!"); + /// }); + /// }, checked); + /// + /// harness.get_by_name("Check me!").click(); + /// harness.run(); + /// + /// assert_eq!(*harness.state(), true); + /// ``` + pub fn build_state<'a>( + self, + app: impl FnMut(&egui::Context, &mut State) + 'a, + state: State, + ) -> Harness<'a, State> { + Harness::from_builder(&self, AppKind::ContextState(Box::new(app)), state) + } + + /// Create a new Harness with the given ui closure and a state. + /// + /// 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, kittest::Queryable}; + /// let mut checked = false; + /// let mut harness = Harness::builder() + /// .with_size(egui::Vec2::new(300.0, 200.0)) + /// .build_ui_state(|ui, checked| { + /// ui.checkbox(checked, "Check me!"); + /// }, checked); + /// + /// harness.get_by_name("Check me!").click(); + /// harness.run(); + /// + /// assert_eq!(*harness.state(), true); + /// ``` + pub fn build_ui_state<'a>( + self, + app: impl FnMut(&mut egui::Ui, &mut State) + 'a, + state: State, + ) -> Harness<'a, State> { + Harness::from_builder(&self, AppKind::UiState(Box::new(app)), state) + } +} + +impl HarnessBuilder { /// Create a new Harness with the given app closure. /// /// The app closure will immediately be called once to create the initial ui. @@ -43,7 +109,7 @@ impl HarnessBuilder { /// # Example /// ```rust /// # use egui::CentralPanel; - /// # use egui_kittest::Harness; + /// # use egui_kittest::{Harness, kittest::Queryable}; /// let mut harness = Harness::builder() /// .with_size(egui::Vec2::new(300.0, 200.0)) /// .build(|ctx| { @@ -53,7 +119,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)), ()) } /// Create a new Harness with the given ui closure. @@ -64,7 +130,7 @@ impl HarnessBuilder { /// /// # Example /// ```rust - /// # use egui_kittest::Harness; + /// # use egui_kittest::{Harness, kittest::Queryable}; /// let mut harness = Harness::builder() /// .with_size(egui::Vec2::new(300.0, 200.0)) /// .build_ui(|ui| { @@ -72,6 +138,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)), ()) } } diff --git a/crates/egui_kittest/src/lib.rs b/crates/egui_kittest/src/lib.rs index dbfde874..8e410d84 100644 --- a/crates/egui_kittest/src/lib.rs +++ b/crates/egui_kittest/src/lib.rs @@ -28,25 +28,34 @@ use kittest::{Node, Queryable}; /// The test Harness. This contains everything needed to run the test. /// Create a new Harness using [`Harness::new`] or [`Harness::builder`]. -pub struct Harness<'a> { +/// +/// The [Harness] has a optional generic state that can be used to pass data to the app / ui closure. +/// In _most cases_ it should be fine to just store the state in the closure itself. +/// The state functions are useful if you need to access the state after the harness has been created. +pub struct Harness<'a, State = ()> { pub ctx: egui::Context, input: egui::RawInput, kittest: kittest::State, output: egui::FullOutput, texture_deltas: Vec, - app: AppKind<'a>, + app: AppKind<'a, State>, event_state: EventState, response: Option, + state: State, } -impl<'a> Debug for Harness<'a> { +impl<'a, State> Debug for Harness<'a, State> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { self.kittest.fmt(f) } } -impl<'a> Harness<'a> { - pub(crate) fn from_builder(builder: &HarnessBuilder, mut app: AppKind<'a>) -> Self { +impl<'a, State> Harness<'a, State> { + pub(crate) fn from_builder( + builder: &HarnessBuilder, + mut app: AppKind<'a, State>, + mut state: State, + ) -> Self { let ctx = egui::Context::default(); ctx.enable_accesskit(); let mut input = egui::RawInput { @@ -61,7 +70,7 @@ impl<'a> Harness<'a> { // 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(), |ctx| { - response = app.run(ctx); + response = app.run(ctx, &mut state, false); }); let mut harness = Self { @@ -79,17 +88,19 @@ impl<'a> Harness<'a> { output, response, event_state: EventState::default(), + state, }; // Run the harness until it is stable, ensuring that all Areas are shown and animations are done harness.run(); harness } - pub fn builder() -> HarnessBuilder { + /// Create a [`Harness`] via a [`HarnessBuilder`]. + pub fn builder() -> HarnessBuilder { HarnessBuilder::default() } - /// Create a new Harness with the given app closure. + /// 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. /// @@ -100,18 +111,24 @@ impl<'a> Harness<'a> { /// # Example /// ```rust /// # use egui::CentralPanel; - /// # use egui_kittest::Harness; - /// let mut harness = Harness::new(|ctx| { + /// # use egui_kittest::{Harness, kittest::Queryable}; + /// let mut checked = false; + /// let mut harness = Harness::new_state(|ctx, checked| { /// CentralPanel::default().show(ctx, |ui| { - /// ui.label("Hello, world!"); + /// ui.checkbox(checked, "Check me!"); /// }); - /// }); + /// }, checked); + /// + /// harness.get_by_name("Check me!").click(); + /// harness.run(); + /// + /// assert_eq!(*harness.state(), true); /// ``` - pub fn new(app: impl FnMut(&egui::Context) + 'a) -> Self { - Self::builder().build(app) + pub fn new_state(app: impl FnMut(&egui::Context, &mut State) + 'a, state: State) -> Self { + Self::builder().build_state(app, state) } - /// Create a new Harness with the given ui closure. + /// Create a new Harness with the given ui closure and a state. /// /// The ui closure will immediately be called once to create the initial ui. /// @@ -121,13 +138,19 @@ impl<'a> Harness<'a> { /// /// # Example /// ```rust - /// # use egui_kittest::Harness; - /// let mut harness = Harness::new_ui(|ui| { - /// ui.label("Hello, world!"); - /// }); + /// # use egui_kittest::{Harness, kittest::Queryable}; + /// let mut checked = false; + /// let mut harness = Harness::new_ui_state(|ui, checked| { + /// ui.checkbox(checked, "Check me!"); + /// }, checked); + /// + /// harness.get_by_name("Check me!").click(); + /// harness.run(); + /// + /// assert_eq!(*harness.state(), true); /// ``` - pub fn new_ui(app: impl FnMut(&mut egui::Ui) + 'a) -> Self { - Self::builder().build_ui(app) + pub fn new_ui_state(app: impl FnMut(&mut egui::Ui, &mut State) + 'a, state: State) -> Self { + Self::builder().build_ui_state(app, state) } /// Set the size of the window. @@ -162,11 +185,7 @@ impl<'a> Harness<'a> { } 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.response = self.app.run(ctx, &mut self.state, sizing_pass); }); self.kittest.update( output @@ -220,9 +239,65 @@ impl<'a> Harness<'a> { pub fn kittest_state(&self) -> &kittest::State { &self.kittest } + + /// Access the state. + pub fn state(&self) -> &State { + &self.state + } + + /// Access the state mutably. + pub fn state_mut(&mut self) -> &mut State { + &mut self.state + } } -impl<'t, 'n, 'h> Queryable<'t, 'n> for Harness<'h> +/// Utilities for stateless harnesses. +impl<'a> Harness<'a> { + /// Create a new Harness with the given app closure. + /// Use the [`Harness::run`], [`Harness::step`], etc... methods to run the app. + /// + /// 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`]. + /// + /// # Example + /// ```rust + /// # use egui::CentralPanel; + /// # use egui_kittest::Harness; + /// let mut harness = Harness::new(|ctx| { + /// CentralPanel::default().show(ctx, |ui| { + /// ui.label("Hello, world!"); + /// }); + /// }); + /// ``` + pub fn new(app: impl FnMut(&egui::Context) + 'a) -> Self { + Self::builder().build(app) + } + + /// Create a new Harness with the given ui closure. + /// Use the [`Harness::run`], [`Harness::step`], etc... methods to run the app. + /// + /// 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) + } +} + +impl<'t, 'n, 'h, State> Queryable<'t, 'n> for Harness<'h, State> where 'n: 't, { diff --git a/crates/egui_kittest/src/snapshot.rs b/crates/egui_kittest/src/snapshot.rs index 7a03b334..40e02027 100644 --- a/crates/egui_kittest/src/snapshot.rs +++ b/crates/egui_kittest/src/snapshot.rs @@ -288,7 +288,7 @@ pub fn image_snapshot(current: &image::RgbaImage, name: &str) { } #[cfg(feature = "wgpu")] -impl Harness<'_> { +impl Harness<'_, State> { /// Render a image using a default [`crate::wgpu::TestRenderer`] and compare it to the snapshot /// with custom options. /// diff --git a/crates/egui_kittest/src/wgpu.rs b/crates/egui_kittest/src/wgpu.rs index 6d165c94..d2cda112 100644 --- a/crates/egui_kittest/src/wgpu.rs +++ b/crates/egui_kittest/src/wgpu.rs @@ -60,7 +60,7 @@ impl TestRenderer { } /// Render the [`Harness`] and return the resulting image. - pub fn render(&mut self, harness: &Harness<'_>) -> RgbaImage { + pub fn render(&mut 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.