1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
//! A wrapper for panics using Bevy's plugin system.
//!
//! 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`.

use std::sync::Arc;

use bevy::prelude::*;

pub trait PanicHandleFn: Fn(&std::panic::PanicInfo) + Send + Sync + 'static {}
impl<T: Fn(&std::panic::PanicInfo) + Send + Sync + 'static> PanicHandleFn for T {}

/// Bevy plugin that opens a popup window on panic & logs an error
#[derive(Default)]
pub struct PanicHandler {
    custom_hook: Option<Arc<dyn PanicHandleFn>>,
}
impl PanicHandler {
    /// Create a new `PanicHandler` with a function to call after the popup is closed. If you only want the popup, use `PanicHandler::default()`
    #[must_use]
    pub fn new(panic_handler: impl PanicHandleFn) -> Self {
        Self {
            custom_hook: Some(Arc::new(panic_handler)),
        }
    }

    /// Create a new `PanicHandler`, calling the already existing panic hook after the popup is closed
    #[must_use]
    pub fn default_take_panic() -> Self {
        Self {
            custom_hook: Some(Arc::new(std::panic::take_hook())),
        }
    }
}

impl Plugin for PanicHandler {
    fn build(&self, _: &mut App) {
        let custom_hook = self
            .custom_hook
            .as_ref()
            .cloned()
            .unwrap_or_else(|| Arc::new(|_| {}));
        std::panic::set_hook(Box::new(move |info| {
            let info_string = format!(
                "Unhandled panic! @ {}:\n{}",
                info.location()
                    .map_or("Unknown Location".to_owned(), ToString::to_string),
                info.payload().downcast_ref::<String>().unwrap_or(
                    &((*info.payload().downcast_ref::<&str>().unwrap_or(&"No Info")).to_string())
                )
            );

            // Known limitations: Logging in tests prints to stdout immediately.
            // This will print duplicate messages to stdout if the default panic hook is being used & env_logger is initialized.
            bevy::log::error!("{info_string}");

            // Don't interrupt test execution with a popup, and dont try on unsupported platforms.
            #[cfg(all(not(test), any(target_os = "windows", target_os = "macos", target_os = "linux")))]
            { _ = msgbox::create("Fatal Error", &info_string, msgbox::IconType::Error); }

            custom_hook(info);
        }));
    }
}