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 underlying window is resized immediately. Notably, this happens on Wayland but
may happen on some other platforms as well. may happen on some other platforms as well.
- `RunningWindow::winit` now returns an `Arc` wrapped winit window. - `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 ### Added

View file

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

View file

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

View file

@ -481,7 +481,7 @@ where
// recovery for a panic inside of a window, the only question is whether // recovery for a panic inside of a window, the only question is whether
// the entire app panics or not. // the entire app panics or not.
let possible_panic = std::panic::catch_unwind(AssertUnwindSafe(move || { 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 // When it takes a while for a graphics stack to initialize, we can
// avoid showing a blank window due to our multi-threaded event // 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 // behavior is dropped. This upholds the requirement for RawWindowHandle
// by making sure that any resources required by the behavior have had a // by making sure that any resources required by the behavior have had a
// chance to be freed. // chance to be freed.
Ok(())
})); }));
if let Err(panic) = possible_panic { match possible_panic {
let _result = proxy.send_event(EventLoopMessage::WindowPanic(window_id)); Ok(Ok(())) => {
std::panic::resume_unwind(panic) let _result = proxy.send_event(EventLoopMessage::CloseWindow(window_id));
} else { }
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()?; .ok()?;
self.responses.1.recv().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> impl<AppMessage> private::ApplicationSealed<AppMessage> for RunningWindow<AppMessage>
@ -990,7 +1003,15 @@ where
/// Returns a new instance of this behavior after initializing itself with /// Returns a new instance of this behavior after initializing itself with
/// the window and context. /// 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. /// Displays the contents of the window.
fn redraw(&mut self, window: &mut RunningWindow<AppMessage>); fn redraw(&mut self, window: &mut RunningWindow<AppMessage>);