#![doc = include_str!("../README.md")]
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
mod builder;
#[cfg(feature = "snapshot")]
mod snapshot;
#[cfg(feature = "snapshot")]
pub use snapshot::*;
use std::fmt::{Debug, Display, Formatter};
use std::time::Duration;
mod app_kind;
mod node;
mod renderer;
#[cfg(feature = "wgpu")]
mod texture_to_image;
#[cfg(feature = "wgpu")]
pub mod wgpu;
pub use kittest;
use crate::app_kind::AppKind;
pub use builder::*;
pub use node::*;
pub use renderer::*;
use egui::epaint::{ClippedShape, RectShape};
use egui::style::ScrollAnimation;
use egui::{Color32, Key, Modifiers, Pos2, Rect, RepaintCause, Shape, Vec2, ViewportId};
use kittest::Queryable;
#[derive(Debug, Clone)]
pub struct ExceededMaxStepsError {
pub max_steps: u64,
pub repaint_causes: Vec<RepaintCause>,
}
impl Display for ExceededMaxStepsError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Harness::run exceeded max_steps ({}). If your expect your ui to keep repainting \
(e.g. when showing a spinner) call Harness::step or Harness::run_steps instead.\
\nRepaint causes: {:#?}",
self.max_steps, self.repaint_causes,
)
}
}
pub struct Harness<'a, State = ()> {
pub ctx: egui::Context,
input: egui::RawInput,
kittest: kittest::State,
output: egui::FullOutput,
app: AppKind<'a, State>,
response: Option<egui::Response>,
state: State,
renderer: Box<dyn TestRenderer>,
max_steps: u64,
step_dt: f32,
wait_for_pending_images: bool,
queued_events: EventQueue,
}
impl<State> Debug for Harness<'_, State> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
self.kittest.fmt(f)
}
}
impl<'a, State> Harness<'a, State> {
pub(crate) fn from_builder(
builder: HarnessBuilder<State>,
mut app: AppKind<'a, State>,
mut state: State,
ctx: Option<egui::Context>,
) -> Self {
let HarnessBuilder {
screen_rect,
pixels_per_point,
theme,
max_steps,
step_dt,
state: _,
mut renderer,
wait_for_pending_images,
} = builder;
let ctx = ctx.unwrap_or_default();
ctx.set_theme(theme);
ctx.enable_accesskit();
ctx.all_styles_mut(|style| {
style.visuals.text_cursor.blink = false;
style.scroll_animation = ScrollAnimation::none();
style.animation_time = 0.0;
});
let mut input = egui::RawInput {
screen_rect: Some(screen_rect),
..Default::default()
};
let viewport = input.viewports.get_mut(&ViewportId::ROOT).unwrap();
viewport.native_pixels_per_point = Some(pixels_per_point);
let mut response = None;
let mut output = ctx.run(input.clone(), |ctx| {
response = app.run(ctx, &mut state, false);
});
renderer.handle_delta(&output.textures_delta);
let mut harness = Self {
app,
ctx,
input,
kittest: kittest::State::new(
output
.platform_output
.accesskit_update
.take()
.expect("AccessKit was disabled"),
),
output,
response,
state,
renderer,
max_steps,
step_dt,
wait_for_pending_images,
queued_events: Default::default(),
};
harness.run_ok();
harness
}
pub fn builder() -> HarnessBuilder<State> {
HarnessBuilder::default()
}
pub fn new_state(app: impl FnMut(&egui::Context, &mut State) + 'a, state: State) -> Self {
Self::builder().build_state(app, state)
}
pub fn new_ui_state(app: impl FnMut(&mut egui::Ui, &mut State) + 'a, state: State) -> Self {
Self::builder().build_ui_state(app, state)
}
#[cfg(feature = "eframe")]
pub fn new_eframe(builder: impl FnOnce(&mut eframe::CreationContext<'a>) -> State) -> Self
where
State: eframe::App,
{
Self::builder().build_eframe(builder)
}
#[inline]
pub fn set_size(&mut self, size: Vec2) -> &mut Self {
self.input.screen_rect = Some(Rect::from_min_size(Pos2::ZERO, size));
self
}
#[inline]
pub fn set_pixels_per_point(&mut self, pixels_per_point: f32) -> &mut Self {
self.ctx.set_pixels_per_point(pixels_per_point);
self
}
pub fn step(&mut self) {
let events = std::mem::take(&mut *self.queued_events.lock());
if events.is_empty() {
self._step(false);
}
for event in events {
match event {
EventType::Event(event) => {
self.input.events.push(event);
}
EventType::Modifiers(modifiers) => {
self.input.modifiers = modifiers;
}
}
self._step(false);
}
}
fn _step(&mut self, sizing_pass: bool) {
self.input.predicted_dt = self.step_dt;
let mut output = self.ctx.run(self.input.take(), |ctx| {
self.response = self.app.run(ctx, &mut self.state, sizing_pass);
});
self.kittest.update(
output
.platform_output
.accesskit_update
.take()
.expect("AccessKit was disabled"),
);
self.renderer.handle_delta(&output.textures_delta);
self.output = output;
}
pub fn fit_contents(&mut self) {
self._step(true);
if let Some(response) = &self.response {
self.set_size(response.rect.size());
}
self.run_ok();
}
#[track_caller]
pub fn run(&mut self) -> u64 {
match self.try_run() {
Ok(steps) => steps,
Err(err) => {
panic!("{err}");
}
}
}
fn _try_run(&mut self, sleep: bool) -> Result<u64, ExceededMaxStepsError> {
let mut steps = 0;
loop {
steps += 1;
self.step();
let wait_for_images = self.wait_for_pending_images && self.ctx.has_pending_images();
if self.root_viewport_output().repaint_delay != Duration::ZERO && !wait_for_images {
break;
} else if sleep || wait_for_images {
std::thread::sleep(Duration::from_secs_f32(self.step_dt));
}
if steps > self.max_steps {
return Err(ExceededMaxStepsError {
max_steps: self.max_steps,
repaint_causes: self.ctx.repaint_causes(),
});
}
}
Ok(steps)
}
pub fn try_run(&mut self) -> Result<u64, ExceededMaxStepsError> {
self._try_run(false)
}
pub fn run_ok(&mut self) -> Option<u64> {
self.try_run().ok()
}
pub fn try_run_realtime(&mut self) -> Result<u64, ExceededMaxStepsError> {
self._try_run(true)
}
pub fn run_steps(&mut self, steps: usize) {
for _ in 0..steps {
self.step();
}
}
pub fn input(&self) -> &egui::RawInput {
&self.input
}
pub fn input_mut(&mut self) -> &mut egui::RawInput {
&mut self.input
}
pub fn output(&self) -> &egui::FullOutput {
&self.output
}
pub fn kittest_state(&self) -> &kittest::State {
&self.kittest
}
pub fn state(&self) -> &State {
&self.state
}
pub fn state_mut(&mut self) -> &mut State {
&mut self.state
}
fn event(&self, event: egui::Event) {
self.queued_events.lock().push(EventType::Event(event));
}
fn event_modifiers(&self, event: egui::Event, modifiers: Modifiers) {
let mut queue = self.queued_events.lock();
queue.push(EventType::Modifiers(modifiers));
queue.push(EventType::Event(event));
queue.push(EventType::Modifiers(Modifiers::default()));
}
fn modifiers(&self, modifiers: Modifiers) {
self.queued_events
.lock()
.push(EventType::Modifiers(modifiers));
}
pub fn key_down(&self, key: egui::Key) {
self.event(egui::Event::Key {
key,
pressed: true,
modifiers: Modifiers::default(),
repeat: false,
physical_key: None,
});
}
pub fn key_down_modifiers(&self, modifiers: Modifiers, key: egui::Key) {
self.event_modifiers(
egui::Event::Key {
key,
pressed: true,
modifiers,
repeat: false,
physical_key: None,
},
modifiers,
);
}
pub fn key_up(&self, key: egui::Key) {
self.event(egui::Event::Key {
key,
pressed: false,
modifiers: Modifiers::default(),
repeat: false,
physical_key: None,
});
}
pub fn key_up_modifiers(&self, modifiers: Modifiers, key: egui::Key) {
self.event_modifiers(
egui::Event::Key {
key,
pressed: false,
modifiers,
repeat: false,
physical_key: None,
},
modifiers,
);
}
pub fn key_combination(&self, keys: &[Key]) {
for key in keys {
self.key_down(*key);
}
for key in keys.iter().rev() {
self.key_up(*key);
}
}
pub fn key_combination_modifiers(&self, modifiers: Modifiers, keys: &[Key]) {
self.modifiers(modifiers);
for pressed in [true, false] {
for key in keys {
self.event(egui::Event::Key {
key: *key,
pressed,
modifiers,
repeat: false,
physical_key: None,
});
}
}
self.modifiers(Modifiers::default());
}
pub fn key_press(&self, key: egui::Key) {
self.key_combination(&[key]);
}
pub fn key_press_modifiers(&self, modifiers: Modifiers, key: egui::Key) {
self.key_combination_modifiers(modifiers, &[key]);
}
pub fn mask(&mut self, rect: Rect) {
self.output.shapes.push(ClippedShape {
clip_rect: Rect::EVERYTHING,
shape: Shape::Rect(RectShape::filled(rect, 0.0, Color32::MAGENTA)),
});
}
#[cfg(any(feature = "wgpu", feature = "snapshot"))]
pub fn render(&mut self) -> Result<image::RgbaImage, String> {
self.renderer.render(&self.ctx, &self.output)
}
fn root_viewport_output(&self) -> &egui::ViewportOutput {
self.output
.viewport_output
.get(&ViewportId::ROOT)
.expect("Missing root viewport")
}
pub fn root(&self) -> Node<'_> {
Node {
accesskit_node: self.kittest.root(),
queue: &self.queued_events,
}
}
#[deprecated = "Use `Harness::root` instead."]
pub fn node(&self) -> Node<'_> {
self.root()
}
}
impl<'a> Harness<'a> {
pub fn new(app: impl FnMut(&egui::Context) + 'a) -> Self {
Self::builder().build(app)
}
pub fn new_ui(app: impl FnMut(&mut egui::Ui) + 'a) -> Self {
Self::builder().build_ui(app)
}
}
impl<'tree, 'node, State> Queryable<'tree, 'node, Node<'tree>> for Harness<'_, State>
where
'node: 'tree,
{
fn queryable_node(&'node self) -> Node<'tree> {
self.root()
}
}