mirror of
https://github.com/emilk/egui.git
synced 2026-06-26 14:49:06 -04:00
Add Harness::spawn_eframe_app (#8120)
This lets you start up the test app from within the test itself, which can be very useful when you have a specific test scenario set up that you need to debug. ### Related * Previous attempt: https://github.com/emilk/egui/pull/5418 ### macOS On macOS, you may only run UIs on the main loop, so you need a few additional steps. Not ideal, but works! ```diff diff --git a/crates/egui_demo_app/Cargo.toml b/crates/egui_demo_app/Cargo.toml index f9a153268..4e0cc14ee 100644 --- a/crates/egui_demo_app/Cargo.toml +++ b/crates/egui_demo_app/Cargo.toml @@ -84,3 +84,7 @@ web-sys.workspace = true [dev-dependencies] egui_kittest = { workspace = true, features = ["eframe", "snapshot", "wgpu"] } + +[[test]] +name = "test_demo_app" +harness = false diff --git a/crates/egui_demo_app/tests/test_demo_app.rs b/crates/egui_demo_app/tests/test_demo_app.rs index e083c8455..7ad9ed516 100644 --- a/crates/egui_demo_app/tests/test_demo_app.rs +++ b/crates/egui_demo_app/tests/test_demo_app.rs @@ -4,7 +4,10 @@ use egui_demo_app::{Anchor, WrapApp}; use egui_kittest::SnapshotResults; use egui_kittest::kittest::Queryable as _; -#[test] +fn main() { + test_demo_app(); +} + fn test_demo_app() { let mut harness = egui_kittest::Harness::builder() .with_size(Vec2::new(900.0, 600.0)) @@ -73,5 +76,8 @@ fn test_demo_app() { harness.run_steps(4); results.add(harness.try_snapshot(anchor.to_string())); + + harness.spawn_eframe_app(); + break; } } ```
This commit is contained in:
@@ -257,8 +257,27 @@ pub mod icon_data;
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
#[allow(clippy::allow_attributes, clippy::needless_pass_by_value)]
|
||||
pub fn run_native(
|
||||
app_name: &str,
|
||||
native_options: NativeOptions,
|
||||
app_creator: AppCreator<'_>,
|
||||
) -> Result {
|
||||
run_native_ext(app_name, native_options, None, app_creator)
|
||||
}
|
||||
|
||||
/// Like [`run_native`], but lets you supply a pre-existing [`egui::Context`].
|
||||
///
|
||||
/// If `egui_ctx` is `Some`, that context will be used by eframe instead of creating a fresh one.
|
||||
/// If it is `None`, eframe creates a new context (same behavior as [`run_native`]).
|
||||
///
|
||||
/// # Errors
|
||||
/// This function can fail if we fail to set up a graphics context.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
#[allow(clippy::allow_attributes, clippy::needless_pass_by_value)]
|
||||
pub fn run_native_ext(
|
||||
app_name: &str,
|
||||
mut native_options: NativeOptions,
|
||||
egui_ctx: Option<egui::Context>,
|
||||
app_creator: AppCreator<'_>,
|
||||
) -> Result {
|
||||
let renderer = init_native(app_name, &mut native_options);
|
||||
@@ -267,13 +286,13 @@ pub fn run_native(
|
||||
#[cfg(feature = "glow")]
|
||||
Renderer::Glow => {
|
||||
log::debug!("Using the glow renderer");
|
||||
native::run::run_glow(app_name, native_options, app_creator)
|
||||
native::run::run_glow(app_name, native_options, egui_ctx, app_creator)
|
||||
}
|
||||
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
Renderer::Wgpu => {
|
||||
log::debug!("Using the wgpu renderer");
|
||||
native::run::run_wgpu(app_name, native_options, app_creator)
|
||||
native::run::run_wgpu(app_name, native_options, egui_ctx, app_creator)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,15 +214,17 @@ impl EpiIntegration {
|
||||
Self {
|
||||
frame,
|
||||
last_auto_save: Instant::now(),
|
||||
egui_ctx,
|
||||
pending_full_output: Default::default(),
|
||||
close: false,
|
||||
can_drag_window: false,
|
||||
#[cfg(feature = "persistence")]
|
||||
persist_window: native_options.persist_window,
|
||||
app_icon_setter,
|
||||
beginning: Instant::now(),
|
||||
beginning: Instant::now()
|
||||
.checked_sub(web_time::Duration::from_secs_f64(egui_ctx.time()))
|
||||
.unwrap_or_else(Instant::now),
|
||||
is_first_frame: true,
|
||||
egui_ctx,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,10 @@ pub struct GlowWinitApp<'app> {
|
||||
// re-initializing the `GlowWinitRunning` state on Android if the application
|
||||
// suspends and resumes.
|
||||
app_creator: Option<AppCreator<'app>>,
|
||||
|
||||
/// An optional pre-existing egui context. If `Some`, it is used instead of
|
||||
/// creating a new one via [`create_egui_context`]. Taken during initialization.
|
||||
egui_ctx: Option<egui::Context>,
|
||||
}
|
||||
|
||||
/// State that is initialized when the application is first starts running via
|
||||
@@ -128,6 +132,7 @@ impl<'app> GlowWinitApp<'app> {
|
||||
event_loop: &EventLoop<UserEvent>,
|
||||
app_name: &str,
|
||||
native_options: NativeOptions,
|
||||
egui_ctx: Option<egui::Context>,
|
||||
app_creator: AppCreator<'app>,
|
||||
) -> Self {
|
||||
profiling::function_scope!();
|
||||
@@ -137,6 +142,7 @@ impl<'app> GlowWinitApp<'app> {
|
||||
native_options,
|
||||
running: None,
|
||||
app_creator: Some(app_creator),
|
||||
egui_ctx,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,7 +215,10 @@ impl<'app> GlowWinitApp<'app> {
|
||||
)
|
||||
};
|
||||
|
||||
let egui_ctx = create_egui_context(storage.as_deref());
|
||||
let egui_ctx = self
|
||||
.egui_ctx
|
||||
.take()
|
||||
.unwrap_or_else(|| create_egui_context(storage.as_deref()));
|
||||
|
||||
let (mut glutin, painter) = Self::create_glutin_windowed_context(
|
||||
&egui_ctx,
|
||||
|
||||
@@ -399,6 +399,7 @@ fn run_and_exit(event_loop: EventLoop<UserEvent>, winit_app: impl WinitApp) -> R
|
||||
pub fn run_glow(
|
||||
app_name: &str,
|
||||
mut native_options: epi::NativeOptions,
|
||||
egui_ctx: Option<egui::Context>,
|
||||
app_creator: epi::AppCreator<'_>,
|
||||
) -> Result {
|
||||
use super::glow_integration::GlowWinitApp;
|
||||
@@ -406,13 +407,15 @@ pub fn run_glow(
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
if native_options.run_and_return {
|
||||
return with_event_loop(native_options, |event_loop, native_options| {
|
||||
let glow_eframe = GlowWinitApp::new(event_loop, app_name, native_options, app_creator);
|
||||
let glow_eframe =
|
||||
GlowWinitApp::new(event_loop, app_name, native_options, egui_ctx, app_creator);
|
||||
run_and_return(event_loop, glow_eframe)
|
||||
})?;
|
||||
}
|
||||
|
||||
let event_loop = create_event_loop(&mut native_options)?;
|
||||
let glow_eframe = GlowWinitApp::new(&event_loop, app_name, native_options, app_creator);
|
||||
let glow_eframe =
|
||||
GlowWinitApp::new(&event_loop, app_name, native_options, egui_ctx, app_creator);
|
||||
run_and_exit(event_loop, glow_eframe)
|
||||
}
|
||||
|
||||
@@ -425,7 +428,7 @@ pub fn create_glow<'a>(
|
||||
) -> impl ApplicationHandler<UserEvent> + 'a {
|
||||
use super::glow_integration::GlowWinitApp;
|
||||
|
||||
let glow_eframe = GlowWinitApp::new(event_loop, app_name, native_options, app_creator);
|
||||
let glow_eframe = GlowWinitApp::new(event_loop, app_name, native_options, None, app_creator);
|
||||
WinitAppWrapper::new(glow_eframe, true)
|
||||
}
|
||||
|
||||
@@ -435,6 +438,7 @@ pub fn create_glow<'a>(
|
||||
pub fn run_wgpu(
|
||||
app_name: &str,
|
||||
mut native_options: epi::NativeOptions,
|
||||
egui_ctx: Option<egui::Context>,
|
||||
app_creator: epi::AppCreator<'_>,
|
||||
) -> Result {
|
||||
use super::wgpu_integration::WgpuWinitApp;
|
||||
@@ -442,13 +446,15 @@ pub fn run_wgpu(
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
if native_options.run_and_return {
|
||||
return with_event_loop(native_options, |event_loop, native_options| {
|
||||
let wgpu_eframe = WgpuWinitApp::new(event_loop, app_name, native_options, app_creator);
|
||||
let wgpu_eframe =
|
||||
WgpuWinitApp::new(event_loop, app_name, native_options, egui_ctx, app_creator);
|
||||
run_and_return(event_loop, wgpu_eframe)
|
||||
})?;
|
||||
}
|
||||
|
||||
let event_loop = create_event_loop(&mut native_options)?;
|
||||
let wgpu_eframe = WgpuWinitApp::new(&event_loop, app_name, native_options, app_creator);
|
||||
let wgpu_eframe =
|
||||
WgpuWinitApp::new(&event_loop, app_name, native_options, egui_ctx, app_creator);
|
||||
run_and_exit(event_loop, wgpu_eframe)
|
||||
}
|
||||
|
||||
@@ -461,7 +467,7 @@ pub fn create_wgpu<'a>(
|
||||
) -> impl ApplicationHandler<UserEvent> + 'a {
|
||||
use super::wgpu_integration::WgpuWinitApp;
|
||||
|
||||
let wgpu_eframe = WgpuWinitApp::new(event_loop, app_name, native_options, app_creator);
|
||||
let wgpu_eframe = WgpuWinitApp::new(event_loop, app_name, native_options, None, app_creator);
|
||||
WinitAppWrapper::new(wgpu_eframe, true)
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,10 @@ pub struct WgpuWinitApp<'app> {
|
||||
|
||||
/// Set when we are actually up and running.
|
||||
running: Option<WgpuWinitRunning<'app>>,
|
||||
|
||||
/// An optional pre-existing egui context. If `Some`, it is used instead of
|
||||
/// creating a new one via [`winit_integration::create_egui_context`]. Taken during initialization.
|
||||
egui_ctx: Option<egui::Context>,
|
||||
}
|
||||
|
||||
/// State that is initialized when the application is first starts running via
|
||||
@@ -105,6 +109,7 @@ impl<'app> WgpuWinitApp<'app> {
|
||||
event_loop: &EventLoop<UserEvent>,
|
||||
app_name: &str,
|
||||
native_options: NativeOptions,
|
||||
egui_ctx: Option<egui::Context>,
|
||||
app_creator: AppCreator<'app>,
|
||||
) -> Self {
|
||||
profiling::function_scope!();
|
||||
@@ -121,6 +126,7 @@ impl<'app> WgpuWinitApp<'app> {
|
||||
native_options,
|
||||
running: None,
|
||||
app_creator: Some(app_creator),
|
||||
egui_ctx,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -428,7 +434,10 @@ impl WinitApp for WgpuWinitApp<'_> {
|
||||
.unwrap_or(&self.app_name),
|
||||
)
|
||||
};
|
||||
let egui_ctx = winit_integration::create_egui_context(storage.as_deref());
|
||||
let egui_ctx = self
|
||||
.egui_ctx
|
||||
.take()
|
||||
.unwrap_or_else(|| winit_integration::create_egui_context(storage.as_deref()));
|
||||
let (window, builder) = create_window(
|
||||
&egui_ctx,
|
||||
event_loop,
|
||||
|
||||
@@ -132,9 +132,11 @@ impl State {
|
||||
};
|
||||
|
||||
let mut slf = Self {
|
||||
egui_ctx,
|
||||
viewport_id,
|
||||
start_time: web_time::Instant::now(),
|
||||
start_time: web_time::Instant::now()
|
||||
.checked_sub(web_time::Duration::from_secs_f64(egui_ctx.time()))
|
||||
.unwrap_or_else(web_time::Instant::now),
|
||||
egui_ctx,
|
||||
egui_input,
|
||||
pointer_pos_in_points: None,
|
||||
any_pointer_button_down: false,
|
||||
|
||||
@@ -4,17 +4,21 @@ type AppKindUiState<'a, State> = Box<dyn FnMut(&mut egui::Ui, &mut State) + 'a>;
|
||||
type AppKindUi<'a> = Box<dyn FnMut(&mut egui::Ui) + 'a>;
|
||||
|
||||
/// In order to access the [`eframe::App`] trait from the generic `State`, we store a function pointer
|
||||
/// here that will return the dyn trait from the struct. In the builder we have the correct where
|
||||
/// clause to be able to create this.
|
||||
/// here that will return the dyn trait from the struct.
|
||||
/// In the builder we have the correct `where`-clause to be able to create this.
|
||||
/// Later we can use it anywhere to get the [`eframe::App`] from the `State`.
|
||||
#[cfg(feature = "eframe")]
|
||||
type AppKindEframe<'a, State> = (fn(&mut State) -> &mut dyn eframe::App, eframe::Frame);
|
||||
pub(crate) struct AppKindEframe<State> {
|
||||
pub get_app: fn(&mut State) -> &mut dyn eframe::App,
|
||||
pub take_app: fn(State) -> Box<dyn eframe::App>,
|
||||
pub frame: eframe::Frame,
|
||||
}
|
||||
|
||||
pub(crate) enum AppKind<'a, State> {
|
||||
Ui(AppKindUi<'a>),
|
||||
UiState(AppKindUiState<'a, State>),
|
||||
#[cfg(feature = "eframe")]
|
||||
Eframe(AppKindEframe<'a, State>),
|
||||
Eframe(AppKindEframe<State>),
|
||||
}
|
||||
|
||||
impl<State> AppKind<'_, State> {
|
||||
@@ -26,13 +30,10 @@ impl<State> AppKind<'_, State> {
|
||||
) -> Option<egui::Response> {
|
||||
match self {
|
||||
#[cfg(feature = "eframe")]
|
||||
AppKind::Eframe((get_app, frame)) => {
|
||||
AppKind::Eframe(AppKindEframe { get_app, frame, .. }) => {
|
||||
let app = get_app(state);
|
||||
|
||||
app.logic(ui, frame);
|
||||
|
||||
app.ui(ui, frame);
|
||||
|
||||
None
|
||||
}
|
||||
kind_ui => Some(kind_ui.run_ui(ui, state, sizing_pass)),
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use crate::app_kind::AppKind;
|
||||
#[cfg(feature = "eframe")]
|
||||
use crate::app_kind::AppKindEframe;
|
||||
use crate::{Harness, LazyRenderer, TestRenderer};
|
||||
use egui::{Pos2, Rect, Vec2};
|
||||
use std::marker::PhantomData;
|
||||
@@ -196,7 +198,7 @@ impl<State> HarnessBuilder<State> {
|
||||
build: impl FnOnce(&mut eframe::CreationContext<'a>) -> State,
|
||||
) -> Harness<'a, State>
|
||||
where
|
||||
State: eframe::App,
|
||||
State: eframe::App + 'static,
|
||||
{
|
||||
let ctx = egui::Context::default();
|
||||
|
||||
@@ -207,7 +209,11 @@ impl<State> HarnessBuilder<State> {
|
||||
|
||||
let app = build(&mut cc);
|
||||
|
||||
let kind = AppKind::Eframe((|state| state, frame));
|
||||
let kind = AppKind::Eframe(AppKindEframe {
|
||||
get_app: |state| state,
|
||||
take_app: |state| Box::new(state),
|
||||
frame,
|
||||
});
|
||||
Harness::from_builder(self, kind, app, Some(ctx))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,7 +214,7 @@ impl<'a, State> Harness<'a, State> {
|
||||
#[track_caller]
|
||||
pub fn new_eframe(builder: impl FnOnce(&mut eframe::CreationContext<'a>) -> State) -> Self
|
||||
where
|
||||
State: eframe::App,
|
||||
State: eframe::App + 'static,
|
||||
{
|
||||
Self::builder().build_eframe(builder)
|
||||
}
|
||||
@@ -460,6 +460,11 @@ impl<'a, State> Harness<'a, State> {
|
||||
&mut self.state
|
||||
}
|
||||
|
||||
/// Consume the harness and return the state.
|
||||
pub fn into_state(self) -> State {
|
||||
self.state
|
||||
}
|
||||
|
||||
/// Queue an event to be processed in the next frame.
|
||||
pub fn event(&self, event: egui::Event) {
|
||||
self.queued_events.lock().push(EventType::Event(event));
|
||||
@@ -681,6 +686,110 @@ impl<'a, State> Harness<'a, State> {
|
||||
queue: &self.queued_events,
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawn a real native eframe window running this harness's app, reusing its [`egui::Context`].
|
||||
///
|
||||
/// Blocks until the window is closed.
|
||||
///
|
||||
/// Useful for interactively debugging a failing test: add a call to this before the failing
|
||||
/// assertion to poke at the UI yourself.
|
||||
///
|
||||
/// # macOS: must be called on the main thread
|
||||
/// `AppKit` requires UI work to happen on the main thread, but by default cargo's test harness
|
||||
/// runs each test on a spawned worker thread, so this function will panic on macOS unless
|
||||
/// you opt out of the default harness.
|
||||
///
|
||||
/// To fix this, disable the default libtest harness for your test target and run tests on
|
||||
/// the main thread yourself. In `Cargo.toml`:
|
||||
///
|
||||
/// ```toml
|
||||
/// [[test]]
|
||||
/// name = "your_test"
|
||||
/// harness = false
|
||||
/// ```
|
||||
///
|
||||
/// Then write a `fn main()` in the test file that invokes your test directly.
|
||||
///
|
||||
/// See also: <https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-harness-field>
|
||||
#[cfg(feature = "eframe")]
|
||||
#[deprecated = "Only for debugging, don't commit this."]
|
||||
pub fn spawn_eframe_app(self)
|
||||
where
|
||||
'a: 'static,
|
||||
State: 'static,
|
||||
{
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// AppKit requires UI work to happen on the main thread, but by default cargo's
|
||||
// test harness runs each test on a spawned worker thread.
|
||||
#[expect(unsafe_code)]
|
||||
// SAFETY: `pthread_main_np` is a thread-safe libc query with no arguments.
|
||||
let is_main_thread = unsafe {
|
||||
unsafe extern "C" {
|
||||
fn pthread_main_np() -> std::ffi::c_int;
|
||||
}
|
||||
pthread_main_np() != 0
|
||||
};
|
||||
assert!(
|
||||
is_main_thread,
|
||||
"spawn_eframe_app must be called on the main thread on macOS, \
|
||||
but the default `cargo test` harness runs each test on a worker thread.\n\
|
||||
\n\
|
||||
To fix this, disable the default libtest harness for your test target and run \
|
||||
tests on the main thread yourself. In Cargo.toml:\n\
|
||||
\n\
|
||||
[[test]]\n\
|
||||
name = \"your_test\"\n\
|
||||
harness = false\n\
|
||||
\n\
|
||||
Then write a `fn main()` in the test file that invokes your test directly.\n\
|
||||
\n\
|
||||
See: https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-harness-field"
|
||||
);
|
||||
}
|
||||
|
||||
struct UiApp {
|
||||
f: Box<dyn FnMut(&mut egui::Ui)>,
|
||||
}
|
||||
|
||||
impl eframe::App for UiApp {
|
||||
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||
(self.f)(ui);
|
||||
}
|
||||
}
|
||||
|
||||
struct UiStateApp<State> {
|
||||
f: Box<dyn FnMut(&mut egui::Ui, &mut State)>,
|
||||
state: State,
|
||||
}
|
||||
|
||||
impl<State: 'static> eframe::App for UiStateApp<State> {
|
||||
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||
let Self { f, state } = self;
|
||||
f(ui, state);
|
||||
}
|
||||
}
|
||||
|
||||
use crate::app_kind::AppKindEframe;
|
||||
|
||||
let Self {
|
||||
ctx, state, app, ..
|
||||
} = self;
|
||||
|
||||
let eframe_app: Box<dyn eframe::App> = match app {
|
||||
AppKind::Ui(f) => Box::new(UiApp { f }),
|
||||
AppKind::UiState(f) => Box::new(UiStateApp { f, state }),
|
||||
AppKind::Eframe(AppKindEframe { take_app, .. }) => take_app(state),
|
||||
};
|
||||
|
||||
eframe::run_native_ext(
|
||||
"egui_kittest",
|
||||
eframe::NativeOptions::default(),
|
||||
Some(ctx),
|
||||
Box::new(|_cc| Ok(eframe_app)),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// Utilities for stateless harnesses.
|
||||
|
||||
@@ -126,7 +126,10 @@ where
|
||||
/// Values must be added with a monotonically increasing time, or at least not decreasing.
|
||||
pub fn add(&mut self, now: f64, value: T) {
|
||||
if let Some((last_time, _)) = self.values.back() {
|
||||
debug_assert!(*last_time <= now, "Time shouldn't move backwards");
|
||||
debug_assert!(
|
||||
*last_time <= now,
|
||||
"Time shouldn't move backwards. Last time: {last_time}, new time: {now}"
|
||||
);
|
||||
}
|
||||
self.total_count += 1;
|
||||
self.values.push_back((now, value));
|
||||
|
||||
Reference in New Issue
Block a user