diff --git a/crates/eframe/src/lib.rs b/crates/eframe/src/lib.rs index 597bfd98d..f0d049858 100644 --- a/crates/eframe/src/lib.rs +++ b/crates/eframe/src/lib.rs @@ -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, 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) } } } diff --git a/crates/eframe/src/native/epi_integration.rs b/crates/eframe/src/native/epi_integration.rs index e558b0a2b..be7e3e787 100644 --- a/crates/eframe/src/native/epi_integration.rs +++ b/crates/eframe/src/native/epi_integration.rs @@ -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, } } diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index 1d94dd8d5..fd61b0cd1 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -55,6 +55,10 @@ pub struct GlowWinitApp<'app> { // re-initializing the `GlowWinitRunning` state on Android if the application // suspends and resumes. app_creator: Option>, + + /// 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, } /// State that is initialized when the application is first starts running via @@ -128,6 +132,7 @@ impl<'app> GlowWinitApp<'app> { event_loop: &EventLoop, app_name: &str, native_options: NativeOptions, + egui_ctx: Option, 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, diff --git a/crates/eframe/src/native/run.rs b/crates/eframe/src/native/run.rs index 73b58ae61..b89212aa2 100644 --- a/crates/eframe/src/native/run.rs +++ b/crates/eframe/src/native/run.rs @@ -399,6 +399,7 @@ fn run_and_exit(event_loop: EventLoop, winit_app: impl WinitApp) -> R pub fn run_glow( app_name: &str, mut native_options: epi::NativeOptions, + egui_ctx: Option, 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 + '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, 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 + '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) } diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index de691153f..161c3f84b 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -48,6 +48,10 @@ pub struct WgpuWinitApp<'app> { /// Set when we are actually up and running. running: Option>, + + /// 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, } /// State that is initialized when the application is first starts running via @@ -105,6 +109,7 @@ impl<'app> WgpuWinitApp<'app> { event_loop: &EventLoop, app_name: &str, native_options: NativeOptions, + egui_ctx: Option, 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, diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 3526f92be..bbb22f0ab 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -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, diff --git a/crates/egui_kittest/src/app_kind.rs b/crates/egui_kittest/src/app_kind.rs index a1277d036..42edd6d66 100644 --- a/crates/egui_kittest/src/app_kind.rs +++ b/crates/egui_kittest/src/app_kind.rs @@ -4,17 +4,21 @@ type AppKindUiState<'a, State> = Box; type AppKindUi<'a> = Box; /// 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 { + pub get_app: fn(&mut State) -> &mut dyn eframe::App, + pub take_app: fn(State) -> Box, + 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), } impl AppKind<'_, State> { @@ -26,13 +30,10 @@ impl AppKind<'_, State> { ) -> Option { 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)), diff --git a/crates/egui_kittest/src/builder.rs b/crates/egui_kittest/src/builder.rs index 6d4853410..58bfb8601 100644 --- a/crates/egui_kittest/src/builder.rs +++ b/crates/egui_kittest/src/builder.rs @@ -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 HarnessBuilder { 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 HarnessBuilder { 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)) } } diff --git a/crates/egui_kittest/src/lib.rs b/crates/egui_kittest/src/lib.rs index 507829d3d..4fa68d93c 100644 --- a/crates/egui_kittest/src/lib.rs +++ b/crates/egui_kittest/src/lib.rs @@ -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: + #[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, + } + + impl eframe::App for UiApp { + fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) { + (self.f)(ui); + } + } + + struct UiStateApp { + f: Box, + state: State, + } + + impl eframe::App for UiStateApp { + 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 = 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. diff --git a/crates/emath/src/history.rs b/crates/emath/src/history.rs index 6aafd0af1..c25b94147 100644 --- a/crates/emath/src/history.rs +++ b/crates/emath/src/history.rs @@ -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));