diff --git a/crates/egui_kittest/src/lib.rs b/crates/egui_kittest/src/lib.rs index a36037f36..a6c69ce73 100644 --- a/crates/egui_kittest/src/lib.rs +++ b/crates/egui_kittest/src/lib.rs @@ -79,7 +79,7 @@ pub struct Harness<'a, State: 'static = ()> { output: egui::FullOutput, app: AppKind<'a, State>, response: Option, - state: State, + state: Option, renderer: Box, max_steps: u64, step_dt: f32, @@ -170,7 +170,7 @@ impl<'a, State: 'static> Harness<'a, State> { kittest: kittest::State::new(initial_accesskit), output, response, - state, + state: Some(state), renderer, max_steps, step_dt, @@ -208,41 +208,12 @@ impl<'a, State: 'static> Harness<'a, State> { self.plugins.push(Box::new(plugin)); } - /// Borrow a registered plugin by type. Returns the first plugin of the matching type - /// in registration order, or `None` if no plugin of that type is registered. - pub fn plugin>(&self) -> Option<&P> { - self.plugins - .iter() - .find_map(|p| (&**p as &dyn std::any::Any).downcast_ref::

()) - } - - /// Mutably borrow a registered plugin by type. - pub fn plugin_mut>(&mut self) -> Option<&mut P> { - self.plugins - .iter_mut() - .find_map(|p| (&mut **p as &mut dyn std::any::Any).downcast_mut::

()) - } - - /// Remove and return the first plugin of the given type. - pub fn take_plugin>(&mut self) -> Option> { - let idx = self - .plugins - .iter() - .position(|p| (&**p as &dyn std::any::Any).is::

())?; - let boxed = self.plugins.remove(idx); - let raw: *mut dyn Plugin = Box::into_raw(boxed); - // SAFETY: `is::

