mirror of
https://github.com/emilk/egui.git
synced 2026-08-31 05:40:03 -04:00
eframe web: detect and report panics during startup (#2992)
* Detect panics during initialization and show them to the user * PanicHandler now also logs the panics * Add example of how to call into your app from JS * Refactor: break out AppRunner and AppRunnerRef to own files * Hide AppRunner * Simplify user code * AppRunnerRef -> WebRunner * Better docs * Don't paint until first animation frame * Update multiple_apps.html * Update web demo * Cleanup and fixes * left-align panic message in html
This commit is contained in:
219
crates/eframe/src/web/web_runner.rs
Normal file
219
crates/eframe/src/web/web_runner.rs
Normal file
@@ -0,0 +1,219 @@
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::{epi, App};
|
||||
|
||||
use super::{events, AppRunner, PanicHandler};
|
||||
|
||||
/// This is how `eframe` runs your wepp application
|
||||
///
|
||||
/// This is cheap to clone.
|
||||
///
|
||||
/// See [the crate level docs](crate) for an example.
|
||||
#[derive(Clone)]
|
||||
pub struct WebRunner {
|
||||
/// Have we ever panicked?
|
||||
panic_handler: PanicHandler,
|
||||
|
||||
/// If we ever panic during running, this RefCell is poisoned.
|
||||
/// So before we use it, we need to check [`Self::panic_handler`].
|
||||
runner: Rc<RefCell<Option<AppRunner>>>,
|
||||
|
||||
/// In case of a panic, unsubscribe these.
|
||||
/// They have to be in a separate `Rc` so that we don't need to pass them to
|
||||
/// the panic handler, since they aren't `Send`.
|
||||
events_to_unsubscribe: Rc<RefCell<Vec<EventToUnsubscribe>>>,
|
||||
}
|
||||
|
||||
impl WebRunner {
|
||||
// Will install a panic handler that will catch and log any panics
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new() -> Self {
|
||||
#[cfg(not(web_sys_unstable_apis))]
|
||||
log::warn!(
|
||||
"eframe compiled without RUSTFLAGS='--cfg=web_sys_unstable_apis'. Copying text won't work."
|
||||
);
|
||||
|
||||
let panic_handler = PanicHandler::install();
|
||||
|
||||
Self {
|
||||
panic_handler,
|
||||
runner: Rc::new(RefCell::new(None)),
|
||||
events_to_unsubscribe: Rc::new(RefCell::new(Default::default())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create the application, install callbacks, and start running the app.
|
||||
///
|
||||
/// # Errors
|
||||
/// Failing to initialize graphics.
|
||||
pub async fn start(
|
||||
&self,
|
||||
canvas_id: &str,
|
||||
web_options: crate::WebOptions,
|
||||
app_creator: epi::AppCreator,
|
||||
) -> Result<(), JsValue> {
|
||||
self.destroy();
|
||||
|
||||
let follow_system_theme = web_options.follow_system_theme;
|
||||
|
||||
let mut runner = AppRunner::new(canvas_id, web_options, app_creator).await?;
|
||||
runner.warm_up();
|
||||
self.runner.replace(Some(runner));
|
||||
|
||||
{
|
||||
events::install_canvas_events(self)?;
|
||||
events::install_document_events(self)?;
|
||||
events::install_window_events(self)?;
|
||||
super::text_agent::install_text_agent(self)?;
|
||||
|
||||
if follow_system_theme {
|
||||
events::install_color_scheme_change_event(self)?;
|
||||
}
|
||||
|
||||
events::request_animation_frame(self.clone())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Has there been a panic?
|
||||
pub fn has_panicked(&self) -> bool {
|
||||
self.panic_handler.has_panicked()
|
||||
}
|
||||
|
||||
/// What was the panic message and callstack?
|
||||
pub fn panic_summary(&self) -> Option<super::PanicSummary> {
|
||||
self.panic_handler.panic_summary()
|
||||
}
|
||||
|
||||
fn unsubscribe_from_all_events(&self) {
|
||||
let events_to_unsubscribe: Vec<_> =
|
||||
std::mem::take(&mut *self.events_to_unsubscribe.borrow_mut());
|
||||
|
||||
if !events_to_unsubscribe.is_empty() {
|
||||
log::debug!("Unsubscribing from {} events", events_to_unsubscribe.len());
|
||||
for x in events_to_unsubscribe {
|
||||
if let Err(err) = x.unsubscribe() {
|
||||
log::warn!("Failed to unsubscribe from event: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn destroy(&self) {
|
||||
self.unsubscribe_from_all_events();
|
||||
|
||||
if let Some(runner) = self.runner.replace(None) {
|
||||
runner.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `None` if there has been a panic, or if we have been destroyed.
|
||||
/// In that case, just return to JS.
|
||||
pub(crate) fn try_lock(&self) -> Option<std::cell::RefMut<'_, AppRunner>> {
|
||||
if self.panic_handler.has_panicked() {
|
||||
// Unsubscribe from all events so that we don't get any more callbacks
|
||||
// that will try to access the poisoned runner.
|
||||
self.unsubscribe_from_all_events();
|
||||
None
|
||||
} else {
|
||||
let lock = self.runner.try_borrow_mut().ok()?;
|
||||
std::cell::RefMut::filter_map(lock, |lock| -> Option<&mut AppRunner> { lock.as_mut() })
|
||||
.ok()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get mutable access to the concrete [`App`] we enclose.
|
||||
///
|
||||
/// This will panic if your app does not implement [`App::as_any_mut`],
|
||||
/// and return `None` if this runner has panicked.
|
||||
pub fn app_mut<ConcreteApp: 'static + App>(
|
||||
&self,
|
||||
) -> Option<std::cell::RefMut<'_, ConcreteApp>> {
|
||||
self.try_lock()
|
||||
.map(|lock| std::cell::RefMut::map(lock, |runner| runner.app_mut::<ConcreteApp>()))
|
||||
}
|
||||
|
||||
/// Convenience function to reduce boilerplate and ensure that all event handlers
|
||||
/// are dealt with in the same way.
|
||||
///
|
||||
/// All events added with this method will automatically be unsubscribed on panic,
|
||||
/// or when [`Self::destroy`] is called.
|
||||
pub fn add_event_listener<E: wasm_bindgen::JsCast>(
|
||||
&self,
|
||||
target: &web_sys::EventTarget,
|
||||
event_name: &'static str,
|
||||
mut closure: impl FnMut(E, &mut AppRunner) + 'static,
|
||||
) -> Result<(), wasm_bindgen::JsValue> {
|
||||
let runner_ref = self.clone();
|
||||
|
||||
// Create a JS closure based on the FnMut provided
|
||||
let closure = Closure::wrap(Box::new(move |event: web_sys::Event| {
|
||||
// Only call the wrapped closure if the egui code has not panicked
|
||||
if let Some(mut runner_lock) = runner_ref.try_lock() {
|
||||
// Cast the event to the expected event type
|
||||
let event = event.unchecked_into::<E>();
|
||||
closure(event, &mut runner_lock);
|
||||
}
|
||||
}) as Box<dyn FnMut(web_sys::Event)>);
|
||||
|
||||
// Add the event listener to the target
|
||||
target.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?;
|
||||
|
||||
let handle = TargetEvent {
|
||||
target: target.clone(),
|
||||
event_name: event_name.to_owned(),
|
||||
closure,
|
||||
};
|
||||
|
||||
// Remember it so we unsubscribe on panic.
|
||||
// Otherwise we get calls into `self.runner` after it has been poisoned by a panic.
|
||||
self.events_to_unsubscribe
|
||||
.borrow_mut()
|
||||
.push(EventToUnsubscribe::TargetEvent(handle));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
struct TargetEvent {
|
||||
target: web_sys::EventTarget,
|
||||
event_name: String,
|
||||
closure: Closure<dyn FnMut(web_sys::Event)>,
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
struct IntervalHandle {
|
||||
handle: i32,
|
||||
closure: Closure<dyn FnMut()>,
|
||||
}
|
||||
|
||||
enum EventToUnsubscribe {
|
||||
TargetEvent(TargetEvent),
|
||||
|
||||
#[allow(unused)]
|
||||
IntervalHandle(IntervalHandle),
|
||||
}
|
||||
|
||||
impl EventToUnsubscribe {
|
||||
pub fn unsubscribe(self) -> Result<(), JsValue> {
|
||||
match self {
|
||||
EventToUnsubscribe::TargetEvent(handle) => {
|
||||
handle.target.remove_event_listener_with_callback(
|
||||
handle.event_name.as_str(),
|
||||
handle.closure.as_ref().unchecked_ref(),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
EventToUnsubscribe::IntervalHandle(handle) => {
|
||||
let window = web_sys::window().unwrap();
|
||||
window.clear_interval_with_handle(handle.handle);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user