Skip to content

Commit 6f73886

Browse files
committed
Allow aborting an exit event
1 parent d9cfeff commit 6f73886

File tree

6 files changed

+165
-26
lines changed

6 files changed

+165
-26
lines changed

eframe/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ NOTE: [`egui_web`](egui_web/CHANGELOG.md), [`egui-winit`](egui-winit/CHANGELOG.m
77
## Unreleased
88
* The default native backend is now `egui_glow` (instead of `egui_glium`) ([#1020](https://github.com/emilk/egui/pull/1020)).
99
* The default web painter is now `egui_glow` (instead of WebGL) ([#1020](https://github.com/emilk/egui/pull/1020)).
10+
* Added `App::on_pre_exit` ([#1038](https://github.com/emilk/egui/pull/1038))
1011

1112

1213
## 0.16.0 - 2021-12-29

eframe/examples/confirm_exit.rs

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
2+
3+
use eframe::{egui, epi};
4+
5+
#[derive(Default)]
6+
struct MyApp {
7+
can_exit: bool,
8+
is_exiting: bool,
9+
}
10+
11+
impl epi::App for MyApp {
12+
fn name(&self) -> &str {
13+
"Confirm exit"
14+
}
15+
16+
fn on_pre_exit(&mut self) -> bool {
17+
self.is_exiting = true;
18+
self.can_exit
19+
}
20+
21+
fn update(&mut self, ctx: &egui::Context, frame: &epi::Frame) {
22+
egui::CentralPanel::default().show(ctx, |ui| {
23+
ui.heading("Try to close the window");
24+
});
25+
26+
if self.is_exiting {
27+
egui::Window::new("Quit")
28+
.collapsible(false)
29+
.resizable(false)
30+
.show(ctx, |ui| {
31+
ui.heading("Do you want to quit?");
32+
ui.horizontal(|ui| {
33+
if ui.button("Yes!").clicked() {
34+
self.can_exit = true;
35+
frame.quit();
36+
}
37+
38+
if ui.button("Not yet").clicked() {
39+
self.is_exiting = false;
40+
}
41+
});
42+
});
43+
}
44+
}
45+
}
46+
47+
fn main() {
48+
let options = eframe::NativeOptions::default();
49+
eframe::run_native(Box::new(MyApp::default()), options);
50+
}

egui-winit/src/epi.rs

+50-6
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,28 @@ impl Persistence {
186186
}
187187

188188
// ----------------------------------------------------------------------------
189+
#[derive(PartialEq, Clone, Copy)]
190+
pub enum EpiQuit {
191+
None,
192+
Recoverable,
193+
Unrecoverable,
194+
}
195+
196+
impl EpiQuit {
197+
pub fn set_recoverable(&mut self) {
198+
// Prevent setting an unrecoverable quit as recoverable
199+
if *self != Self::Unrecoverable {
200+
*self = EpiQuit::Recoverable;
201+
}
202+
}
203+
204+
/// Tries to abort the quit if possible
205+
pub fn set_aborted(&mut self) {
206+
if *self != Self::Unrecoverable {
207+
*self = EpiQuit::None;
208+
}
209+
}
210+
}
189211

190212
/// Everything needed to make a winit-based integration for [`epi`].
191213
pub struct EpiIntegration {
@@ -195,7 +217,7 @@ pub struct EpiIntegration {
195217
egui_winit: crate::State,
196218
pub app: Box<dyn epi::App>,
197219
/// When set, it is time to quit
198-
quit: bool,
220+
quit: EpiQuit,
199221
}
200222

201223
impl EpiIntegration {
@@ -228,7 +250,7 @@ impl EpiIntegration {
228250
egui_ctx,
229251
egui_winit: crate::State::new(window),
230252
app,
231-
quit: false,
253+
quit: EpiQuit::None,
232254
};
233255

234256
slf.setup(window);
@@ -243,7 +265,11 @@ impl EpiIntegration {
243265
self.app
244266
.setup(&self.egui_ctx, &self.frame, self.persistence.storage());
245267
let app_output = self.frame.take_app_output();
246-
self.quit |= app_output.quit;
268+
269+
if app_output.quit {
270+
self.quit.set_recoverable();
271+
}
272+
247273
let tex_alloc_data =
248274
crate::epi::handle_app_output(window, self.egui_ctx.pixels_per_point(), app_output);
249275
self.frame.lock().output.tex_allocation_data = tex_alloc_data; // Do it later
@@ -259,13 +285,18 @@ impl EpiIntegration {
259285
}
260286

261287
/// If `true`, it is time to shut down.
262-
pub fn should_quit(&self) -> bool {
288+
pub fn should_quit(&self) -> EpiQuit {
263289
self.quit
264290
}
265291

266292
pub fn on_event(&mut self, event: &winit::event::WindowEvent<'_>) {
267293
use winit::event::WindowEvent;
268-
self.quit |= matches!(event, WindowEvent::CloseRequested | WindowEvent::Destroyed);
294+
if *event == WindowEvent::CloseRequested {
295+
self.quit.set_recoverable();
296+
} else if *event == WindowEvent::Destroyed {
297+
self.quit = EpiQuit::Unrecoverable;
298+
}
299+
269300
self.egui_winit.on_event(&self.egui_ctx, event);
270301
}
271302

@@ -290,7 +321,11 @@ impl EpiIntegration {
290321
.handle_output(window, &self.egui_ctx, egui_output);
291322

292323
let app_output = self.frame.take_app_output();
293-
self.quit |= app_output.quit;
324+
325+
if app_output.quit {
326+
self.quit.set_recoverable();
327+
}
328+
294329
let tex_allocation_data =
295330
crate::epi::handle_app_output(window, self.egui_ctx.pixels_per_point(), app_output);
296331

@@ -305,6 +340,15 @@ impl EpiIntegration {
305340
.maybe_autosave(&mut *self.app, &self.egui_ctx, window);
306341
}
307342

343+
pub fn on_pre_exit(&mut self) -> bool {
344+
let should_exit = self.app.on_pre_exit();
345+
if !should_exit {
346+
self.quit.set_aborted();
347+
}
348+
349+
should_exit
350+
}
351+
308352
pub fn on_exit(&mut self, window: &winit::window::Window) {
309353
self.app.on_exit();
310354
self.persistence

egui_glium/src/epi_backend.rs

+25-10
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ fn create_display(
2929

3030
// ----------------------------------------------------------------------------
3131

32+
use egui_winit::epi::EpiQuit;
3233
pub use epi::NativeOptions;
3334

3435
/// Run an egui app
@@ -66,7 +67,7 @@ pub fn run(app: Box<dyn epi::App>, native_options: &epi::NativeOptions) -> ! {
6667
std::thread::sleep(std::time::Duration::from_millis(10));
6768
}
6869

69-
let (needs_repaint, mut tex_allocation_data, shapes) =
70+
let (mut needs_repaint, mut tex_allocation_data, shapes) =
7071
integration.update(display.gl_window().window());
7172
let clipped_meshes = integration.egui_ctx.tessellate(shapes);
7273

@@ -97,13 +98,22 @@ pub fn run(app: Box<dyn epi::App>, native_options: &epi::NativeOptions) -> ! {
9798
}
9899

99100
{
100-
*control_flow = if integration.should_quit() {
101-
glutin::event_loop::ControlFlow::Exit
102-
} else if needs_repaint {
103-
display.gl_window().window().request_redraw();
104-
glutin::event_loop::ControlFlow::Poll
105-
} else {
106-
glutin::event_loop::ControlFlow::Wait
101+
if EpiQuit::Recoverable == integration.should_quit() && !integration.on_pre_exit() {
102+
needs_repaint = true;
103+
}
104+
105+
*control_flow = match integration.should_quit() {
106+
EpiQuit::Recoverable | EpiQuit::Unrecoverable => {
107+
glutin::event_loop::ControlFlow::Exit
108+
}
109+
EpiQuit::None => {
110+
if needs_repaint {
111+
display.gl_window().window().request_redraw();
112+
glutin::event_loop::ControlFlow::Poll
113+
} else {
114+
glutin::event_loop::ControlFlow::Wait
115+
}
116+
}
107117
};
108118
}
109119

@@ -123,8 +133,13 @@ pub fn run(app: Box<dyn epi::App>, native_options: &epi::NativeOptions) -> ! {
123133
}
124134

125135
integration.on_event(&event);
126-
if integration.should_quit() {
127-
*control_flow = glium::glutin::event_loop::ControlFlow::Exit;
136+
137+
if EpiQuit::Recoverable == integration.should_quit() {
138+
if integration.on_pre_exit() {
139+
*control_flow = glutin::event_loop::ControlFlow::Exit;
140+
}
141+
} else if EpiQuit::Unrecoverable == integration.should_quit() {
142+
*control_flow = glutin::event_loop::ControlFlow::Exit;
128143
}
129144

130145
display.gl_window().window().request_redraw(); // TODO: ask egui if the events warrants a repaint instead

egui_glow/src/epi_backend.rs

+24-9
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ fn create_display(
4242

4343
// ----------------------------------------------------------------------------
4444

45+
use egui_winit::epi::EpiQuit;
4546
pub use epi::NativeOptions;
4647

4748
/// Run an egui app
@@ -82,7 +83,7 @@ pub fn run(app: Box<dyn epi::App>, native_options: &epi::NativeOptions) -> ! {
8283
std::thread::sleep(std::time::Duration::from_millis(10));
8384
}
8485

85-
let (needs_repaint, mut tex_allocation_data, shapes) =
86+
let (mut needs_repaint, mut tex_allocation_data, shapes) =
8687
integration.update(gl_window.window());
8788
let clipped_meshes = integration.egui_ctx.tessellate(shapes);
8889

@@ -115,13 +116,22 @@ pub fn run(app: Box<dyn epi::App>, native_options: &epi::NativeOptions) -> ! {
115116
}
116117

117118
{
118-
*control_flow = if integration.should_quit() {
119-
glutin::event_loop::ControlFlow::Exit
120-
} else if needs_repaint {
121-
gl_window.window().request_redraw();
122-
glutin::event_loop::ControlFlow::Poll
123-
} else {
124-
glutin::event_loop::ControlFlow::Wait
119+
if EpiQuit::Recoverable == integration.should_quit() && !integration.on_pre_exit() {
120+
needs_repaint = true;
121+
}
122+
123+
*control_flow = match integration.should_quit() {
124+
EpiQuit::Recoverable | EpiQuit::Unrecoverable => {
125+
glutin::event_loop::ControlFlow::Exit
126+
}
127+
EpiQuit::None => {
128+
if needs_repaint {
129+
gl_window.window().request_redraw();
130+
glutin::event_loop::ControlFlow::Poll
131+
} else {
132+
glutin::event_loop::ControlFlow::Wait
133+
}
134+
}
125135
};
126136
}
127137

@@ -145,7 +155,12 @@ pub fn run(app: Box<dyn epi::App>, native_options: &epi::NativeOptions) -> ! {
145155
}
146156

147157
integration.on_event(&event);
148-
if integration.should_quit() {
158+
159+
if EpiQuit::Recoverable == integration.should_quit() {
160+
if integration.on_pre_exit() {
161+
*control_flow = glutin::event_loop::ControlFlow::Exit;
162+
}
163+
} else if EpiQuit::Unrecoverable == integration.should_quit() {
149164
*control_flow = glutin::event_loop::ControlFlow::Exit;
150165
}
151166

epi/src/lib.rs

+15-1
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,21 @@ pub trait App {
144144
/// where `APPNAME` is what is returned by [`Self::name()`].
145145
fn save(&mut self, _storage: &mut dyn Storage) {}
146146

147-
/// Called once on shutdown (before or after [`Self::save`])
147+
/// Called before an exit that can be aborted.
148+
/// By returning `false` the exit will be aborted. To continue the exit return `true`.
149+
///
150+
/// A scenario where this method will be
151+
/// run is after pressing the close button on a native window, which allows you to ask the user
152+
/// whether they want to do something before exiting. See the example
153+
/// `eframe/examples/confirm_exit.rs` for practical usage.
154+
///
155+
/// It will _not_ be called on the web or when the window is forcefully closed.
156+
fn on_pre_exit(&mut self) -> bool {
157+
true
158+
}
159+
160+
/// Called once on shutdown (before or after [`Self::save`]). If you need to abort an exit use
161+
/// [`Self::on_pre_exit`]
148162
fn on_exit(&mut self) {}
149163

150164
// ---------

0 commit comments

Comments
 (0)