Generalized app error handling

This commit is contained in:
Jonathan Johnson 2024-10-07 07:44:56 -07:00
parent 662cebf193
commit be6918d5eb
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
4 changed files with 115 additions and 13 deletions

View file

@ -18,6 +18,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
underlying window is resized immediately. Notably, this happens on Wayland but
may happen on some other platforms as well.
- `RunningWindow::winit` now returns an `Arc` wrapped winit window.
- `AppMessage::Error` is a new associated type that is used to communicate
errors from window threads to the event loop. To facilitate this
communication, `App::send_error` and `Application::send_error` have been added.
Additionally, `WindowBehavior::Initialize` now returns a `Result<Self,
AppMessage::Error>`.
To install a hander, use `PendingApp::on_error`.
### Added

View file

@ -11,6 +11,7 @@ mod window;
mod xdg;
use std::collections::HashMap;
use std::convert::Infallible;
use std::ops::Deref;
use std::process::exit;
use std::sync::atomic::{AtomicBool, Ordering};
@ -24,7 +25,7 @@ use winit::application::ApplicationHandler;
use winit::error::{EventLoopError, OsError};
use winit::event::StartCause;
use winit::event_loop::{
ActiveEventLoop, ControlFlow, EventLoop, EventLoopProxy, OwnedDisplayHandle,
ActiveEventLoop, ControlFlow, EventLoop, EventLoopClosed, EventLoopProxy, OwnedDisplayHandle,
};
use winit::monitor::MonitorHandle;
use winit::window::WindowId;
@ -143,6 +144,7 @@ where
running: App<AppMessage>,
on_startup: Vec<Box<StartupClosure<AppMessage>>>,
pending_windows: Vec<PendingWindow<AppMessage>>,
on_error: Option<Box<dyn FnMut(AppMessage::Error)>>,
}
struct PendingWindow<AppMessage>
@ -197,9 +199,19 @@ where
message_callback: Box::new(event_callback),
on_startup: Vec::new(),
pending_windows: Vec::new(),
on_error: None,
}
}
/// Sets a handler that is invoked when an app receives an
/// [`AppTypes::Error`].
pub fn on_error<F>(&mut self, on_error: F)
where
F: FnMut(AppMessage::Error) + 'static,
{
self.on_error = Some(Box::new(on_error));
}
/// Executes `on_startup` once the app event loop has started.
///
/// This is useful because some information provided by winit is only
@ -228,6 +240,7 @@ where
running,
on_startup,
pending_windows,
on_error,
} = self;
#[cfg(all(target_os = "linux", feature = "xdg"))]
@ -238,6 +251,7 @@ where
running,
on_startup,
pending_windows,
on_error,
})
}
}
@ -250,6 +264,7 @@ where
running: App<AppMessage>,
on_startup: Vec<Box<StartupClosure<AppMessage>>>,
pending_windows: Vec<PendingWindow<AppMessage>>,
on_error: Option<Box<dyn FnMut(AppMessage::Error)>>,
}
impl<AppMessage> ApplicationHandler<EventLoopMessage<AppMessage>> for RunningApp<AppMessage>
@ -340,6 +355,11 @@ where
exit(0)
}
}
EventLoopMessage::Error(err) => {
if let Some(handler) = &mut self.on_error {
handler(err);
}
}
#[cfg(all(target_os = "linux", feature = "xdg"))]
EventLoopMessage::ThemeChanged(theme) => {
self.running.windows.theme_changed(theme);
@ -385,6 +405,26 @@ where
response_receiver.recv().ok()
}
/// Sends an error to the event loop.
///
/// # Errors
///
/// Returns an error if the event loop is not currently running.
pub fn send_error(
&self,
error: AppMessage::Error,
) -> Result<(), EventLoopClosed<AppMessage::Error>> {
if !self.started.load(Ordering::Relaxed) {
return Err(EventLoopClosed(error));
}
match self.proxy.send_event(EventLoopMessage::Error(error)) {
Ok(()) => Ok(()),
Err(EventLoopClosed(EventLoopMessage::Error(err))) => Err(EventLoopClosed(err)),
_ => unreachable!("returned value should be the same"),
}
}
/// Creates a guard that prevents this app from shutting down.
///
/// If the app is not currently running, this function returns None.
@ -428,6 +468,16 @@ where
/// running. Otherwise, this function will block until the result of the
/// callback has been received.
fn send(&mut self, message: AppMessage) -> Option<AppMessage::Response>;
/// Sends an error to the event loop.
///
/// # Errors
///
/// Returns an error if the event loop is not currently running.
fn send_error(
&mut self,
error: AppMessage::Error,
) -> Result<(), EventLoopClosed<AppMessage::Error>>;
}
/// A type that contains a reference to an [`Application`] implementor.
@ -487,11 +537,15 @@ pub trait Message: Send + 'static {
type Window: Send;
/// The type returned when responding to this message.
type Response: Send;
/// The type that is communicated when an error occurs that the event
/// loop/app should handle.
type Error: Send;
}
impl Message for () {
type Response = ();
type Window = ();
type Error = Infallible;
}
impl<AppMessage> Application<AppMessage> for PendingApp<AppMessage>
@ -508,6 +562,16 @@ where
ExecutingApp::new(&self.running.windows, &self.event_loop),
))
}
fn send_error(
&mut self,
error: <AppMessage as Message>::Error,
) -> Result<(), EventLoopClosed<<AppMessage as Message>::Error>> {
if let Some(on_error) = &mut self.on_error {
on_error(error);
}
Ok(())
}
}
impl<AppMessage> private::ApplicationSealed<AppMessage> for PendingApp<AppMessage>
@ -541,6 +605,14 @@ where
let this: &Self = self;
this.send(message)
}
fn send_error(
&mut self,
error: <AppMessage as Message>::Error,
) -> Result<(), EventLoopClosed<<AppMessage as Message>::Error>> {
let this: &Self = self;
this.send_error(error)
}
}
impl<AppMessage> private::ApplicationSealed<AppMessage> for App<AppMessage>
@ -735,16 +807,16 @@ struct OpenWindow<User> {
}
/// A guard preventing an [`App`] from shutting down.
pub struct ShutdownGuard<Message>
pub struct ShutdownGuard<AppMessage>
where
Message: crate::Message,
AppMessage: Message,
{
app: App<Message>,
app: App<AppMessage>,
}
impl<Message> Drop for ShutdownGuard<Message>
impl<AppMessage> Drop for ShutdownGuard<AppMessage>
where
Message: crate::Message,
AppMessage: Message,
{
fn drop(&mut self) {
let _ = self.app.proxy.send_event(EventLoopMessage::AllowShutdown);

View file

@ -62,6 +62,7 @@ where
},
PreventShutdown,
AllowShutdown,
Error(AppMessage::Error),
#[cfg(all(target_os = "linux", feature = "xdg"))]
ThemeChanged(Theme),
}

