Continue execution after closing native eframe window (#1889)

This adds `NativeOptions::run_and_return` with default value `true`.

If `true`, execution will continue after the eframe window is closed. This is a new behavior introduced in this PR.
If `false`, the app will close once the eframe window is closed. The is the old behavior.

This is `true` by default, and the `false` option is only there so we can revert if we find any bugs.

When `true`, [`winit::platform::run_return::EventLoopExtRunReturn::run_return`](https://docs.rs/winit/latest/winit/platform/run_return/trait.EventLoopExtRunReturn.html#tymethod.run_return) is used. The winit docs warns of its usage, recommending `EventLoop::run`, but 🤷 
When `false`, [`winit::event_loop::EventLoop::run`](https://docs.rs/winit/latest/winit/event_loop/struct.EventLoop.html#method.run) is used.

This is a really useful feature. You can now use `eframe` to quickly open up a window and show some data or options, and then continue your program after the `eframe` window is closed

My previous attempt at this caused some problems, but my new attempt seems to be working much better, at least on my Mac.
This commit is contained in:
Emil Ernerfeldt 2022-08-05 08:20:31 +02:00 committed by GitHub
parent e3f993d7b4
commit 66c601f775
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 533 additions and 251 deletions

View file

@ -269,6 +269,20 @@ pub struct NativeOptions {
///
/// Default: `Theme::Dark`.
pub default_theme: Theme,
/// This controls what happens when you close the main eframe window.
///
/// If `true`, execution will continue after the eframe window is closed.
/// If `false`, the app will close once the eframe window is closed.
///
/// This is `true` by default, and the `false` option is only there
/// so we can revert if we find any bugs.
///
/// This feature was introduced in <https://github.com/emilk/egui/pull/1889>.
///
/// When `true`, [`winit::platform::run_return::EventLoopExtRunReturn::run_return`] is used.
/// When `false`, [`winit::event_loop::EventLoop::run`] is used.
pub run_and_return: bool,
}
#[cfg(not(target_arch = "wasm32"))]
@ -295,6 +309,7 @@ impl Default for NativeOptions {
renderer: Renderer::default(),
follow_system_theme: cfg!(target_os = "macos") || cfg!(target_os = "windows"),
default_theme: Theme::Dark,
run_and_return: true,
}
}
}

View file

@ -161,20 +161,20 @@ mod native;
/// ```
#[cfg(not(target_arch = "wasm32"))]
#[allow(clippy::needless_pass_by_value)]
pub fn run_native(app_name: &str, native_options: NativeOptions, app_creator: AppCreator) -> ! {
pub fn run_native(app_name: &str, native_options: NativeOptions, app_creator: AppCreator) {
let renderer = native_options.renderer;
match renderer {
#[cfg(feature = "glow")]
Renderer::Glow => {
tracing::debug!("Using the glow renderer");
native::run::run_glow(app_name, &native_options, app_creator)
native::run::run_glow(app_name, &native_options, app_creator);
}
#[cfg(feature = "wgpu")]
Renderer::Wgpu => {
tracing::debug!("Using the wgpu renderer");
native::run::run_wgpu(app_name, &native_options, app_creator)
native::run::run_wgpu(app_name, &native_options, app_creator);
}
}
}

View file

@ -1,7 +1,16 @@
use super::epi_integration;
use crate::epi;
use egui_winit::winit;
//! Note that this file contains two similar paths - one for [`glow`], one for [`wgpu`].
//! When making changes to one you often also want to apply it to the other.
use std::time::Instant;
use std::{sync::Arc, time::Duration};
use egui_winit::winit;
use winit::event_loop::{ControlFlow, EventLoop};
use super::epi_integration::{self, EpiIntegration};
use crate::epi;
#[derive(Debug)]
struct RequestRepaintEvent;
#[cfg(feature = "glow")]
@ -9,7 +18,7 @@ struct RequestRepaintEvent;
fn create_display(
native_options: &NativeOptions,
window_builder: winit::window::WindowBuilder,
event_loop: &winit::event_loop::EventLoop<RequestRepaintEvent>,
event_loop: &EventLoop<RequestRepaintEvent>,
) -> (
glutin::WindowedContext<glutin::PossiblyCurrent>,
glow::Context,
@ -47,28 +56,180 @@ fn create_display(
pub use epi::NativeOptions;
enum EventResult {
Wait,
RepaintAsap,
RepaintAt(Instant),
Exit,
}
trait WinitApp {
fn is_focused(&self) -> bool;
fn integration(&self) -> &EpiIntegration;
fn window(&self) -> &winit::window::Window;
fn save_and_destroy(&mut self);
fn paint(&mut self) -> EventResult;
fn on_event(&mut self, event: winit::event::Event<'_, RequestRepaintEvent>) -> EventResult;
}
fn run_and_return(mut event_loop: EventLoop<RequestRepaintEvent>, mut winit_app: impl WinitApp) {
use winit::platform::run_return::EventLoopExtRunReturn as _;
tracing::debug!("event_loop.run_return");
let mut next_repaint_time = Instant::now();
event_loop.run_return(|event, _, control_flow| {
let event_result = match event {
winit::event::Event::LoopDestroyed => EventResult::Exit,
// Platform-dependent event handlers to workaround a winit bug
// See: https://github.com/rust-windowing/winit/issues/987
// See: https://github.com/rust-windowing/winit/issues/1619
winit::event::Event::RedrawEventsCleared if cfg!(windows) => {
next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000);
winit_app.paint()
}
winit::event::Event::RedrawRequested(_) if !cfg!(windows) => {
next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000);
winit_app.paint()
}
winit::event::Event::UserEvent(RequestRepaintEvent)
| winit::event::Event::NewEvents(winit::event::StartCause::ResumeTimeReached {
..
}) => EventResult::RepaintAsap,
event => winit_app.on_event(event),
};
match event_result {
EventResult::Wait => {}
EventResult::RepaintAsap => {
next_repaint_time = Instant::now();
}
EventResult::RepaintAt(repaint_time) => {
next_repaint_time = next_repaint_time.min(repaint_time);
}
EventResult::Exit => {
*control_flow = ControlFlow::Exit;
return;
}
}
*control_flow = match next_repaint_time.checked_duration_since(Instant::now()) {
None => {
winit_app.window().request_redraw();
ControlFlow::Poll
}
Some(time_until_next_repaint) => {
ControlFlow::WaitUntil(Instant::now() + time_until_next_repaint)
}
}
});
tracing::debug!("eframe window closed");
winit_app.save_and_destroy();
}
fn run_and_exit(
event_loop: EventLoop<RequestRepaintEvent>,
mut winit_app: impl WinitApp + 'static,
) -> ! {
tracing::debug!("event_loop.run");
let mut next_repaint_time = Instant::now();
event_loop.run(move |event, _, control_flow| {
let event_result = match event {
winit::event::Event::LoopDestroyed => EventResult::Exit,
// Platform-dependent event handlers to workaround a winit bug
// See: https://github.com/rust-windowing/winit/issues/987
// See: https://github.com/rust-windowing/winit/issues/1619
winit::event::Event::RedrawEventsCleared if cfg!(windows) => {
next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000);
winit_app.paint()
}
winit::event::Event::RedrawRequested(_) if !cfg!(windows) => {
next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000);
winit_app.paint()
}
winit::event::Event::UserEvent(RequestRepaintEvent)
| winit::event::Event::NewEvents(winit::event::StartCause::ResumeTimeReached {
..
}) => EventResult::RepaintAsap,
event => winit_app.on_event(event),
};
match event_result {
EventResult::Wait => {}
EventResult::RepaintAsap => {
next_repaint_time = Instant::now();
}
EventResult::RepaintAt(repaint_time) => {
next_repaint_time = next_repaint_time.min(repaint_time);
}
EventResult::Exit => {
tracing::debug!("Quitting…");
winit_app.save_and_destroy();
#[allow(clippy::exit)]
std::process::exit(0);
}
}
*control_flow = match next_repaint_time.checked_duration_since(Instant::now()) {
None => {
winit_app.window().request_redraw();
ControlFlow::Poll
}
Some(time_until_next_repaint) => {
ControlFlow::WaitUntil(Instant::now() + time_until_next_repaint)
}
}
})
}
// ----------------------------------------------------------------------------
/// Run an egui app
#[cfg(feature = "glow")]
pub fn run_glow(
mod glow_integration {
use super::*;
struct GlowWinitApp {
gl_window: glutin::WindowedContext<glutin::PossiblyCurrent>,
gl: Arc<glow::Context>,
painter: egui_glow::Painter,
integration: epi_integration::EpiIntegration,
app: Box<dyn epi::App>,
is_focused: bool,
}
impl GlowWinitApp {
fn new(
event_loop: &EventLoop<RequestRepaintEvent>,
app_name: &str,
native_options: &epi::NativeOptions,
app_creator: epi::AppCreator,
) -> ! {
) -> Self {
let storage = epi_integration::create_storage(app_name);
let window_settings = epi_integration::load_window_settings(storage.as_deref());
let event_loop = winit::event_loop::EventLoop::with_user_event();
let window_builder =
epi_integration::window_builder(native_options, &window_settings).with_title(app_name);
let (gl_window, gl) = create_display(native_options, window_builder, &event_loop);
let gl = std::sync::Arc::new(gl);
let window_builder = epi_integration::window_builder(native_options, &window_settings)
.with_title(app_name);
let (gl_window, gl) = create_display(native_options, window_builder, event_loop);
let gl = Arc::new(gl);
let mut painter = egui_glow::Painter::new(gl.clone(), None, "")
let painter = egui_glow::Painter::new(gl.clone(), None, "")
.unwrap_or_else(|error| panic!("some OpenGL error occurred {}\n", error));
let system_theme = native_options.system_theme();
let mut integration = epi_integration::EpiIntegration::new(
&event_loop,
event_loop,
painter.max_texture_side(),
gl_window.window(),
system_theme,
@ -100,20 +261,56 @@ pub fn run_glow(
integration.warm_up(app.as_mut(), gl_window.window());
}
let mut is_focused = true;
Self {
gl_window,
gl,
painter,
integration,
app,
is_focused: true,
}
}
}
event_loop.run(move |event, _, control_flow| {
let window = gl_window.window();
impl WinitApp for GlowWinitApp {
fn is_focused(&self) -> bool {
self.is_focused
}
let mut redraw = || {
fn integration(&self) -> &EpiIntegration {
&self.integration
}
fn window(&self) -> &winit::window::Window {
self.gl_window.window()
}
fn save_and_destroy(&mut self) {
self.integration
.save(&mut *self.app, self.gl_window.window());
self.app.on_exit(Some(&self.gl));
self.painter.destroy();
}
fn paint(&mut self) -> EventResult {
#[cfg(feature = "puffin")]
puffin::GlobalProfiler::lock().new_frame();
crate::profile_scope!("frame");
let Self {
gl_window,
gl,
app,
integration,
painter,
..
} = self;
let window = gl_window.window();
let screen_size_in_pixels: [u32; 2] = window.inner_size().into();
egui_glow::painter::clear(
&gl,
gl,
screen_size_in_pixels,
app.clear_color(&integration.egui_ctx.style().visuals),
);
@ -146,11 +343,10 @@ pub fn run_glow(
gl_window.swap_buffers().unwrap();
}
*control_flow = if integration.should_quit() {
winit::event_loop::ControlFlow::Exit
let control_flow = if integration.should_quit() {
EventResult::Exit
} else if repaint_after.is_zero() {
window.request_redraw();
winit::event_loop::ControlFlow::Poll
EventResult::RepaintAsap
} else if let Some(repaint_after_instant) =
std::time::Instant::now().checked_add(repaint_after)
{
@ -159,14 +355,14 @@ pub fn run_glow(
// technically, this might lead to some weird corner cases where the user *WANTS*
// winit to use `WaitUntil(MAX_INSTANT)` explicitly. they can roll their own
// egui backend impl i guess.
winit::event_loop::ControlFlow::WaitUntil(repaint_after_instant)
EventResult::RepaintAt(repaint_after_instant)
} else {
winit::event_loop::ControlFlow::Wait
EventResult::Wait
};
integration.maybe_autosave(app.as_mut(), window);
if !is_focused {
if !self.is_focused {
// On Mac, a minimized Window uses up all CPU: https://github.com/emilk/egui/issues/325
// We can't know if we are minimized: https://github.com/rust-windowing/winit/issues/208
// But we know if we are focused (in foreground). When minimized, we are not focused.
@ -175,78 +371,103 @@ pub fn run_glow(
crate::profile_scope!("bg_sleep");
std::thread::sleep(std::time::Duration::from_millis(10));
}
};
match event {
// Platform-dependent event handlers to workaround a winit bug
// See: https://github.com/rust-windowing/winit/issues/987
// See: https://github.com/rust-windowing/winit/issues/1619
winit::event::Event::RedrawEventsCleared if cfg!(windows) => redraw(),
winit::event::Event::RedrawRequested(_) if !cfg!(windows) => redraw(),
control_flow
}
fn on_event(&mut self, event: winit::event::Event<'_, RequestRepaintEvent>) -> EventResult {
match event {
winit::event::Event::WindowEvent { event, .. } => {
match &event {
winit::event::WindowEvent::Focused(new_focused) => {
is_focused = *new_focused;
self.is_focused = *new_focused;
}
winit::event::WindowEvent::Resized(physical_size) => {
// Resize with 0 width and height is used by winit to signal a minimize event on Windows.
// See: https://github.com/rust-windowing/winit/issues/208
// This solves an issue where the app would panic when minimizing on Windows.
if physical_size.width > 0 && physical_size.height > 0 {
gl_window.resize(*physical_size);
self.gl_window.resize(*physical_size);
}
}
winit::event::WindowEvent::ScaleFactorChanged { new_inner_size, .. } => {
gl_window.resize(**new_inner_size);
winit::event::WindowEvent::ScaleFactorChanged {
new_inner_size, ..
} => {
self.gl_window.resize(**new_inner_size);
}
winit::event::WindowEvent::CloseRequested if integration.should_quit() => {
*control_flow = winit::event_loop::ControlFlow::Exit;
winit::event::WindowEvent::CloseRequested
if self.integration.should_quit() =>
{
return EventResult::Exit
}
_ => {}
}
integration.on_event(app.as_mut(), &event);
if integration.should_quit() {
*control_flow = winit::event_loop::ControlFlow::Exit;
}
window.request_redraw(); // TODO(emilk): ask egui if the events warrants a repaint instead
}
winit::event::Event::LoopDestroyed => {
integration.save(&mut *app, window);
app.on_exit(Some(&gl));
painter.destroy();
}
winit::event::Event::UserEvent(RequestRepaintEvent) => window.request_redraw(),
winit::event::Event::NewEvents(winit::event::StartCause::ResumeTimeReached {
..
}) => {
window.request_redraw();
}
_ => {}
}
});
}
self.integration.on_event(self.app.as_mut(), &event);
// TODO(emilk): merge with with the clone above
/// Run an egui app
#[cfg(feature = "wgpu")]
pub fn run_wgpu(
if self.integration.should_quit() {
EventResult::Exit
} else {
// TODO(emilk): ask egui if the event warrants a repaint
EventResult::RepaintAsap
}
}
_ => EventResult::Wait,
}
}
}
pub fn run_glow(
app_name: &str,
native_options: &epi::NativeOptions,
app_creator: epi::AppCreator,
) -> ! {
) {
let event_loop = EventLoop::with_user_event();
let glow_eframe = GlowWinitApp::new(&event_loop, app_name, native_options, app_creator);
if native_options.run_and_return {
run_and_return(event_loop, glow_eframe);
} else {
run_and_exit(event_loop, glow_eframe);
}
}
}
#[cfg(feature = "glow")]
pub use glow_integration::run_glow;
// ----------------------------------------------------------------------------
#[cfg(feature = "wgpu")]
mod wgpu_integration {
use super::*;
struct WgpuWinitApp {
window: winit::window::Window,
painter: egui_wgpu::winit::Painter<'static>,
integration: epi_integration::EpiIntegration,
app: Box<dyn epi::App>,
is_focused: bool,
}
impl WgpuWinitApp {
fn new(
event_loop: &EventLoop<RequestRepaintEvent>,
app_name: &str,
native_options: &epi::NativeOptions,
app_creator: epi::AppCreator,
) -> Self {
let storage = epi_integration::create_storage(app_name);
let window_settings = epi_integration::load_window_settings(storage.as_deref());
let event_loop = winit::event_loop::EventLoop::with_user_event();
let window = epi_integration::window_builder(native_options, &window_settings)
.with_title(app_name)
.build(&event_loop)
.build(event_loop)
.unwrap();
// SAFETY: `window` must outlive `painter`.
#[allow(unsafe_code)]
let mut painter = unsafe {
let painter = unsafe {
let mut painter = egui_wgpu::winit::Painter::new(
wgpu::Backends::PRIMARY | wgpu::Backends::GL,
wgpu::PowerPreference::HighPerformance,
@ -267,7 +488,7 @@ pub fn run_wgpu(
let system_theme = native_options.system_theme();
let mut integration = epi_integration::EpiIntegration::new(
&event_loop,
event_loop,
painter.max_texture_side().unwrap_or(2048),
&window,
system_theme,
@ -299,16 +520,54 @@ pub fn run_wgpu(
integration.warm_up(app.as_mut(), &window);
}
let mut is_focused = true;
Self {
window,
painter,
integration,
app,
is_focused: true,
}
}
}
event_loop.run(move |event, _, control_flow| {
let window = &window;
impl WinitApp for WgpuWinitApp {
fn is_focused(&self) -> bool {
self.is_focused
}
let mut redraw = || {
fn integration(&self) -> &EpiIntegration {
&self.integration
}
fn window(&self) -> &winit::window::Window {
&self.window
}
fn save_and_destroy(&mut self) {
self.integration.save(&mut *self.app, &self.window);
#[cfg(feature = "glow")]
self.app.on_exit(None);
#[cfg(not(feature = "glow"))]
self.app.on_exit();
self.painter.destroy();
}
fn paint(&mut self) -> EventResult {
#[cfg(feature = "puffin")]
puffin::GlobalProfiler::lock().new_frame();
crate::profile_scope!("frame");
let Self {
window,
app,
integration,
painter,
..
} = self;
let egui::FullOutput {
platform_output,
repaint_after,
@ -330,11 +589,10 @@ pub fn run_wgpu(
&textures_delta,
);
*control_flow = if integration.should_quit() {
winit::event_loop::ControlFlow::Exit
let control_flow = if integration.should_quit() {
EventResult::Exit
} else if repaint_after.is_zero() {
window.request_redraw();
winit::event_loop::ControlFlow::Poll
EventResult::RepaintAsap
} else if let Some(repaint_after_instant) =
std::time::Instant::now().checked_add(repaint_after)
{
@ -343,14 +601,14 @@ pub fn run_wgpu(
// technically, this might lead to some weird corner cases where the user *WANTS*
// winit to use `WaitUntil(MAX_INSTANT)` explicitly. they can roll their own
// egui backend impl i guess.
winit::event_loop::ControlFlow::WaitUntil(repaint_after_instant)
EventResult::RepaintAt(repaint_after_instant)
} else {
winit::event_loop::ControlFlow::Wait
EventResult::Wait
};
integration.maybe_autosave(app.as_mut(), window);
if !is_focused {
if !self.is_focused {
// On Mac, a minimized Window uses up all CPU: https://github.com/emilk/egui/issues/325
// We can't know if we are minimized: https://github.com/rust-windowing/winit/issues/208
// But we know if we are focused (in foreground). When minimized, we are not focused.
@ -359,15 +617,12 @@ pub fn run_wgpu(
crate::profile_scope!("bg_sleep");
std::thread::sleep(std::time::Duration::from_millis(10));
}
};
control_flow
}
fn on_event(&mut self, event: winit::event::Event<'_, RequestRepaintEvent>) -> EventResult {
match event {
// Platform-dependent event handlers to workaround a winit bug
// See: https://github.com/rust-windowing/winit/issues/987
// See: https://github.com/rust-windowing/winit/issues/1619
winit::event::Event::RedrawEventsCleared if cfg!(windows) => redraw(),
winit::event::Event::RedrawRequested(_) if !cfg!(windows) => redraw(),
#[cfg(target_os = "android")]
winit::event::Event::Resumed => unsafe {
painter.set_window(Some(&window));
@ -380,49 +635,61 @@ pub fn run_wgpu(
winit::event::Event::WindowEvent { event, .. } => {
match &event {
winit::event::WindowEvent::Focused(new_focused) => {
is_focused = *new_focused;
self.is_focused = *new_focused;
}
winit::event::WindowEvent::Resized(physical_size) => {
// Resize with 0 width and height is used by winit to signal a minimize event on Windows.
// See: https://github.com/rust-windowing/winit/issues/208
// This solves an issue where the app would panic when minimizing on Windows.
if physical_size.width > 0 && physical_size.height > 0 {
painter.on_window_resized(physical_size.width, physical_size.height);
self.painter
.on_window_resized(physical_size.width, physical_size.height);
}
}
winit::event::WindowEvent::ScaleFactorChanged { new_inner_size, .. } => {
painter.on_window_resized(new_inner_size.width, new_inner_size.height);
winit::event::WindowEvent::ScaleFactorChanged {
new_inner_size, ..
} => {
self.painter
.on_window_resized(new_inner_size.width, new_inner_size.height);
}
winit::event::WindowEvent::CloseRequested if integration.should_quit() => {
*control_flow = winit::event_loop::ControlFlow::Exit;
winit::event::WindowEvent::CloseRequested
if self.integration.should_quit() =>
{
return EventResult::Exit
}
_ => {}
};
integration.on_event(app.as_mut(), &event);
if integration.should_quit() {
*control_flow = winit::event_loop::ControlFlow::Exit;
self.integration.on_event(self.app.as_mut(), &event);
if self.integration.should_quit() {
EventResult::Exit
} else {
// TODO(emilk): ask egui if the event warrants a repaint
EventResult::RepaintAsap
}
}
_ => EventResult::Wait,
}
}
window.request_redraw(); // TODO(emilk): ask egui if the events warrants a repaint instead
}
winit::event::Event::LoopDestroyed => {
integration.save(&mut *app, window);
#[cfg(feature = "glow")]
app.on_exit(None);
pub fn run_wgpu(
app_name: &str,
native_options: &epi::NativeOptions,
app_creator: epi::AppCreator,
) {
let event_loop = EventLoop::with_user_event();
let wgpu_eframe = WgpuWinitApp::new(&event_loop, app_name, native_options, app_creator);
#[cfg(not(feature = "glow"))]
app.on_exit();
painter.destroy();
if native_options.run_and_return {
run_and_return(event_loop, wgpu_eframe);
} else {
run_and_exit(event_loop, wgpu_eframe);
}
winit::event::Event::UserEvent(RequestRepaintEvent) => window.request_redraw(),
winit::event::Event::NewEvents(winit::event::StartCause::ResumeTimeReached {
..
}) => {
window.request_redraw();
}
_ => (),
}
});
}
// ----------------------------------------------------------------------------
#[cfg(feature = "wgpu")]
pub use wgpu_integration::run_wgpu;