Skip to content

Commit

Permalink
egui_kittest: Allow passing state to the app closure (#5313)
Browse files Browse the repository at this point in the history
The allows us to pass any state to the ui closure. While it is possible
to just store state in the closure itself, accessing that state after
the harness was created to e.g. read or modify it would require interior
mutability. With this change there are new `Harness::new_state`,
`Harness::run_state`, ... methods that allow passing state on each run.

This builds on top of #5301, which should be merged first

---------

Co-authored-by: Emil Ernerfeldt <[email protected]>
  • Loading branch information
lucasmerlin and emilk authored Nov 6, 2024
1 parent fc743d6 commit 3c7ad0e
Show file tree
Hide file tree
Showing 6 changed files with 215 additions and 56 deletions.
16 changes: 10 additions & 6 deletions crates/egui_demo_lib/src/demo/text_edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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!");
}
}
42 changes: 28 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,34 @@ pub(crate) enum AppKind<'a> {
// }
// }

impl<'a> AppKind<'a> {
pub fn run(&mut self, ctx: &egui::Context) -> Option<egui::Response> {
impl<'a, State> AppKind<'a, State> {
pub fn run(
&mut self,
ctx: &egui::Context,
state: &mut State,
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 State,
sizing_pass: bool,
) -> egui::Response {
egui::CentralPanel::default()
.frame(Frame::none())
.show(ctx, |ui| {
Expand All @@ -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
})
Expand Down
80 changes: 73 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<State = ()> {
pub(crate) screen_rect: Rect,
pub(crate) pixels_per_point: f32,
pub(crate) state: PhantomData<State>,
}

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

0 comments on commit 3c7ad0e

Please sign in to comment.