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 logging through `bevy_log` if the `log` feature is enabled.
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    name: Option<Arc<dyn PanicHandleFn<String>>>,
23    body: Option<Arc<dyn PanicHandleFn<String>>>,
24    hook: Option<Arc<dyn PanicHandleFn<()>>>,
25}
26impl PanicHandlerBuilder {
27    #[must_use]
28    /// Builds the `PanicHandler`
29    pub fn build(self) -> PanicHandler {
30        PanicHandler {
31            title: {
32                self.name.unwrap_or_else(|| {
33                    Arc::new(|_: &std::panic::PanicHookInfo| "Fatal Error".to_owned())
34                })
35            },
36            body: {
37                self.body.unwrap_or_else(|| {
38                    Arc::new(|info| {
39                        format!(
40                            "Unhandled panic! at {}:\n{}",
41                            info.location()
42                                .map_or_else(|| "Unknown Location".to_owned(), ToString::to_string),
43                            info.payload().downcast_ref::<String>().map_or_else(
44                                || (*info.payload().downcast_ref::<&str>().unwrap_or(&"No Info"))
45                                    .to_string(),
46                                ToOwned::to_owned,
47                            )
48                        )
49                    })
50                })
51            },
52            hook: { self.hook.unwrap_or_else(|| Arc::new(|_| {})) },
53        }
54    }
55
56    #[must_use]
57    /// After the popup is closed, the previously existing panic hook will be called
58    pub fn take_call_from_existing(mut self) -> Self {
59        self.hook = Some(Arc::new(std::panic::take_hook()));
60        self
61    }
62
63    #[must_use]
64    /// After the popup is closed, this function will be called
65    pub fn set_call_func(mut self, call_func: impl PanicHandleFn<()>) -> Self {
66        self.hook = Some(Arc::new(call_func));
67        self
68    }
69
70    #[must_use]
71    /// The popup title will be set to the result of this function
72    pub fn set_title_func(mut self, title_func: impl PanicHandleFn<String>) -> Self {
73        self.name = Some(Arc::new(title_func));
74        self
75    }
76
77    #[must_use]
78    /// The popup body will be set to the result of this function
79    pub fn set_body_func(mut self, body_func: impl PanicHandleFn<String>) -> Self {
80        self.body = Some(Arc::new(body_func));
81        self
82    }
83}
84
85/// Bevy plugin that opens a popup window on panic & logs an error
86#[derive(Clone)]
87pub struct PanicHandler {
88    pub title: Arc<dyn PanicHandleFn<String>>,
89    pub body: Arc<dyn PanicHandleFn<String>>,
90    pub hook: Arc<dyn PanicHandleFn<()>>,
91}
92impl PanicHandler {
93    #[must_use]
94    #[allow(clippy::new_ret_no_self)]
95    /// Create a new builder. The custom hook does nothing.
96    pub fn new() -> PanicHandlerBuilder {
97        PanicHandlerBuilder::default()
98    }
99
100    #[must_use]
101    /// Create a new builder. The custom hook is taken from `std::panic::take_hook()`
102    pub fn new_take_old() -> PanicHandlerBuilder {
103        PanicHandlerBuilder::default().take_call_from_existing()
104    }
105}
106
107impl Plugin for PanicHandler {
108    fn build(&self, _: &mut App) {
109        let handler = self.clone();
110        std::panic::set_hook(Box::new(move |info| {
111            #[cfg(not(test))]
112            let title_string = (handler.title)(info);
113            #[cfg(not(test))]
114            let info_string = (handler.body)(info);
115
116            // This will print duplicate messages to stdout if the default panic hook is being used & env_logger is initialized.
117            #[cfg(all(not(test), feature = "log"))]
118            bevy::log::error!("{title_string}\n{info_string}");
119
120            // Don't interrupt test execution with a popup, and dont try on unsupported platforms.
121            #[cfg(all(
122                not(test),
123                any(target_os = "windows", target_os = "macos", target_family = "unix")
124            ))]
125            {
126                let builder = native_dialog::MessageDialogBuilder::default()
127                    .set_title(&title_string)
128                    .set_text(&info_string)
129                    .set_level(native_dialog::MessageLevel::Error);
130                if let Err(e) = builder.alert().show() {
131                    #[cfg(feature = "log")]
132                    bevy::log::error!("{e}");
133                    #[cfg(not(feature = "log"))]
134                    {
135                        _ = e;
136                    }
137                }
138            }
139
140            (handler.hook)(info);
141        }));
142    }
143}