bevy_panic_handler/
lib.rs

1//! A wrapper for panics using Bevy's plugin system.
2//!
3//! On supported platforms (windows, macos, linux) will produce a popup using the `msgbox` crate in addition to writing via `log::error!`, or if `bevy::log::LogPlugin` is not enabled, `stderr`.
4
5#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
6
7use std::sync::Arc;
8
9use bevy::prelude::*;
10
11pub trait PanicHandleFn<Res>:
12    Fn(&std::panic::PanicHookInfo) -> Res + Send + Sync + 'static
13{
14}
15impl<Res, T: Fn(&std::panic::PanicHookInfo) -> Res + Send + Sync + 'static> PanicHandleFn<Res>
16    for T
17{
18}
19
20#[derive(Default)]
21pub struct PanicHandlerBuilder {
22    custom_name: Option<Arc<dyn PanicHandleFn<String>>>,
23    custom_body: Option<Arc<dyn PanicHandleFn<String>>>,
24    custom_hook: Option<Arc<dyn PanicHandleFn<()>>>,
25}
26impl PanicHandlerBuilder {
27    #[must_use]
28    /// Builds the `PanicHandler`
29    pub fn build(self) -> PanicHandler {
30        PanicHandler {
31            custom_title: {
32                self.custom_name.unwrap_or_else(|| {
33                    Arc::new(|_: &std::panic::PanicHookInfo| "Fatal Error".to_owned())
34                })
35            },
36            custom_body: {
37                self.custom_body.unwrap_or_else(|| {
38                    Arc::new(|info| {
39                        format!(
40                            "Unhandled panic! @ {}:\n{}",
41                            info.location()
42                                .map_or("Unknown Location".to_owned(), ToString::to_string),
43                            info.payload().downcast_ref::<String>().unwrap_or(
44                                &((*info.payload().downcast_ref::<&str>().unwrap_or(&"No Info"))
45                                    .to_string())
46                            )
47                        )
48                    })
49                })
50            },
51            custom_hook: { self.custom_hook.unwrap_or_else(|| Arc::new(|_| {})) },
52        }
53    }
54
55    #[must_use]
56    /// After the popup is closed, the previously existing panic hook will be called
57    pub fn take_call_from_existing(mut self) -> Self {
58        self.custom_hook = Some(Arc::new(std::panic::take_hook()));
59        self
60    }
61
62    #[must_use]
63    /// After the popup is closed, this function will be called
64    pub fn set_call_func(mut self, call_func: impl PanicHandleFn<()>) -> Self {
65        self.custom_hook = Some(Arc::new(call_func));
66        self
67    }
68
69    #[must_use]
70    /// The popup title will be set to the result of this function
71    pub fn set_title_func(mut self, title_func: impl PanicHandleFn<String>) -> Self {
72        self.custom_name = Some(Arc::new(title_func));
73        self
74    }
75
76    #[must_use]
77    /// The popup body will be set to the result of this function
78    pub fn set_body_func(mut self, body_func: impl PanicHandleFn<String>) -> Self {
79        self.custom_body = Some(Arc::new(body_func));
80        self
81    }
82}
83
84/// Bevy plugin that opens a popup window on panic & logs an error
85#[derive(Clone)]
86pub struct PanicHandler {
87    pub custom_title: Arc<dyn PanicHandleFn<String>>,
88    pub custom_body: Arc<dyn PanicHandleFn<String>>,
89    pub custom_hook: Arc<dyn PanicHandleFn<()>>,
90}
91impl PanicHandler {
92    #[must_use]
93    #[allow(clippy::new_ret_no_self)]
94    /// Create a new builder. The custom hook does nothing.
95    pub fn new() -> PanicHandlerBuilder {
96        PanicHandlerBuilder::default()
97    }
98
99    #[must_use]
100    /// Create a new builder. The custom hook is taken from `std::panic::take_hook()`
101    pub fn new_take_old() -> PanicHandlerBuilder {
102        PanicHandlerBuilder::default().take_call_from_existing()
103    }
104}
105
106impl Plugin for PanicHandler {
107    fn build(&self, _: &mut App) {
108        let handler = self.clone();
109        std::panic::set_hook(Box::new(move |info| {
110            let title_string = (handler.custom_title)(info);
111            let info_string = (handler.custom_body)(info);
112
113            // Known limitations: Logging in tests prints to stdout immediately.
114            // This will print duplicate messages to stdout if the default panic hook is being used & env_logger is initialized.
115            bevy::log::error!("{title_string}\n{info_string}");
116
117            // Don't interrupt test execution with a popup, and dont try on unsupported platforms.
118            #[cfg(all(
119                not(test),
120                any(target_os = "windows", target_os = "macos", target_os = "linux")
121            ))]
122            {
123                if let Err(e) = native_dialog::MessageDialog::new()
124                    .set_title(&title_string)
125                    .set_text(&info_string)
126                    .set_type(native_dialog::MessageType::Error)
127                    .show_alert()
128                {
129                    bevy::log::error!("{e}");
130                }
131            }
132
133            (handler.custom_hook)(info);
134        }));
135    }
136}