diff --git a/crates/egui_demo_lib/src/demo/text_edit.rs b/crates/egui_demo_lib/src/demo/text_edit.rs index 96763625fca..5ce5c93be04 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 5adca264fb3..8a180b3b93b 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 edf84b87f5a..45ad00e83a1 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 dbfde8748c8..8e410d84d11 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 + } +} + +/// 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> Queryable<'t, 'n> for Harness<'h> +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 7a03b3341bc..40e02027b4b 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 6d165c94bda..d2cda112af7 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.