1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-26 22:53:14 -04:00

More cleanup

This commit is contained in:
lucasmerlin
2026-04-24 12:27:29 +02:00
parent 7a105a1843
commit 06ef7151f2
4 changed files with 28 additions and 76 deletions

View File

@@ -79,7 +79,7 @@ pub struct Harness<'a, State: 'static = ()> {
output: egui::FullOutput,
app: AppKind<'a, State>,
response: Option<egui::Response>,
state: State,
state: Option<State>,
renderer: Box<dyn TestRenderer>,
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<P: Plugin<State>>(&self) -> Option<&P> {
self.plugins
.iter()
.find_map(|p| (&**p as &dyn std::any::Any).downcast_ref::<P>())
}
/// Mutably borrow a registered plugin by type.
pub fn plugin_mut<P: Plugin<State>>(&mut self) -> Option<&mut P> {
self.plugins
.iter_mut()
.find_map(|p| (&mut **p as &mut dyn std::any::Any).downcast_mut::<P>())
}
/// Remove and return the first plugin of the given type.
pub fn take_plugin<P: Plugin<State>>(&mut self) -> Option<Box<P>> {
let idx = self
.plugins
.iter()
.position(|p| (&**p as &dyn std::any::Any).is::<P>())?;
let boxed = self.plugins.remove(idx);
let raw: *mut dyn Plugin<State> = Box::into_raw(boxed);
// SAFETY: `is::<P>()` 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::<P>()) })
}
/// 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<State>, &mut Self)) {
fn plugin_dispatch(&mut self, mut f: impl FnMut(&mut dyn Plugin<State>, &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;
@@ -442,7 +413,7 @@ impl<'a, State: 'static> Harness<'a, State> {
}
fn _try_run(&mut self, sleep: bool) -> Result<u64, ExceededMaxStepsError> {
self.dispatch(|p, h| p.before_run(h));
self.plugin_dispatch(|p, h| p.before_run(h));
let mut steps = 0;
let result = loop {
@@ -464,7 +435,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
}
@@ -566,12 +537,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.
@@ -797,7 +773,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)
}
@@ -890,7 +866,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);
}
}
@@ -898,9 +874,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);
}
}
}
@@ -967,10 +943,10 @@ impl<State: 'static> 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));
}
}
}

View File

@@ -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<State = ()>: Send + Any {
pub trait Plugin<State = ()>: 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>) {}

View File

@@ -628,7 +628,7 @@ impl<State: 'static> 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
}

View File

@@ -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::<CountingPlugin>().is_some());
}
/// Downcasting via `plugin::<P>()` / `plugin_mut::<P>()` / `take_plugin::<P>()`.
#[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::<CountingPlugin>().is_some());
assert!(harness.plugin_mut::<CountingPlugin>().is_some());
let taken = harness.take_plugin::<CountingPlugin>();
assert!(taken.is_some());
assert!(harness.plugin::<CountingPlugin>().is_none());
}
/// When `Harness::drop` fires while a panic is unwinding, `on_test_result` gets `Fail`.