Skip to content

Commit

Permalink
egui_kittest: Allow passing state to the app closure
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasmerlin committed Oct 29, 2024
1 parent c93846c commit 1c60bb5
Show file tree
Hide file tree
Showing 4 changed files with 234 additions and 64 deletions.
20 changes: 12 additions & 8 deletions crates/egui_demo_lib/src/demo/text_edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,27 +122,31 @@ 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 mut harness = Harness::new_state(
move |ctx, text| {
CentralPanel::default().show(ctx, |ui| {
ui.text_edit_singleline(text);
});
},
&mut text,
);

harness.run();
harness.run_state(&mut text);

let text_edit = harness.get_by_role(accesskit::Role::TextInput);
assert_eq!(text_edit.value().as_deref(), Some("Hello, world!"));

text_edit.key_combination(&[Key::Command, Key::A]);
text_edit.type_text("Hi ");

harness.run();
harness.run_state(&mut text);
harness
.get_by_role(accesskit::Role::TextInput)
.type_text("there!");

harness.run();
harness.run_state(&mut text);
let text_edit = harness.get_by_role(accesskit::Role::TextInput);
assert_eq!(text_edit.value().as_deref(), Some("Hi there!"));
assert_eq!(text, "Hi there!");
}
}
37 changes: 23 additions & 14 deletions crates/egui_kittest/src/app_kind.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
use egui::Frame;

type AppKindContextState<'a, State> = Box<dyn FnMut(&egui::Context, &mut State) + 'a>;
type AppKindUiState<'a, State> = Box<dyn FnMut(&mut egui::Ui, &mut State) + 'a>;
type AppKindContext<'a> = Box<dyn FnMut(&egui::Context) + 'a>;
type AppKindUi<'a> = Box<dyn FnMut(&mut egui::Ui) + 'a>;

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 :(
Expand All @@ -32,28 +36,29 @@ pub(crate) enum AppKind<'a> {
// }
// }

impl<'a> AppKind<'a> {
pub fn run(&mut self, ctx: &egui::Context) -> Option<egui::Response> {
impl<'a, S> AppKind<'a, S> {
pub fn run(
&mut self,
ctx: &egui::Context,
state: &mut S,
sizing_pass: bool,
) -> Option<egui::Response> {
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<egui::Response> {
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 S, sizing_pass: bool) -> egui::Response {
egui::CentralPanel::default()
.frame(Frame::none())
.show(ctx, |ui| {
Expand All @@ -65,7 +70,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
})
Expand Down
88 changes: 81 additions & 7 deletions crates/egui_kittest/src/builder.rs
Original file line number Diff line number Diff line change
@@ -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<S = ()> {
pub(crate) screen_rect: Rect,
pub(crate) pixels_per_point: f32,
pub(crate) state: PhantomData<S>,
}

impl Default for HarnessBuilder {
impl<S> Default for HarnessBuilder<S> {
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<S> HarnessBuilder<S> {
/// Set the size of the window.
#[inline]
pub fn with_size(mut self, size: impl Into<Vec2>) -> Self {
Expand All @@ -34,6 +37,73 @@ impl HarnessBuilder {
self
}

/// Create a new Harness with the given app closure and a state.
/// Use the [`Harness::run_state`], [`Harness::step_state`], etc... methods to run the app with
/// your 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 mut 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!");
/// });
/// }, &mut checked);
///
/// harness.get_by_name("Check me!").click();
/// harness.run_state(&mut checked);
///
/// assert_eq!(checked, true);
/// ```
pub fn build_state<'a>(
self,
app: impl FnMut(&egui::Context, &mut S) + 'a,
state: &mut S,
) -> Harness<'a, S> {
Harness::from_builder(&self, AppKind::ContextState(Box::new(app)), state)
}

/// Create a new Harness with the given ui closure and a state.
/// Use the [`Harness::run_state`], [`Harness::step_state`], etc... methods to run the app with
/// your 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!");
/// }, &mut checked);
///
/// harness.get_by_name("Check me!").click();
/// harness.run_state(&mut checked);
///
/// assert_eq!(checked, true);
/// ```
pub fn build_ui_state<'a>(
self,
app: impl FnMut(&mut egui::Ui, &mut S) + 'a,
state: &mut S,
) -> Harness<'a, S> {
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.
Expand All @@ -43,7 +113,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| {
Expand All @@ -53,7 +123,11 @@ 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)),
&mut Default::default(),
)
}

/// Create a new Harness with the given ui closure.
Expand All @@ -64,14 +138,14 @@ 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| {
/// ui.label("Hello, world!");
/// });
/// ```
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)), &mut Default::default())
}
}
Loading

0 comments on commit 1c60bb5

Please sign in to comment.