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>>, /// 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>>, } 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 runner = AppRunner::new(canvas_id, web_options, app_creator).await?; 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 { 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: {}", super::string_from_js_value(&err) ); } } } } /// Shut down eframe and clean up resources. 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> { 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( &self, ) -> Option> { self.try_lock() .map(|lock| std::cell::RefMut::map(lock, |runner| runner.app_mut::())) } /// 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( &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::(); closure(event, &mut runner_lock); } }) as Box); // 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, } #[allow(unused)] struct IntervalHandle { handle: i32, closure: Closure, } enum EventToUnsubscribe { TargetEvent(TargetEvent), #[allow(unused)] IntervalHandle(IntervalHandle), } impl EventToUnsubscribe { pub fn unsubscribe(self) -> Result<(), JsValue> { match self { Self::TargetEvent(handle) => { handle.target.remove_event_listener_with_callback( handle.event_name.as_str(), handle.closure.as_ref().unchecked_ref(), )?; Ok(()) } Self::IntervalHandle(handle) => { let window = web_sys::window().unwrap(); window.clear_interval_with_handle(handle.handle); Ok(()) } } } }