View file

@ -481,7 +481,7 @@ where
// recovery for a panic inside of a window, the only question is whether
// the entire app panics or not.
let possible_panic = std::panic::catch_unwind(AssertUnwindSafe(move || {
let mut behavior = Behavior::initialize(&mut self, context);
let mut behavior = Behavior::initialize(&mut self, context)?;
// When it takes a while for a graphics stack to initialize, we can
// avoid showing a blank window due to our multi-threaded event
@ -513,13 +513,20 @@ where
// behavior is dropped. This upholds the requirement for RawWindowHandle
// by making sure that any resources required by the behavior have had a
// chance to be freed.
Ok(())
}));
if let Err(panic) = possible_panic {
let _result = proxy.send_event(EventLoopMessage::WindowPanic(window_id));
std::panic::resume_unwind(panic)
} else {
let _result = proxy.send_event(EventLoopMessage::CloseWindow(window_id));
match possible_panic {
Ok(Ok(())) => {
let _result = proxy.send_event(EventLoopMessage::CloseWindow(window_id));
}
Ok(Err(init_error)) => {
let _result = proxy.send_event(EventLoopMessage::Error(init_error));
}
Err(panic) => {
let _result = proxy.send_event(EventLoopMessage::WindowPanic(window_id));
std::panic::resume_unwind(panic)
}
}
}
@ -801,6 +808,12 @@ where
.ok()?;
self.responses.1.recv().ok()
}
fn send_error(
&mut self,
error: <AppMessage as Message>::Error,
) -> Result<(), winit::event_loop::EventLoopClosed<<AppMessage as Message>::Error>> {
self.app.send_error(error)
}
}
impl<AppMessage> private::ApplicationSealed<AppMessage> for RunningWindow<AppMessage>
@ -990,7 +1003,15 @@ where
/// Returns a new instance of this behavior after initializing itself with
/// the window and context.
fn initialize(window: &mut RunningWindow<AppMessage>, context: Self::Context) -> Self;
///
/// # Errors
///
/// If the window cannot be initialized, this function should return the
/// cause of the failure.
fn initialize(
window: &mut RunningWindow<AppMessage>,
context: Self::Context,
) -> Result<Self, AppMessage::Error>;
/// Displays the contents of the window.
fn redraw(&mut self, window: &mut RunningWindow<AppMessage>);