()` confirmed the concrete type is `P`. Fat-to-thin pointer - // cast preserves the data pointer, which is the address of the underlying `P`. - #[expect(unsafe_code)] - Some(unsafe { Box::from_raw(raw.cast::

()) }) - } - /// Advance the harness by one frame without firing plugin hooks. /// /// This is useful for running steps within a plugin, without ending in an infinite loop where /// the plugin is called again. pub fn step_no_side_effects(&mut self) { - self._step_inner(false); + self._step_no_side_effects(false); } /// [`std::panic::Location`] of the most recent public `#[track_caller]` entry point @@ -256,7 +227,7 @@ impl<'a, State: 'static> Harness<'a, State> { &self.consumed_event_locations } - fn dispatch(&mut self, mut f: impl FnMut(&mut dyn Plugin, &mut Self)) { + fn plugin_dispatch(&mut self, mut f: impl FnMut(&mut dyn Plugin, &mut Self)) { if self.plugins.is_empty() { return; } @@ -339,7 +310,7 @@ impl<'a, State: 'static> Harness<'a, State> { EventType::Event(event, loc) => { self.consumed_event_locations.push(loc); self.input.events.push(event.clone()); - self.dispatch(|p, h| p.on_event(h, &event)); + self.plugin_dispatch(|p, h| p.on_event(h, &event)); } EventType::Modifiers(modifiers, loc) => { self.consumed_event_locations.push(loc); @@ -352,25 +323,25 @@ impl<'a, State: 'static> Harness<'a, State> { /// Run a single step, firing `before_step` / `after_step` plugin hooks. fn _step(&mut self, sizing_pass: bool) { - self.dispatch(|p, h| p.before_step(h)); - self._step_inner(sizing_pass); - self.dispatch(|p, h| p.after_step(h)); + self.plugin_dispatch(|p, h| p.before_step(h)); + self._step_no_side_effects(sizing_pass); + self.plugin_dispatch(|p, h| p.after_step(h)); } /// Core frame advance. Does NOT fire plugin hooks — callable from within /// hooks via [`Self::step_no_side_effects`] without recursing. - fn _step_inner(&mut self, sizing_pass: bool) { + fn _step_no_side_effects(&mut self, sizing_pass: bool) { self.input.predicted_dt = self.step_dt; let mut output = self.ctx.run_ui(self.input.take(), |ui| { - self.response = self.app.run(ui, &mut self.state, sizing_pass); + self.response = self.app.run(ui, self.state.as_mut().unwrap(), sizing_pass); }); let accesskit_update = output .platform_output .accesskit_update .take() .expect("AccessKit was disabled"); - self.dispatch(|p, h| p.on_accesskit_update(h, &accesskit_update)); + self.plugin_dispatch(|p, h| p.on_accesskit_update(h, &accesskit_update)); self.kittest.update(accesskit_update); self.renderer.handle_delta(&output.textures_delta); self.output = output; @@ -438,7 +409,7 @@ impl<'a, State: 'static> Harness<'a, State> { } fn _try_run(&mut self, sleep: bool) -> Result { - self.dispatch(|p, h| p.before_run(h)); + self.plugin_dispatch(|p, h| p.before_run(h)); let mut steps = 0; let result = loop { @@ -460,7 +431,7 @@ impl<'a, State: 'static> Harness<'a, State> { }); } }; - self.dispatch(|p, h| p.after_run(h, result.as_ref().map(|s| *s))); + self.plugin_dispatch(|p, h| p.after_run(h, result.as_ref().map(|s| *s))); result } @@ -562,12 +533,17 @@ impl<'a, State: 'static> Harness<'a, State> { /// Access the state. pub fn state(&self) -> &State { - &self.state + self.state.as_ref().expect("state already taken via into_state") } /// Access the state mutably. pub fn state_mut(&mut self) -> &mut State { - &mut self.state + self.state.as_mut().expect("state already taken via into_state") + } + + /// Consume the harness and return the state. + pub fn into_state(mut self) -> State { + self.state.take().expect("state already taken via into_state") } /// Queue an event to be processed in the next frame. @@ -793,7 +769,7 @@ impl<'a, State: 'static> Harness<'a, State> { } let image = self.renderer.render(&self.ctx, &output)?; - self.dispatch(|p, h| p.on_render(h, &image)); + self.plugin_dispatch(|p, h| p.on_render(h, &image)); Ok(image) } @@ -886,7 +862,7 @@ impl<'a, State: 'static> Harness<'a, State> { if let AppKind::Eframe(crate::app_kind::AppKindEframe { get_app, .. }) = &mut self.harness.app { - get_app(&mut self.harness.state).logic(ctx, frame); + get_app(self.harness.state.as_mut().unwrap()).logic(ctx, frame); } } @@ -894,9 +870,9 @@ impl<'a, State: 'static> Harness<'a, State> { let harness = &mut self.harness; match &mut harness.app { AppKind::Ui(f) => f(ui), - AppKind::UiState(f) => f(ui, &mut harness.state), + AppKind::UiState(f) => f(ui, harness.state.as_mut().unwrap()), AppKind::Eframe(crate::app_kind::AppKindEframe { get_app, .. }) => { - get_app(&mut harness.state).ui(ui, frame); + get_app(harness.state.as_mut().unwrap()).ui(ui, frame); } } } @@ -963,10 +939,10 @@ impl Drop for Harness<'_, State> { if std::thread::panicking() { plugin::with_fail_test_result(|result| { - self.dispatch(|p, h| p.on_test_result(h, fail_ref(&result))); + self.plugin_dispatch(|p, h| p.on_test_result(h, fail_ref(&result))); }); } else { - self.dispatch(|p, h| p.on_test_result(h, TestResult::Pass)); + self.plugin_dispatch(|p, h| p.on_test_result(h, TestResult::Pass)); } } } diff --git a/crates/egui_kittest/src/plugin.rs b/crates/egui_kittest/src/plugin.rs index 9647d2291..65e6b2e9d 100644 --- a/crates/egui_kittest/src/plugin.rs +++ b/crates/egui_kittest/src/plugin.rs @@ -4,8 +4,6 @@ //! renders, snapshots, and final pass/fail. Register plugins via //! [`crate::HarnessBuilder::with_plugin`] or [`crate::Harness::add_plugin`]. -use std::any::Any; - use crate::{ExceededMaxStepsError, Harness}; /// A plugin observes the test-harness lifecycle and can drive additional frames. @@ -25,12 +23,6 @@ use crate::{ExceededMaxStepsError, Harness}; /// } /// ``` /// -/// # Downcasting -/// -/// [`Any`] is a supertrait, so [`Harness::plugin`] / [`Harness::plugin_mut`] / -/// [`Harness::take_plugin`] downcast registered plugins back to their concrete type via -/// trait upcasting. No boilerplate needed on your end. -/// /// # Re-entrancy /// /// Plugin hooks receive `&mut Harness`. Calling [`Harness::step`] / [`Harness::run`] / @@ -38,7 +30,7 @@ use crate::{ExceededMaxStepsError, Harness}; /// a plugin needs to advance the harness from inside a hook — e.g. an inspector that /// blocks on user input — use [`Harness::step_no_side_effects`] instead. #[expect(unused_variables, reason = "default no-op impls")] -pub trait Plugin: Send + Any { +pub trait Plugin: Send + 'static { /// Called once at the start of every `run()` / `try_run()` / `try_run_realtime()` / /// `run_ok()` invocation, before the first step. fn before_run(&mut self, harness: &mut Harness<'_, State>) {} diff --git a/crates/egui_kittest/src/snapshot.rs b/crates/egui_kittest/src/snapshot.rs index 1bca3d3c9..851a56e30 100644 --- a/crates/egui_kittest/src/snapshot.rs +++ b/crates/egui_kittest/src/snapshot.rs @@ -629,7 +629,7 @@ impl Harness<'_, State> { Err(err) => return Err(SnapshotError::RenderError { err }), }; let result = try_image_snapshot_options(&image, name.clone(), options); - self.dispatch(|p, h| p.on_snapshot(h, &name, &image, &result)); + self.plugin_dispatch(|p, h| p.on_snapshot(h, &name, &image, &result)); result } diff --git a/crates/egui_kittest/tests/plugin.rs b/crates/egui_kittest/tests/plugin.rs index 0a4b406ca..ef83d3ddc 100644 --- a/crates/egui_kittest/tests/plugin.rs +++ b/crates/egui_kittest/tests/plugin.rs @@ -196,22 +196,6 @@ fn mid_dispatch_registration_is_deferred() { // any "before_step" entries. Since we merged logs, we can't easily isolate — instead, // verify the harness does not deadlock / recurse. harness.step(); - assert!(harness.plugin::().is_some()); -} - -/// Downcasting via `plugin::

()` / `plugin_mut::

()` / `take_plugin::

()`. -#[test] -fn downcast_plugin_by_type() { - let (plugin, _log) = CountingPlugin::new(); - let mut harness = Harness::builder().with_plugin(plugin).build_ui(|ui| { - ui.label("hi"); - }); - - assert!(harness.plugin::().is_some()); - assert!(harness.plugin_mut::().is_some()); - let taken = harness.take_plugin::(); - assert!(taken.is_some()); - assert!(harness.plugin::().is_none()); } /// When `Harness::drop` fires while a panic is unwinding, `on_test_result` gets `Fail`.