From ac50fa0d9425f15c286e7e1ba3258241f655e638 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 21 Apr 2023 08:33:01 +0200 Subject: [PATCH] eframe web: Better panic handling (#2942) * Refactor: remove extra store of events * Remove unnecessary extra function * Refactor: simplify event registering * Store panic summary * egui_demo_app: move web-part to own module * index.html: await * Properly unsubscribe from events on panic * Better error handling * Demo app html: hide the wasm canvas and show an error message on panic * egui_demo_app: add panic button to test panic response on web * fix typo * Use a constructor to create WebHandle * Refactor: less use of locks in the interfaces * More consistent naming --- crates/eframe/src/lib.rs | 50 +- crates/eframe/src/web/backend.rs | 344 ++++++++---- crates/eframe/src/web/events.rs | 615 +++++++++------------- crates/eframe/src/web/mod.rs | 5 +- crates/eframe/src/web/text_agent.rs | 101 ++-- crates/egui_demo_app/src/backend_panel.rs | 12 +- crates/egui_demo_app/src/lib.rs | 62 +-- crates/egui_demo_app/src/web.rs | 57 ++ docs/index.html | 56 +- docs/multiple_apps.html | 51 +- 10 files changed, 703 insertions(+), 650 deletions(-) create mode 100644 crates/egui_demo_app/src/web.rs diff --git a/crates/eframe/src/lib.rs b/crates/eframe/src/lib.rs index ad74ff34..90b73cc3 100644 --- a/crates/eframe/src/lib.rs +++ b/crates/eframe/src/lib.rs @@ -81,61 +81,17 @@ pub use epi::*; // ---------------------------------------------------------------------------- // When compiling for web -#[cfg(target_arch = "wasm32")] -pub mod web; - #[cfg(target_arch = "wasm32")] pub use wasm_bindgen; -#[cfg(target_arch = "wasm32")] -use web::AppRunnerRef; - #[cfg(target_arch = "wasm32")] pub use web_sys; -/// Install event listeners to register different input events -/// and start running the given app. -/// -/// ``` no_run -/// #[cfg(target_arch = "wasm32")] -/// use wasm_bindgen::prelude::*; -/// -/// /// This is the entry-point for all the web-assembly. -/// /// This is called from the HTML. -/// /// It loads the app, installs some callbacks, then returns. -/// /// It returns a handle to the running app that can be stopped calling `AppRunner::stop_web`. -/// /// You can add more callbacks like this if you want to call in to your code. -/// #[cfg(target_arch = "wasm32")] -/// #[wasm_bindgen] -/// pub struct WebHandle { -/// handle: AppRunnerRef, -/// } -/// #[cfg(target_arch = "wasm32")] -/// #[wasm_bindgen] -/// pub async fn start(canvas_id: &str) -> Result { -/// let web_options = eframe::WebOptions::default(); -/// eframe::start_web( -/// canvas_id, -/// web_options, -/// Box::new(|cc| Box::new(MyEguiApp::new(cc))), -/// ) -/// .await -/// .map(|handle| WebHandle { handle }) -/// } -/// ``` -/// -/// # Errors -/// Failing to initialize WebGL graphics. #[cfg(target_arch = "wasm32")] -pub async fn start_web( - canvas_id: &str, - web_options: WebOptions, - app_creator: AppCreator, -) -> std::result::Result { - let handle = web::start(canvas_id, web_options, app_creator).await?; +pub mod web; - Ok(handle) -} +#[cfg(target_arch = "wasm32")] +pub use web::start_web; // ---------------------------------------------------------------------------- // When compiling natively diff --git a/crates/eframe/src/web/backend.rs b/crates/eframe/src/web/backend.rs index e07df652..83bdaec2 100644 --- a/crates/eframe/src/web/backend.rs +++ b/crates/eframe/src/web/backend.rs @@ -1,7 +1,6 @@ -use egui::{ - mutex::{Mutex, MutexGuard}, - TexturesDelta, -}; +use std::{cell::RefCell, rc::Rc}; + +use egui::{mutex::Mutex, TexturesDelta}; use crate::{epi, App}; @@ -170,6 +169,64 @@ fn test_parse_query() { // ---------------------------------------------------------------------------- +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_namespace = console)] + fn error(msg: String); + + type Error; + + #[wasm_bindgen(constructor)] + fn new() -> Error; + + #[wasm_bindgen(structural, method, getter)] + fn stack(error: &Error) -> String; +} + +#[derive(Clone, Debug)] +pub struct PanicSummary { + message: String, + callstack: String, +} + +impl PanicSummary { + pub fn new(info: &std::panic::PanicInfo<'_>) -> Self { + let message = info.to_string(); + let callstack = Error::new().stack(); + Self { message, callstack } + } + + pub fn message(&self) -> String { + self.message.clone() + } + + pub fn callstack(&self) -> String { + self.callstack.clone() + } +} + +/// Handle to information about any panic than has occurred +#[derive(Clone, Default)] +pub struct PanicHandler { + summary: Option, +} + +impl PanicHandler { + pub fn has_panicked(&self) -> bool { + self.summary.is_some() + } + + pub fn panic_summary(&self) -> Option { + self.summary.clone() + } + + pub fn on_panic(&mut self, info: &std::panic::PanicInfo<'_>) { + self.summary = Some(PanicSummary::new(info)); + } +} + +// ---------------------------------------------------------------------------- + pub struct AppRunner { pub(crate) frame: epi::Frame, egui_ctx: egui::Context, @@ -183,7 +240,6 @@ pub struct AppRunner { pub(crate) text_cursor_pos: Option, pub(crate) mutable_text_under_cursor: bool, textures_delta: TexturesDelta, - pub events_to_unsubscribe: Vec, } impl Drop for AppRunner { @@ -277,7 +333,6 @@ impl AppRunner { text_cursor_pos: None, mutable_text_under_cursor: false, textures_delta: Default::default(), - events_to_unsubscribe: Default::default(), }; runner.input.raw.max_texture_side = Some(runner.painter.max_texture_side()); @@ -333,21 +388,13 @@ impl AppRunner { Ok(()) } - pub fn destroy(&mut self) -> Result<(), JsValue> { - let is_destroyed_already = self.is_destroyed.fetch(); - - if is_destroyed_already { + fn destroy(&mut self) { + if self.is_destroyed.fetch() { log::warn!("App was destroyed already"); - Ok(()) } else { log::debug!("Destroying"); - for x in self.events_to_unsubscribe.drain(..) { - x.unsubscribe()?; - } - self.painter.destroy(); self.is_destroyed.set_true(); - Ok(()) } } @@ -439,7 +486,130 @@ impl AppRunner { // ---------------------------------------------------------------------------- -pub type AppRunnerRef = Arc>; +/// This is how we access the [`AppRunner`]. +/// This is cheap to clone. +#[derive(Clone)] +pub struct AppRunnerRef { + /// If we ever panic during running, this mutex is poisoned. + /// So before we use it, we need to check `panic_handler`. + runner: Rc>, + + /// Have we ever panicked? + panic_handler: Arc>, + + /// In case of a panic, unsubscribe these. + /// They have to be in a separate `Arc` so that we don't need to pass them to + /// the panic handler, since they aren't `Send`. + events_to_unsubscribe: Rc>>, +} + +impl AppRunnerRef { + pub fn new(runner: AppRunner) -> Self { + Self { + runner: Rc::new(RefCell::new(runner)), + panic_handler: Arc::new(Mutex::new(Default::default())), + events_to_unsubscribe: Rc::new(RefCell::new(Default::default())), + } + } + + /// Returns true if there has been a panic. + fn unsubscribe_if_panicked(&self) { + if self.panic_handler.lock().has_panicked() { + // Unsubscribe from all events so that we don't get any more callbacks + // that will try to access the poisoned runner. + 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 due to panic", + events_to_unsubscribe.len() + ); + for x in events_to_unsubscribe { + if let Err(err) = x.unsubscribe() { + log::error!("Failed to unsubscribe from event: {err:?}"); + } + } + } + } + } + + /// Returns true if there has been a panic. + pub fn has_panicked(&self) -> bool { + self.unsubscribe_if_panicked(); + self.panic_handler.lock().has_panicked() + } + + /// Returns `Some` if there has been a panic. + pub fn panic_summary(&self) -> Option { + self.unsubscribe_if_panicked(); + self.panic_handler.lock().panic_summary() + } + + pub fn destroy(&self) { + if let Some(mut runner) = self.try_lock() { + runner.destroy(); + } + } + + /// Returns `None` if there has been a panic, or if we have been destroyed. + /// In that case, just return to JS. + pub fn try_lock(&self) -> Option> { + if self.has_panicked() { + None + } else { + let lock = self.runner.borrow_mut(); + if lock.is_destroyed.fetch() { + None + } else { + Some(lock) + } + } + } + + /// Convenience function to reduce boilerplate and ensure that all event handlers + /// are dealt with in the same way + pub fn add_event_listener( + &self, + target: &EventTarget, + event_name: &'static str, + mut closure: impl FnMut(E, &mut AppRunner) + 'static, + ) -> Result<(), JsValue> { + // Create a JS closure based on the FnMut provided + let closure = Closure::wrap({ + // Clone atomics + let runner_ref = self.clone(); + + 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(()) + } +} + +// ---------------------------------------------------------------------------- pub struct TargetEvent { target: EventTarget, @@ -454,7 +624,7 @@ pub struct IntervalHandle { pub enum EventToUnsubscribe { TargetEvent(TargetEvent), - #[allow(dead_code)] + IntervalHandle(IntervalHandle), } @@ -477,61 +647,42 @@ impl EventToUnsubscribe { } } -pub struct AppRunnerContainer { - pub runner: AppRunnerRef, - - /// Set to `true` if there is a panic. - /// Used to ignore callbacks after a panic. - pub panicked: Arc, - pub events: Vec, -} - -impl AppRunnerContainer { - /// Convenience function to reduce boilerplate and ensure that all event handlers - /// are dealt with in the same way - pub fn add_event_listener( - &mut self, - target: &EventTarget, - event_name: &'static str, - mut closure: impl FnMut(E, MutexGuard<'_, AppRunner>) + 'static, - ) -> Result<(), JsValue> { - // Create a JS closure based on the FnMut provided - let closure = Closure::wrap({ - // Clone atomics - let runner_ref = self.runner.clone(); - let panicked = self.panicked.clone(); - - Box::new(move |event: web_sys::Event| { - // Only call the wrapped closure if the egui code has not panicked - if !panicked.load(Ordering::SeqCst) { - // Cast the event to the expected event type - let event = event.unchecked_into::(); - - closure(event, runner_ref.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, - }; - - self.events.push(EventToUnsubscribe::TargetEvent(handle)); - - Ok(()) - } -} - // ---------------------------------------------------------------------------- /// Install event listeners to register different input events /// and start running the given app. -pub async fn start( +/// +/// ``` no_run +/// #[cfg(target_arch = "wasm32")] +/// use wasm_bindgen::prelude::*; +/// +/// /// This is the entry-point for all the web-assembly. +/// /// This is called from the HTML. +/// /// It loads the app, installs some callbacks, then returns. +/// /// It returns a handle to the running app that can be stopped calling `AppRunner::stop_web`. +/// /// You can add more callbacks like this if you want to call in to your code. +/// #[cfg(target_arch = "wasm32")] +/// #[wasm_bindgen] +/// pub struct WebHandle { +/// handle: AppRunnerRef, +/// } +/// #[cfg(target_arch = "wasm32")] +/// #[wasm_bindgen] +/// pub async fn start(canvas_id: &str) -> Result { +/// let web_options = eframe::WebOptions::default(); +/// eframe::start_web( +/// canvas_id, +/// web_options, +/// Box::new(|cc| Box::new(MyEguiApp::new(cc))), +/// ) +/// .await +/// .map(|handle| WebHandle { handle }) +/// } +/// ``` +/// +/// # Errors +/// Failing to initialize WebGL graphics. +pub async fn start_web( canvas_id: &str, web_options: crate::WebOptions, app_creator: epi::AppCreator, @@ -544,43 +695,36 @@ pub async fn start( let mut runner = AppRunner::new(canvas_id, web_options, app_creator).await?; runner.warm_up()?; - start_runner(runner, follow_system_theme) -} + let runner_ref = AppRunnerRef::new(runner); -/// Install event listeners to register different input events -/// and starts running the given [`AppRunner`]. -fn start_runner(app_runner: AppRunner, follow_system_theme: bool) -> Result { - let mut runner_container = AppRunnerContainer { - runner: Arc::new(Mutex::new(app_runner)), - panicked: Arc::new(AtomicBool::new(false)), - events: Vec::with_capacity(20), - }; - - super::events::install_canvas_events(&mut runner_container)?; - super::events::install_document_events(&mut runner_container)?; - super::events::install_window_events(&mut runner_container)?; - text_agent::install_text_agent(&mut runner_container)?; - - if follow_system_theme { - super::events::install_color_scheme_change_event(&mut runner_container)?; + // Install events: + { + super::events::install_canvas_events(&runner_ref)?; + super::events::install_document_events(&runner_ref)?; + super::events::install_window_events(&runner_ref)?; + text_agent::install_text_agent(&runner_ref)?; + if follow_system_theme { + super::events::install_color_scheme_change_event(&runner_ref)?; + } + super::events::paint_and_schedule(&runner_ref)?; } - super::events::paint_and_schedule(&runner_container.runner, runner_container.panicked.clone())?; + // Instal panic handler: + { + // Disable all event handlers on panic + let previous_hook = std::panic::take_hook(); + let panic_handler = runner_ref.panic_handler.clone(); - // Disable all event handlers on panic - let previous_hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |panic_info| { + log::info!("eframe detected a panic"); + panic_handler.lock().on_panic(panic_info); - runner_container.runner.lock().events_to_unsubscribe = runner_container.events; + // Propagate panic info to the previously registered panic hook + previous_hook(panic_info); + })); + } - std::panic::set_hook(Box::new(move |panic_info| { - log::info!("egui disabled all event handlers due to panic"); - runner_container.panicked.store(true, SeqCst); - - // Propagate panic info to the previously registered panic hook - previous_hook(panic_info); - })); - - Ok(runner_container.runner) + Ok(runner_ref) } // ---------------------------------------------------------------------------- diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index b8a7f0cd..5294ff26 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -1,99 +1,80 @@ -use std::sync::atomic::{AtomicBool, Ordering}; - -use egui::Key; - use super::*; /// Calls `request_animation_frame` to schedule repaint. /// /// It will only paint if needed, but will always call `request_animation_frame` immediately. -pub fn paint_and_schedule( - runner_ref: &AppRunnerRef, - panicked: Arc, -) -> Result<(), JsValue> { - struct IsDestroyed(pub bool); - - fn paint_if_needed(runner_ref: &AppRunnerRef) -> Result { - let mut runner_lock = runner_ref.lock(); - let is_destroyed = runner_lock.is_destroyed.fetch(); - - if !is_destroyed && runner_lock.needs_repaint.when_to_repaint() <= now_sec() { - runner_lock.needs_repaint.clear(); - let (repaint_after, clipped_primitives) = runner_lock.logic()?; - runner_lock.paint(&clipped_primitives)?; - runner_lock +pub fn paint_and_schedule(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { + fn paint_if_needed(runner: &mut AppRunner) -> Result<(), JsValue> { + if runner.needs_repaint.when_to_repaint() <= now_sec() { + runner.needs_repaint.clear(); + let (repaint_after, clipped_primitives) = runner.logic()?; + runner.paint(&clipped_primitives)?; + runner .needs_repaint .repaint_after(repaint_after.as_secs_f64()); - runner_lock.auto_save_if_needed(); + runner.auto_save_if_needed(); } - - Ok(IsDestroyed(is_destroyed)) + Ok(()) } - fn request_animation_frame( - runner_ref: AppRunnerRef, - panicked: Arc, - ) -> Result<(), JsValue> { + fn request_animation_frame(runner_ref: AppRunnerRef) -> Result<(), JsValue> { let window = web_sys::window().unwrap(); - let closure = Closure::once(move || paint_and_schedule(&runner_ref, panicked)); + let closure = Closure::once(move || paint_and_schedule(&runner_ref)); window.request_animation_frame(closure.as_ref().unchecked_ref())?; closure.forget(); // We must forget it, or else the callback is canceled on drop Ok(()) } // Only paint and schedule if there has been no panic - if !panicked.load(Ordering::SeqCst) { - let is_destroyed = paint_if_needed(runner_ref)?; - if !is_destroyed.0 { - request_animation_frame(runner_ref.clone(), panicked)?; - } + if let Some(mut runner_lock) = runner_ref.try_lock() { + paint_if_needed(&mut runner_lock)?; + drop(runner_lock); + request_animation_frame(runner_ref.clone())?; } Ok(()) } -pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> { +pub fn install_document_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { let document = web_sys::window().unwrap().document().unwrap(); { // Avoid sticky modifier keys on alt-tab: for event_name in ["blur", "focus"] { - let closure = - move |_event: web_sys::MouseEvent, - mut runner_lock: egui::mutex::MutexGuard<'_, AppRunner>| { - let has_focus = event_name == "focus"; + let closure = move |_event: web_sys::MouseEvent, runner: &mut AppRunner| { + let has_focus = event_name == "focus"; - if !has_focus { - // We lost focus - good idea to save - runner_lock.save(); - } + if !has_focus { + // We lost focus - good idea to save + runner.save(); + } - runner_lock.input.on_web_page_focus_change(has_focus); - runner_lock.egui_ctx().request_repaint(); - // log::debug!("{event_name:?}"); - }; + runner.input.on_web_page_focus_change(has_focus); + runner.egui_ctx().request_repaint(); + // log::debug!("{event_name:?}"); + }; - runner_container.add_event_listener(&document, event_name, closure)?; + runner_ref.add_event_listener(&document, event_name, closure)?; } } - runner_container.add_event_listener( + runner_ref.add_event_listener( &document, "keydown", - |event: web_sys::KeyboardEvent, mut runner_lock| { + |event: web_sys::KeyboardEvent, runner| { if event.is_composing() || event.key_code() == 229 { // https://web.archive.org/web/20200526195704/https://www.fxsitecompat.dev/en-CA/docs/2018/keydown-and-keyup-events-are-now-fired-during-ime-composition/ return; } let modifiers = modifiers_from_event(&event); - runner_lock.input.raw.modifiers = modifiers; + runner.input.raw.modifiers = modifiers; let key = event.key(); let egui_key = translate_key(&key); if let Some(key) = egui_key { - runner_lock.input.raw.events.push(egui::Event::Key { + runner.input.raw.events.push(egui::Event::Key { key, pressed: true, repeat: false, // egui will fill this in for us! @@ -106,18 +87,18 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res // When text agent is shown, it sends text event instead. && text_agent::text_agent().hidden() { - runner_lock.input.raw.events.push(egui::Event::Text(key)); + runner.input.raw.events.push(egui::Event::Text(key)); } - runner_lock.needs_repaint.repaint_asap(); + runner.needs_repaint.repaint_asap(); - let egui_wants_keyboard = runner_lock.egui_ctx().wants_keyboard_input(); + let egui_wants_keyboard = runner.egui_ctx().wants_keyboard_input(); #[allow(clippy::if_same_then_else)] - let prevent_default = if egui_key == Some(Key::Tab) { + let prevent_default = if egui_key == Some(egui::Key::Tab) { // Always prevent moving cursor to url bar. // egui wants to use tab to move to the next text field. true - } else if egui_key == Some(Key::P) { + } else if egui_key == Some(egui::Key::P) { #[allow(clippy::needless_bool)] if modifiers.ctrl || modifiers.command || modifiers.mac_cmd { true // Prevent ctrl-P opening the print dialog. Users may want to use it for a command palette. @@ -152,35 +133,35 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res }, )?; - runner_container.add_event_listener( + runner_ref.add_event_listener( &document, "keyup", - |event: web_sys::KeyboardEvent, mut runner_lock| { + |event: web_sys::KeyboardEvent, runner| { let modifiers = modifiers_from_event(&event); - runner_lock.input.raw.modifiers = modifiers; + runner.input.raw.modifiers = modifiers; if let Some(key) = translate_key(&event.key()) { - runner_lock.input.raw.events.push(egui::Event::Key { + runner.input.raw.events.push(egui::Event::Key { key, pressed: false, repeat: false, modifiers, }); } - runner_lock.needs_repaint.repaint_asap(); + runner.needs_repaint.repaint_asap(); }, )?; #[cfg(web_sys_unstable_apis)] - runner_container.add_event_listener( + runner_ref.add_event_listener( &document, "paste", - |event: web_sys::ClipboardEvent, mut runner_lock| { + |event: web_sys::ClipboardEvent, runner| { if let Some(data) = event.clipboard_data() { if let Ok(text) = data.get_data("text") { let text = text.replace("\r\n", "\n"); if !text.is_empty() { - runner_lock.input.raw.events.push(egui::Event::Paste(text)); - runner_lock.needs_repaint.repaint_asap(); + runner.input.raw.events.push(egui::Event::Paste(text)); + runner.needs_repaint.repaint_asap(); } event.stop_propagation(); event.prevent_default(); @@ -190,76 +171,54 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res )?; #[cfg(web_sys_unstable_apis)] - runner_container.add_event_listener( - &document, - "cut", - |_: web_sys::ClipboardEvent, mut runner_lock| { - runner_lock.input.raw.events.push(egui::Event::Cut); - runner_lock.needs_repaint.repaint_asap(); - }, - )?; + runner_ref.add_event_listener(&document, "cut", |_: web_sys::ClipboardEvent, runner| { + runner.input.raw.events.push(egui::Event::Cut); + runner.needs_repaint.repaint_asap(); + })?; #[cfg(web_sys_unstable_apis)] - runner_container.add_event_listener( - &document, - "copy", - |_: web_sys::ClipboardEvent, mut runner_lock| { - runner_lock.input.raw.events.push(egui::Event::Copy); - runner_lock.needs_repaint.repaint_asap(); - }, - )?; + runner_ref.add_event_listener(&document, "copy", |_: web_sys::ClipboardEvent, runner| { + runner.input.raw.events.push(egui::Event::Copy); + runner.needs_repaint.repaint_asap(); + })?; Ok(()) } -pub fn install_window_events(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> { +pub fn install_window_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { let window = web_sys::window().unwrap(); // Save-on-close - runner_container.add_event_listener( - &window, - "onbeforeunload", - |_: web_sys::Event, mut runner_lock| { - runner_lock.save(); - }, - )?; + runner_ref.add_event_listener(&window, "onbeforeunload", |_: web_sys::Event, runner| { + runner.save(); + })?; for event_name in &["load", "pagehide", "pageshow", "resize"] { - runner_container.add_event_listener( - &window, - event_name, - |_: web_sys::Event, runner_lock| { - runner_lock.needs_repaint.repaint_asap(); - }, - )?; + runner_ref.add_event_listener(&window, event_name, |_: web_sys::Event, runner| { + runner.needs_repaint.repaint_asap(); + })?; } - runner_container.add_event_listener( - &window, - "hashchange", - |_: web_sys::Event, mut runner_lock| { - // `epi::Frame::info(&self)` clones `epi::IntegrationInfo`, but we need to modify the original here - runner_lock.frame.info.web_info.location.hash = location_hash(); - }, - )?; + runner_ref.add_event_listener(&window, "hashchange", |_: web_sys::Event, runner| { + // `epi::Frame::info(&self)` clones `epi::IntegrationInfo`, but we need to modify the original here + runner.frame.info.web_info.location.hash = location_hash(); + })?; Ok(()) } -pub fn install_color_scheme_change_event( - runner_container: &mut AppRunnerContainer, -) -> Result<(), JsValue> { +pub fn install_color_scheme_change_event(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { let window = web_sys::window().unwrap(); if let Some(media_query_list) = prefers_color_scheme_dark(&window)? { - runner_container.add_event_listener::( + runner_ref.add_event_listener::( &media_query_list, "change", - |event, mut runner_lock| { + |event, runner| { let theme = theme_from_dark_mode(event.matches()); - runner_lock.frame.info.system_theme = Some(theme); - runner_lock.egui_ctx().set_visuals(theme.egui_visuals()); - runner_lock.needs_repaint.repaint_asap(); + runner.frame.info.system_theme = Some(theme); + runner.egui_ctx().set_visuals(theme.egui_visuals()); + runner.needs_repaint.repaint_asap(); }, )?; } @@ -267,8 +226,8 @@ pub fn install_color_scheme_change_event( Ok(()) } -pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> { - let canvas = canvas_element(runner_container.runner.lock().canvas_id()).unwrap(); +pub fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { + let canvas = canvas_element(runner_ref.try_lock().unwrap().canvas_id()).unwrap(); { let prevent_default_events = [ @@ -280,285 +239,231 @@ pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Resul ]; for event_name in prevent_default_events { - let closure = - move |event: web_sys::MouseEvent, - mut _runner_lock: egui::mutex::MutexGuard<'_, AppRunner>| { - event.prevent_default(); - // event.stop_propagation(); - // log::debug!("Preventing event {event_name:?}"); - }; + let closure = move |event: web_sys::MouseEvent, _runner: &mut AppRunner| { + event.prevent_default(); + // event.stop_propagation(); + // log::debug!("Preventing event {event_name:?}"); + }; - runner_container.add_event_listener(&canvas, event_name, closure)?; + runner_ref.add_event_listener(&canvas, event_name, closure)?; } } - runner_container.add_event_listener( + runner_ref.add_event_listener( &canvas, "mousedown", - |event: web_sys::MouseEvent, mut runner_lock: egui::mutex::MutexGuard<'_, AppRunner>| { + |event: web_sys::MouseEvent, runner: &mut AppRunner| { if let Some(button) = button_from_mouse_event(&event) { - let pos = pos_from_mouse_event(runner_lock.canvas_id(), &event); - let modifiers = runner_lock.input.raw.modifiers; - runner_lock - .input - .raw - .events - .push(egui::Event::PointerButton { - pos, - button, - pressed: true, - modifiers, - }); - runner_lock.needs_repaint.repaint_asap(); + let pos = pos_from_mouse_event(runner.canvas_id(), &event); + let modifiers = runner.input.raw.modifiers; + runner.input.raw.events.push(egui::Event::PointerButton { + pos, + button, + pressed: true, + modifiers, + }); + runner.needs_repaint.repaint_asap(); } event.stop_propagation(); // Note: prevent_default breaks VSCode tab focusing, hence why we don't call it here. }, )?; - runner_container.add_event_listener( + runner_ref.add_event_listener( &canvas, "mousemove", - |event: web_sys::MouseEvent, mut runner_lock| { - let pos = pos_from_mouse_event(runner_lock.canvas_id(), &event); - runner_lock - .input - .raw - .events - .push(egui::Event::PointerMoved(pos)); - runner_lock.needs_repaint.repaint_asap(); + |event: web_sys::MouseEvent, runner| { + let pos = pos_from_mouse_event(runner.canvas_id(), &event); + runner.input.raw.events.push(egui::Event::PointerMoved(pos)); + runner.needs_repaint.repaint_asap(); event.stop_propagation(); event.prevent_default(); }, )?; - runner_container.add_event_listener( - &canvas, - "mouseup", - |event: web_sys::MouseEvent, mut runner_lock| { - if let Some(button) = button_from_mouse_event(&event) { - let pos = pos_from_mouse_event(runner_lock.canvas_id(), &event); - let modifiers = runner_lock.input.raw.modifiers; - runner_lock - .input - .raw - .events - .push(egui::Event::PointerButton { - pos, - button, - pressed: false, - modifiers, - }); - runner_lock.needs_repaint.repaint_asap(); + runner_ref.add_event_listener(&canvas, "mouseup", |event: web_sys::MouseEvent, runner| { + if let Some(button) = button_from_mouse_event(&event) { + let pos = pos_from_mouse_event(runner.canvas_id(), &event); + let modifiers = runner.input.raw.modifiers; + runner.input.raw.events.push(egui::Event::PointerButton { + pos, + button, + pressed: false, + modifiers, + }); + runner.needs_repaint.repaint_asap(); - text_agent::update_text_agent(runner_lock); - } - event.stop_propagation(); - event.prevent_default(); - }, - )?; + text_agent::update_text_agent(runner); + } + event.stop_propagation(); + event.prevent_default(); + })?; - runner_container.add_event_listener( + runner_ref.add_event_listener( &canvas, "mouseleave", - |event: web_sys::MouseEvent, mut runner_lock| { - runner_lock.input.raw.events.push(egui::Event::PointerGone); - runner_lock.needs_repaint.repaint_asap(); + |event: web_sys::MouseEvent, runner| { + runner.input.raw.events.push(egui::Event::PointerGone); + runner.needs_repaint.repaint_asap(); event.stop_propagation(); event.prevent_default(); }, )?; - runner_container.add_event_listener( + runner_ref.add_event_listener( &canvas, "touchstart", - |event: web_sys::TouchEvent, mut runner_lock| { - let mut latest_touch_pos_id = runner_lock.input.latest_touch_pos_id; - let pos = - pos_from_touch_event(runner_lock.canvas_id(), &event, &mut latest_touch_pos_id); - runner_lock.input.latest_touch_pos_id = latest_touch_pos_id; - runner_lock.input.latest_touch_pos = Some(pos); - let modifiers = runner_lock.input.raw.modifiers; - runner_lock - .input - .raw - .events - .push(egui::Event::PointerButton { - pos, - button: egui::PointerButton::Primary, - pressed: true, - modifiers, - }); - - push_touches(&mut runner_lock, egui::TouchPhase::Start, &event); - runner_lock.needs_repaint.repaint_asap(); - event.stop_propagation(); - event.prevent_default(); - }, - )?; - - runner_container.add_event_listener( - &canvas, - "touchmove", - |event: web_sys::TouchEvent, mut runner_lock| { - let mut latest_touch_pos_id = runner_lock.input.latest_touch_pos_id; - let pos = - pos_from_touch_event(runner_lock.canvas_id(), &event, &mut latest_touch_pos_id); - runner_lock.input.latest_touch_pos_id = latest_touch_pos_id; - runner_lock.input.latest_touch_pos = Some(pos); - runner_lock - .input - .raw - .events - .push(egui::Event::PointerMoved(pos)); - - push_touches(&mut runner_lock, egui::TouchPhase::Move, &event); - runner_lock.needs_repaint.repaint_asap(); - event.stop_propagation(); - event.prevent_default(); - }, - )?; - - runner_container.add_event_listener( - &canvas, - "touchend", - |event: web_sys::TouchEvent, mut runner_lock| { - if let Some(pos) = runner_lock.input.latest_touch_pos { - let modifiers = runner_lock.input.raw.modifiers; - // First release mouse to click: - runner_lock - .input - .raw - .events - .push(egui::Event::PointerButton { - pos, - button: egui::PointerButton::Primary, - pressed: false, - modifiers, - }); - // Then remove hover effect: - runner_lock.input.raw.events.push(egui::Event::PointerGone); - - push_touches(&mut runner_lock, egui::TouchPhase::End, &event); - runner_lock.needs_repaint.repaint_asap(); - event.stop_propagation(); - event.prevent_default(); - } - - // Finally, focus or blur text agent to toggle mobile keyboard: - text_agent::update_text_agent(runner_lock); - }, - )?; - - runner_container.add_event_listener( - &canvas, - "touchcancel", - |event: web_sys::TouchEvent, mut runner_lock| { - push_touches(&mut runner_lock, egui::TouchPhase::Cancel, &event); - event.stop_propagation(); - event.prevent_default(); - }, - )?; - - runner_container.add_event_listener( - &canvas, - "wheel", - |event: web_sys::WheelEvent, mut runner_lock| { - let unit = match event.delta_mode() { - web_sys::WheelEvent::DOM_DELTA_PIXEL => egui::MouseWheelUnit::Point, - web_sys::WheelEvent::DOM_DELTA_LINE => egui::MouseWheelUnit::Line, - web_sys::WheelEvent::DOM_DELTA_PAGE => egui::MouseWheelUnit::Page, - _ => return, - }; - // delta sign is flipped to match native (winit) convention. - let delta = -egui::vec2(event.delta_x() as f32, event.delta_y() as f32); - let modifiers = runner_lock.input.raw.modifiers; - - runner_lock.input.raw.events.push(egui::Event::MouseWheel { - unit, - delta, + |event: web_sys::TouchEvent, runner| { + let mut latest_touch_pos_id = runner.input.latest_touch_pos_id; + let pos = pos_from_touch_event(runner.canvas_id(), &event, &mut latest_touch_pos_id); + runner.input.latest_touch_pos_id = latest_touch_pos_id; + runner.input.latest_touch_pos = Some(pos); + let modifiers = runner.input.raw.modifiers; + runner.input.raw.events.push(egui::Event::PointerButton { + pos, + button: egui::PointerButton::Primary, + pressed: true, modifiers, }); - let scroll_multiplier = match unit { - egui::MouseWheelUnit::Page => canvas_size_in_points(runner_lock.canvas_id()).y, - egui::MouseWheelUnit::Line => { - #[allow(clippy::let_and_return)] - let points_per_scroll_line = 8.0; // Note that this is intentionally different from what we use in winit. - points_per_scroll_line - } - egui::MouseWheelUnit::Point => 1.0, - }; - - let mut delta = scroll_multiplier * delta; - - // Report a zoom event in case CTRL (on Windows or Linux) or CMD (on Mac) is pressed. - // This if-statement is equivalent to how `Modifiers.command` is determined in - // `modifiers_from_event()`, but we cannot directly use that fn for a [`WheelEvent`]. - if event.ctrl_key() || event.meta_key() { - let factor = (delta.y / 200.0).exp(); - runner_lock.input.raw.events.push(egui::Event::Zoom(factor)); - } else { - if event.shift_key() { - // Treat as horizontal scrolling. - // Note: one Mac we already get horizontal scroll events when shift is down. - delta = egui::vec2(delta.x + delta.y, 0.0); - } - - runner_lock - .input - .raw - .events - .push(egui::Event::Scroll(delta)); - } - - runner_lock.needs_repaint.repaint_asap(); + push_touches(runner, egui::TouchPhase::Start, &event); + runner.needs_repaint.repaint_asap(); event.stop_propagation(); event.prevent_default(); }, )?; - runner_container.add_event_listener( + runner_ref.add_event_listener( &canvas, - "dragover", - |event: web_sys::DragEvent, mut runner_lock| { - if let Some(data_transfer) = event.data_transfer() { - runner_lock.input.raw.hovered_files.clear(); - for i in 0..data_transfer.items().length() { - if let Some(item) = data_transfer.items().get(i) { - runner_lock.input.raw.hovered_files.push(egui::HoveredFile { - mime: item.type_(), - ..Default::default() - }); - } - } - runner_lock.needs_repaint.repaint_asap(); - event.stop_propagation(); - event.prevent_default(); - } - }, - )?; + "touchmove", + |event: web_sys::TouchEvent, runner| { + let mut latest_touch_pos_id = runner.input.latest_touch_pos_id; + let pos = pos_from_touch_event(runner.canvas_id(), &event, &mut latest_touch_pos_id); + runner.input.latest_touch_pos_id = latest_touch_pos_id; + runner.input.latest_touch_pos = Some(pos); + runner.input.raw.events.push(egui::Event::PointerMoved(pos)); - runner_container.add_event_listener( - &canvas, - "dragleave", - |event: web_sys::DragEvent, mut runner_lock| { - runner_lock.input.raw.hovered_files.clear(); - runner_lock.needs_repaint.repaint_asap(); + push_touches(runner, egui::TouchPhase::Move, &event); + runner.needs_repaint.repaint_asap(); event.stop_propagation(); event.prevent_default(); }, )?; - runner_container.add_event_listener(&canvas, "drop", { - let runner_ref = runner_container.runner.clone(); + runner_ref.add_event_listener(&canvas, "touchend", |event: web_sys::TouchEvent, runner| { + if let Some(pos) = runner.input.latest_touch_pos { + let modifiers = runner.input.raw.modifiers; + // First release mouse to click: + runner.input.raw.events.push(egui::Event::PointerButton { + pos, + button: egui::PointerButton::Primary, + pressed: false, + modifiers, + }); + // Then remove hover effect: + runner.input.raw.events.push(egui::Event::PointerGone); - move |event: web_sys::DragEvent, mut runner_lock| { + push_touches(runner, egui::TouchPhase::End, &event); + runner.needs_repaint.repaint_asap(); + event.stop_propagation(); + event.prevent_default(); + } + + // Finally, focus or blur text agent to toggle mobile keyboard: + text_agent::update_text_agent(runner); + })?; + + runner_ref.add_event_listener( + &canvas, + "touchcancel", + |event: web_sys::TouchEvent, runner| { + push_touches(runner, egui::TouchPhase::Cancel, &event); + event.stop_propagation(); + event.prevent_default(); + }, + )?; + + runner_ref.add_event_listener(&canvas, "wheel", |event: web_sys::WheelEvent, runner| { + let unit = match event.delta_mode() { + web_sys::WheelEvent::DOM_DELTA_PIXEL => egui::MouseWheelUnit::Point, + web_sys::WheelEvent::DOM_DELTA_LINE => egui::MouseWheelUnit::Line, + web_sys::WheelEvent::DOM_DELTA_PAGE => egui::MouseWheelUnit::Page, + _ => return, + }; + // delta sign is flipped to match native (winit) convention. + let delta = -egui::vec2(event.delta_x() as f32, event.delta_y() as f32); + let modifiers = runner.input.raw.modifiers; + + runner.input.raw.events.push(egui::Event::MouseWheel { + unit, + delta, + modifiers, + }); + + let scroll_multiplier = match unit { + egui::MouseWheelUnit::Page => canvas_size_in_points(runner.canvas_id()).y, + egui::MouseWheelUnit::Line => { + #[allow(clippy::let_and_return)] + let points_per_scroll_line = 8.0; // Note that this is intentionally different from what we use in winit. + points_per_scroll_line + } + egui::MouseWheelUnit::Point => 1.0, + }; + + let mut delta = scroll_multiplier * delta; + + // Report a zoom event in case CTRL (on Windows or Linux) or CMD (on Mac) is pressed. + // This if-statement is equivalent to how `Modifiers.command` is determined in + // `modifiers_from_event()`, but we cannot directly use that fn for a [`WheelEvent`]. + if event.ctrl_key() || event.meta_key() { + let factor = (delta.y / 200.0).exp(); + runner.input.raw.events.push(egui::Event::Zoom(factor)); + } else { + if event.shift_key() { + // Treat as horizontal scrolling. + // Note: one Mac we already get horizontal scroll events when shift is down. + delta = egui::vec2(delta.x + delta.y, 0.0); + } + + runner.input.raw.events.push(egui::Event::Scroll(delta)); + } + + runner.needs_repaint.repaint_asap(); + event.stop_propagation(); + event.prevent_default(); + })?; + + runner_ref.add_event_listener(&canvas, "dragover", |event: web_sys::DragEvent, runner| { + if let Some(data_transfer) = event.data_transfer() { + runner.input.raw.hovered_files.clear(); + for i in 0..data_transfer.items().length() { + if let Some(item) = data_transfer.items().get(i) { + runner.input.raw.hovered_files.push(egui::HoveredFile { + mime: item.type_(), + ..Default::default() + }); + } + } + runner.needs_repaint.repaint_asap(); + event.stop_propagation(); + event.prevent_default(); + } + })?; + + runner_ref.add_event_listener(&canvas, "dragleave", |event: web_sys::DragEvent, runner| { + runner.input.raw.hovered_files.clear(); + runner.needs_repaint.repaint_asap(); + event.stop_propagation(); + event.prevent_default(); + })?; + + runner_ref.add_event_listener(&canvas, "drop", { + let runner_ref = runner_ref.clone(); + + move |event: web_sys::DragEvent, runner| { if let Some(data_transfer) = event.data_transfer() { - runner_lock.input.raw.hovered_files.clear(); - runner_lock.needs_repaint.repaint_asap(); - // Unlock the runner so it can be locked after a future await point - drop(runner_lock); + runner.input.raw.hovered_files.clear(); + runner.needs_repaint.repaint_asap(); if let Some(files) = data_transfer.files() { for i in 0..files.length() { @@ -578,17 +483,17 @@ pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Resul let bytes = js_sys::Uint8Array::new(&array_buffer).to_vec(); log::debug!("Loaded {:?} ({} bytes).", name, bytes.len()); - // Re-lock the mutex on the other side of the await point - let mut runner_lock = runner_ref.lock(); - runner_lock.input.raw.dropped_files.push( - egui::DroppedFile { - name, - last_modified: Some(last_modified), - bytes: Some(bytes.into()), - ..Default::default() - }, - ); - runner_lock.needs_repaint.repaint_asap(); + if let Some(mut runner_lock) = runner_ref.try_lock() { + runner_lock.input.raw.dropped_files.push( + egui::DroppedFile { + name, + last_modified: Some(last_modified), + bytes: Some(bytes.into()), + ..Default::default() + }, + ); + runner_lock.needs_repaint.repaint_asap(); + } } Err(err) => { log::error!("Failed to read file: {:?}", err); diff --git a/crates/eframe/src/web/mod.rs b/crates/eframe/src/web/mod.rs index a78ce931..2251728a 100644 --- a/crates/eframe/src/web/mod.rs +++ b/crates/eframe/src/web/mod.rs @@ -32,10 +32,7 @@ pub use events::*; pub use storage::*; use std::collections::BTreeMap; -use std::sync::{ - atomic::{AtomicBool, Ordering}, - Arc, -}; +use std::sync::Arc; use egui::Vec2; use wasm_bindgen::prelude::*; diff --git a/crates/eframe/src/web/text_agent.rs b/crates/eframe/src/web/text_agent.rs index de83ddab..66e08ea8 100644 --- a/crates/eframe/src/web/text_agent.rs +++ b/crates/eframe/src/web/text_agent.rs @@ -1,12 +1,12 @@ //! The text agent is an `` element used to trigger //! mobile keyboard and IME input. +//! +use std::{cell::Cell, rc::Rc}; -use super::{canvas_element, AppRunner, AppRunnerContainer}; -use egui::mutex::MutexGuard; -use std::cell::Cell; -use std::rc::Rc; use wasm_bindgen::prelude::*; +use super::{canvas_element, AppRunner, AppRunnerRef}; + static AGENT_ID: &str = "egui_text_agent"; pub fn text_agent() -> web_sys::HtmlInputElement { @@ -21,7 +21,7 @@ pub fn text_agent() -> web_sys::HtmlInputElement { } /// Text event handler, -pub fn install_text_agent(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> { +pub fn install_text_agent(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { let window = web_sys::window().unwrap(); let document = window.document().unwrap(); let body = document.body().expect("document should have a body"); @@ -44,60 +44,56 @@ pub fn install_text_agent(runner_container: &mut AppRunnerContainer) -> Result<( input.set_hidden(true); // When IME is off - runner_container.add_event_listener(&input, "input", { + runner_ref.add_event_listener(&input, "input", { let input_clone = input.clone(); let is_composing = is_composing.clone(); - move |_event: web_sys::InputEvent, mut runner_lock| { + move |_event: web_sys::InputEvent, runner| { let text = input_clone.value(); if !text.is_empty() && !is_composing.get() { input_clone.set_value(""); - runner_lock.input.raw.events.push(egui::Event::Text(text)); - runner_lock.needs_repaint.repaint_asap(); + runner.input.raw.events.push(egui::Event::Text(text)); + runner.needs_repaint.repaint_asap(); } } })?; { // When IME is on, handle composition event - runner_container.add_event_listener(&input, "compositionstart", { + runner_ref.add_event_listener(&input, "compositionstart", { let input_clone = input.clone(); let is_composing = is_composing.clone(); - move |_event: web_sys::CompositionEvent, mut runner_lock: MutexGuard<'_, AppRunner>| { + move |_event: web_sys::CompositionEvent, runner: &mut AppRunner| { is_composing.set(true); input_clone.set_value(""); - runner_lock - .input - .raw - .events - .push(egui::Event::CompositionStart); - runner_lock.needs_repaint.repaint_asap(); + runner.input.raw.events.push(egui::Event::CompositionStart); + runner.needs_repaint.repaint_asap(); } })?; - runner_container.add_event_listener( + runner_ref.add_event_listener( &input, "compositionupdate", - move |event: web_sys::CompositionEvent, mut runner_lock: MutexGuard<'_, AppRunner>| { + move |event: web_sys::CompositionEvent, runner: &mut AppRunner| { if let Some(event) = event.data().map(egui::Event::CompositionUpdate) { - runner_lock.input.raw.events.push(event); - runner_lock.needs_repaint.repaint_asap(); + runner.input.raw.events.push(event); + runner.needs_repaint.repaint_asap(); } }, )?; - runner_container.add_event_listener(&input, "compositionend", { + runner_ref.add_event_listener(&input, "compositionend", { let input_clone = input.clone(); - move |event: web_sys::CompositionEvent, mut runner_lock: MutexGuard<'_, AppRunner>| { + move |event: web_sys::CompositionEvent, runner: &mut AppRunner| { is_composing.set(false); input_clone.set_value(""); if let Some(event) = event.data().map(egui::Event::CompositionEnd) { - runner_lock.input.raw.events.push(event); - runner_lock.needs_repaint.repaint_asap(); + runner.input.raw.events.push(event); + runner.needs_repaint.repaint_asap(); } } })?; @@ -105,20 +101,16 @@ pub fn install_text_agent(runner_container: &mut AppRunnerContainer) -> Result<( // When input lost focus, focus on it again. // It is useful when user click somewhere outside canvas. - runner_container.add_event_listener( - &input, - "focusout", - move |_event: web_sys::MouseEvent, _| { - // Delay 10 ms, and focus again. - let func = js_sys::Function::new_no_args(&format!( - "document.getElementById('{}').focus()", - AGENT_ID - )); - window - .set_timeout_with_callback_and_timeout_and_arguments_0(&func, 10) - .unwrap(); - }, - )?; + runner_ref.add_event_listener(&input, "focusout", move |_event: web_sys::MouseEvent, _| { + // Delay 10 ms, and focus again. + let func = js_sys::Function::new_no_args(&format!( + "document.getElementById('{}').focus()", + AGENT_ID + )); + window + .set_timeout_with_callback_and_timeout_and_arguments_0(&func, 10) + .unwrap(); + })?; body.append_child(&input)?; @@ -126,7 +118,7 @@ pub fn install_text_agent(runner_container: &mut AppRunnerContainer) -> Result<( } /// Focus or blur text agent to toggle mobile keyboard. -pub fn update_text_agent(runner: MutexGuard<'_, AppRunner>) -> Option<()> { +pub fn update_text_agent(runner: &mut AppRunner) -> Option<()> { use web_sys::HtmlInputElement; let window = web_sys::window()?; let document = window.document()?; @@ -166,9 +158,6 @@ pub fn update_text_agent(runner: MutexGuard<'_, AppRunner>) -> Option<()> { } } } else { - // Drop runner lock - drop(runner); - // Holding the runner lock while calling input.blur() causes a panic. // This is most probably caused by the browser running the event handler // for the triggered blur event synchronously, meaning that the mutex @@ -178,15 +167,33 @@ pub fn update_text_agent(runner: MutexGuard<'_, AppRunner>) -> Option<()> { // and this apparently is the fix for it // // ¯\_(ツ)_/¯ - @DusterTheFirst - input.blur().ok()?; - input.set_hidden(true); - canvas_style.set_property("position", "absolute").ok()?; - canvas_style.set_property("top", "0%").ok()?; // move back to normal position + // So since we are inside a runner lock here, we just postpone the blur/hide: + + call_after_delay(std::time::Duration::from_millis(0), move || { + input.blur().ok(); + input.set_hidden(true); + canvas_style.set_property("position", "absolute").ok(); + canvas_style.set_property("top", "0%").ok(); // move back to normal position + }); } Some(()) } +fn call_after_delay(delay: std::time::Duration, f: impl FnOnce() + 'static) { + use wasm_bindgen::prelude::*; + let window = web_sys::window().unwrap(); + let closure = Closure::once(f); + let delay_ms = delay.as_millis() as _; + window + .set_timeout_with_callback_and_timeout_and_arguments_0( + closure.as_ref().unchecked_ref(), + delay_ms, + ) + .unwrap(); + closure.forget(); // We must forget it, or else the callback is canceled on drop +} + /// If context is running under mobile device? fn is_mobile() -> Option { const MOBILE_DEVICE: [&str; 6] = ["Android", "iPhone", "iPad", "iPod", "webOS", "BlackBerry"]; diff --git a/crates/egui_demo_app/src/backend_panel.rs b/crates/egui_demo_app/src/backend_panel.rs index fabbe86a..7fc9fbce 100644 --- a/crates/egui_demo_app/src/backend_panel.rs +++ b/crates/egui_demo_app/src/backend_panel.rs @@ -108,11 +108,10 @@ impl BackendPanel { ui.ctx().set_debug_on_hover(debug_on_hover); } - ui.separator(); - #[cfg(target_arch = "wasm32")] #[cfg(feature = "web_screen-reader")] { + ui.separator(); let mut screen_reader = ui.ctx().options(|o| o.screen_reader); ui.checkbox(&mut screen_reader, "🔈 Screen reader").on_hover_text("Experimental feature: checking this will turn on the screen reader on supported platforms"); ui.ctx().options_mut(|o| o.screen_reader = screen_reader); @@ -125,6 +124,15 @@ impl BackendPanel { frame.close(); } } + + if cfg!(debug_assertions) && cfg!(target_arch = "wasm32") { + ui.separator(); + // For testing panic handling on web: + #[allow(clippy::manual_assert)] + if ui.button("panic!()").clicked() { + panic!("intentional panic!"); + } + } } fn integration_ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) { diff --git a/crates/egui_demo_app/src/lib.rs b/crates/egui_demo_app/src/lib.rs index 4608c2d6..aa220f2b 100644 --- a/crates/egui_demo_app/src/lib.rs +++ b/crates/egui_demo_app/src/lib.rs @@ -3,12 +3,9 @@ mod apps; mod backend_panel; -pub(crate) mod frame_history; +mod frame_history; mod wrap_app; -#[cfg(target_arch = "wasm32")] -use eframe::web::AppRunnerRef; - pub use wrap_app::WrapApp; /// Time of day as seconds since midnight. Used for clock in demo app. @@ -21,60 +18,7 @@ pub(crate) fn seconds_since_midnight() -> f64 { // ---------------------------------------------------------------------------- #[cfg(target_arch = "wasm32")] -use eframe::wasm_bindgen::{self, prelude::*}; +mod web; #[cfg(target_arch = "wasm32")] -#[wasm_bindgen] -pub struct WebHandle { - handle: AppRunnerRef, -} - -#[cfg(target_arch = "wasm32")] -#[wasm_bindgen] -impl WebHandle { - #[wasm_bindgen] - pub fn stop_web(&self) -> Result<(), wasm_bindgen::JsValue> { - let mut app = self.handle.lock(); - app.destroy() - } - - #[wasm_bindgen] - pub fn set_some_content_from_javascript(&mut self, _some_data: &str) { - let _app = self.handle.lock().app_mut::(); - // _app.data = some_data; - } -} - -#[cfg(target_arch = "wasm32")] -#[wasm_bindgen] -pub fn init_wasm_hooks() { - // Make sure panics are logged using `console.error`. - console_error_panic_hook::set_once(); - - // Redirect tracing to console.log and friends: - eframe::web::WebLogger::init(log::LevelFilter::Debug).ok(); -} - -#[cfg(target_arch = "wasm32")] -#[wasm_bindgen] -pub async fn start_separate(canvas_id: &str) -> Result { - let web_options = eframe::WebOptions::default(); - eframe::start_web( - canvas_id, - web_options, - Box::new(|cc| Box::new(WrapApp::new(cc))), - ) - .await - .map(|handle| WebHandle { handle }) -} - -/// This is the entry-point for all the web-assembly. -/// This is called once from the HTML. -/// It loads the app, installs some callbacks, then returns. -/// You can add more callbacks like this if you want to call in to your code. -#[cfg(target_arch = "wasm32")] -#[wasm_bindgen] -pub async fn start(canvas_id: &str) -> Result { - init_wasm_hooks(); - start_separate(canvas_id).await -} +pub use web::*; diff --git a/crates/egui_demo_app/src/web.rs b/crates/egui_demo_app/src/web.rs new file mode 100644 index 00000000..7ffe99d3 --- /dev/null +++ b/crates/egui_demo_app/src/web.rs @@ -0,0 +1,57 @@ +use eframe::{ + wasm_bindgen::{self, prelude::*}, + web::AppRunnerRef, +}; + +use crate::WrapApp; + +#[wasm_bindgen] +pub struct WebHandle { + runner: AppRunnerRef, +} + +#[wasm_bindgen] +impl WebHandle { + /// This is the entry-point for all the web-assembly. + /// + /// This is called once from the HTML. + /// It loads the app, installs some callbacks, then returns. + #[wasm_bindgen(constructor)] + pub async fn new(canvas_id: &str) -> Result { + // Redirect tracing to console.log and friends: + eframe::web::WebLogger::init(log::LevelFilter::Debug).ok(); + + // Make sure panics are logged using `console.error`. + console_error_panic_hook::set_once(); + + let web_options = eframe::WebOptions::default(); + let runner = eframe::start_web( + canvas_id, + web_options, + Box::new(|cc| Box::new(WrapApp::new(cc))), + ) + .await?; + + Ok(WebHandle { runner }) + } + + #[wasm_bindgen] + pub fn destroy(&self) { + self.runner.destroy(); + } + + #[wasm_bindgen] + pub fn has_panicked(&self) -> bool { + self.runner.panic_summary().is_some() + } + + #[wasm_bindgen] + pub fn panic_message(&self) -> Option { + self.runner.panic_summary().map(|s| s.message()) + } + + #[wasm_bindgen] + pub fn panic_callstack(&self) -> Option { + self.runner.panic_summary().map(|s| s.callstack()) + } +} diff --git a/docs/index.html b/docs/index.html index e228ed34..8d6e7c81 100644 --- a/docs/index.html +++ b/docs/index.html @@ -91,6 +91,7 @@ transform: rotate(360deg); } } + @@ -124,27 +125,58 @@ // We'll defer our execution until the wasm is ready to go. // Here we tell bindgen the path to the wasm file so it can start // initialization and return to us a promise when it's done. - console.debug("loading wasm…"); + console.debug("Loading wasm…"); wasm_bindgen("./egui_demo_app_bg.wasm") .then(on_wasm_loaded) - .catch(on_wasm_error); + .catch(on_error); function on_wasm_loaded() { - console.debug("wasm loaded. starting app…"); + console.debug("Wasm loaded. Starting app…"); // This call installs a bunch of callbacks and then returns: - const handle = wasm_bindgen.start("the_canvas_id"); - - // call `handle.stop_web()` to stop - // uncomment to quick result - // setTimeout(() => {handle.stop_web(); handle.free())}, 2000) - - console.debug("app started."); - document.getElementById("center_text").remove(); + let handle = new wasm_bindgen.WebHandle("the_canvas_id"); + handle.then(on_app_started).catch(on_error); } - function on_wasm_error(error) { + function on_app_started(handle) { + // Call `handle.destroy()` to stop. Uncomment to quick result: + // setTimeout(() => { handle.destroy(); handle.free()) }, 2000) + + console.debug("App started."); + document.getElementById("center_text").innerHTML = ''; + + function check_for_panic() { + if (handle.has_panicked()) { + console.error("The egui app has crashed"); + + // The demo app already logs the panic message and callstack, but you + // can access them like this if you want to show them in the html: + // console.error(`${handle.panic_message()}`); + // console.error(`${handle.panic_callstack()}`); + + document.getElementById("the_canvas_id").remove(); + document.getElementById("center_text").innerHTML = ` +

+ The egui app has crashed. +

+

+ See the console for details. +

+

+ Reload the page to try again. +

`; + } else { + let delay_ms = 1000; + setTimeout(check_for_panic, delay_ms); + } + } + + check_for_panic(); + } + + function on_error(error) { console.error("Failed to start: " + error); + document.getElementById("the_canvas_id").remove(); document.getElementById("center_text").innerHTML = `

An error occurred during loading: diff --git a/docs/multiple_apps.html b/docs/multiple_apps.html index e08659c0..2e38f259 100644 --- a/docs/multiple_apps.html +++ b/docs/multiple_apps.html @@ -1,3 +1,4 @@ + @@ -17,10 +18,10 @@ /* Light mode background color for what is not covered by the egui canvas, or where the egui canvas is translucent. */ background: #909090; - display:flex; + display: flex; } - .canvas_wrap{ + .canvas_wrap { /* height: 200px; */ width: 400px; } @@ -52,8 +53,8 @@ top: 0%; left: 50%; transform: translate(-50%, 0%); */ - width:90%; - height:90%; + width: 45%; + height: 90%; } .centered { @@ -99,6 +100,7 @@ transform: rotate(360deg); } } + @@ -108,19 +110,17 @@

controls
- +
-
- +
+
- -

Loading… @@ -148,38 +148,41 @@ // We'll defer our execution until the wasm is ready to go. // Here we tell bindgen the path to the wasm file so it can start // initialization and return to us a promise when it's done. - console.debug("loading wasm…"); + console.debug("Loading wasm…"); wasm_bindgen("./egui_demo_app_bg.wasm") .then(on_wasm_loaded) - .catch(on_wasm_error); + .catch(on_error); function on_wasm_loaded() { - console.debug("wasm loaded. starting app…"); + console.debug("Wasm loaded. Starting apps…"); // This call installs a bunch of callbacks and then returns: - wasm_bindgen.init_wasm_hooks() + const handle_one = new wasm_bindgen.WebHandle("canvas_id_one"); + const handle_two = new wasm_bindgen.WebHandle("canvas_id_two"); - const handle_one = wasm_bindgen.start_separate("the_canvas_id_one"); - const handle_two = wasm_bindgen.start_separate("the_canvas_id_two"); + Promise.all([handle_one, handle_two]).then((handles) => { + on_apps_started(handles[0], handles[1]) + }).catch(on_error); + } + function on_apps_started(handle_one, handle_two) { const button = document.getElementsByClassName("stop_one")[0] - button.addEventListener("click", ()=>{ - handle_one.stop_web() + button.addEventListener("click", () => { + document.getElementById("canvas_id_one").remove() + handle_one.destroy() handle_one.free() }); - - // call `handle.stop_web()` to stop - // uncomment to quick result - // setTimeout(() => {handle.stop_web()}, 2000) + // Call `handle.destroy()` to stop. Uncomment to quick result: + // setTimeout(() => { handle.destroy() }, 2000) - console.debug("app started."); + console.debug("Apps started."); document.getElementById("center_text").remove(); } - function on_wasm_error(error) { + function on_error(error) { console.error("Failed to start: " + error); document.getElementById("center_text").innerHTML = `