diff --git a/Cargo.lock b/Cargo.lock index 95184825..5096cda2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -789,16 +789,6 @@ dependencies = [ "env_logger", ] -[[package]] -name = "console_error_panic_hook" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" -dependencies = [ - "cfg-if", - "wasm-bindgen", -] - [[package]] name = "core-foundation" version = "0.9.3" @@ -1281,7 +1271,6 @@ version = "0.21.0" dependencies = [ "bytemuck", "chrono", - "console_error_panic_hook", "eframe", "egui", "egui_demo_lib", diff --git a/crates/eframe/src/lib.rs b/crates/eframe/src/lib.rs index 90b73cc3..929b7b98 100644 --- a/crates/eframe/src/lib.rs +++ b/crates/eframe/src/lib.rs @@ -42,15 +42,74 @@ //! //! ## Usage, web: //! ``` no_run -//! #[cfg(target_arch = "wasm32")] +//! # #[cfg(target_arch = "wasm32")] //! use wasm_bindgen::prelude::*; //! -//! /// Call this once from the HTML. -//! #[cfg(target_arch = "wasm32")] +//! /// Your handle to the web app from JavaScript. +//! # #[cfg(target_arch = "wasm32")] +//! #[derive(Clone)] //! #[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 +//! pub struct WebHandle { +//! runner: WebRunner, +//! } +//! +//! # #[cfg(target_arch = "wasm32")] +//! #[wasm_bindgen] +//! impl WebHandle { +//! /// Installs a panic hook, then returns. +//! #[allow(clippy::new_without_default)] +//! #[wasm_bindgen(constructor)] +//! pub fn new() -> Self { +//! // Redirect [`log`] message to `console.log` and friends: +//! eframe::web::WebLogger::init(log::LevelFilter::Debug).ok(); +//! +//! Self { +//! runner: WebRunner::new(), +//! } +//! } +//! +//! /// Call this once from JavaScript to start your app. +//! #[wasm_bindgen] +//! pub async fn start(&self, canvas_id: &str) -> Result<(), wasm_bindgen::JsValue> { +//! self.runner +//! .start( +//! canvas_id, +//! eframe::WebOptions::default(), +//! Box::new(|cc| Box::new(MyEguiApp::new(cc))), +//! ) +//! .await +//! } +//! +//! // The following are optional: +//! +//! #[wasm_bindgen] +//! pub fn destroy(&self) { +//! self.runner.destroy(); +//! } +//! +//! /// Example on how to call into your app from JavaScript. +//! #[wasm_bindgen] +//! pub fn example(&self) { +//! if let Some(app) = self.runner.app_mut::() { +//! app.example(); +//! } +//! } +//! +//! /// The JavaScript can check whether or not your app has crashed: +//! #[wasm_bindgen] +//! pub fn has_panicked(&self) -> bool { +//! self.runner.has_panicked() +//! } +//! +//! #[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()) +//! } //! } //! ``` //! @@ -91,7 +150,7 @@ pub use web_sys; pub mod web; #[cfg(target_arch = "wasm32")] -pub use web::start_web; +pub use web::WebRunner; // ---------------------------------------------------------------------------- // When compiling natively diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs new file mode 100644 index 00000000..4da93c52 --- /dev/null +++ b/crates/eframe/src/web/app_runner.rs @@ -0,0 +1,273 @@ +use egui::TexturesDelta; +use wasm_bindgen::JsValue; + +use crate::{epi, App}; + +use super::{now_sec, web_painter::WebPainter, NeedRepaint}; + +pub struct AppRunner { + pub(crate) frame: epi::Frame, + egui_ctx: egui::Context, + painter: super::ActiveWebPainter, + pub(crate) input: super::WebInput, + app: Box, + pub(crate) needs_repaint: std::sync::Arc, + last_save_time: f64, + screen_reader: super::screen_reader::ScreenReader, + pub(crate) text_cursor_pos: Option, + pub(crate) mutable_text_under_cursor: bool, + textures_delta: TexturesDelta, +} + +impl Drop for AppRunner { + fn drop(&mut self) { + log::debug!("AppRunner has fully dropped"); + } +} + +impl AppRunner { + /// # Errors + /// Failure to initialize WebGL renderer. + pub async fn new( + canvas_id: &str, + web_options: crate::WebOptions, + app_creator: epi::AppCreator, + ) -> Result { + let painter = super::ActiveWebPainter::new(canvas_id, &web_options).await?; + + let system_theme = if web_options.follow_system_theme { + super::system_theme() + } else { + None + }; + + let info = epi::IntegrationInfo { + web_info: epi::WebInfo { + user_agent: super::user_agent().unwrap_or_default(), + location: super::web_location(), + }, + system_theme, + cpu_usage: None, + native_pixels_per_point: Some(super::native_pixels_per_point()), + }; + let storage = LocalStorage::default(); + + let egui_ctx = egui::Context::default(); + egui_ctx.set_os(egui::os::OperatingSystem::from_user_agent( + &super::user_agent().unwrap_or_default(), + )); + super::load_memory(&egui_ctx); + + let theme = system_theme.unwrap_or(web_options.default_theme); + egui_ctx.set_visuals(theme.egui_visuals()); + + let app = app_creator(&epi::CreationContext { + egui_ctx: egui_ctx.clone(), + integration_info: info.clone(), + storage: Some(&storage), + + #[cfg(feature = "glow")] + gl: Some(painter.gl().clone()), + + #[cfg(all(feature = "wgpu", not(feature = "glow")))] + wgpu_render_state: painter.render_state(), + #[cfg(all(feature = "wgpu", feature = "glow"))] + wgpu_render_state: None, + }); + + let frame = epi::Frame { + info, + output: Default::default(), + storage: Some(Box::new(storage)), + + #[cfg(feature = "glow")] + gl: Some(painter.gl().clone()), + + #[cfg(all(feature = "wgpu", not(feature = "glow")))] + wgpu_render_state: painter.render_state(), + #[cfg(all(feature = "wgpu", feature = "glow"))] + wgpu_render_state: None, + }; + + let needs_repaint: std::sync::Arc = Default::default(); + { + let needs_repaint = needs_repaint.clone(); + egui_ctx.set_request_repaint_callback(move |info| { + needs_repaint.repaint_after(info.after.as_secs_f64()); + }); + } + + let mut runner = Self { + frame, + egui_ctx, + painter, + input: Default::default(), + app, + needs_repaint, + last_save_time: now_sec(), + screen_reader: Default::default(), + text_cursor_pos: None, + mutable_text_under_cursor: false, + textures_delta: Default::default(), + }; + + runner.input.raw.max_texture_side = Some(runner.painter.max_texture_side()); + + Ok(runner) + } + + pub fn egui_ctx(&self) -> &egui::Context { + &self.egui_ctx + } + + /// Get mutable access to the concrete [`App`] we enclose. + /// + /// This will panic if your app does not implement [`App::as_any_mut`]. + pub fn app_mut(&mut self) -> &mut ConcreteApp { + self.app + .as_any_mut() + .expect("Your app must implement `as_any_mut`, but it doesn't") + .downcast_mut::() + .expect("app_mut got the wrong type of App") + } + + pub fn auto_save_if_needed(&mut self) { + let time_since_last_save = now_sec() - self.last_save_time; + if time_since_last_save > self.app.auto_save_interval().as_secs_f64() { + self.save(); + } + } + + pub fn save(&mut self) { + if self.app.persist_egui_memory() { + super::save_memory(&self.egui_ctx); + } + if let Some(storage) = self.frame.storage_mut() { + self.app.save(storage); + } + self.last_save_time = now_sec(); + } + + pub fn canvas_id(&self) -> &str { + self.painter.canvas_id() + } + + pub fn warm_up(&mut self) { + if self.app.warm_up_enabled() { + let saved_memory: egui::Memory = self.egui_ctx.memory(|m| m.clone()); + self.egui_ctx + .memory_mut(|m| m.set_everything_is_visible(true)); + self.logic(); + self.egui_ctx.memory_mut(|m| *m = saved_memory); // We don't want to remember that windows were huge. + self.egui_ctx.clear_animations(); + } + } + + pub fn destroy(mut self) { + log::debug!("Destroying AppRunner"); + self.painter.destroy(); + } + + /// Returns how long to wait until the next repaint. + /// + /// Call [`Self::paint`] later to paint + pub fn logic(&mut self) -> (std::time::Duration, Vec) { + let frame_start = now_sec(); + + super::resize_canvas_to_screen_size(self.canvas_id(), self.app.max_size_points()); + let canvas_size = super::canvas_size_in_points(self.canvas_id()); + let raw_input = self.input.new_frame(canvas_size); + + let full_output = self.egui_ctx.run(raw_input, |egui_ctx| { + self.app.update(egui_ctx, &mut self.frame); + }); + let egui::FullOutput { + platform_output, + repaint_after, + textures_delta, + shapes, + } = full_output; + + self.handle_platform_output(platform_output); + self.textures_delta.append(textures_delta); + let clipped_primitives = self.egui_ctx.tessellate(shapes); + + { + let app_output = self.frame.take_app_output(); + let epi::backend::AppOutput {} = app_output; + } + + self.frame.info.cpu_usage = Some((now_sec() - frame_start) as f32); + + (repaint_after, clipped_primitives) + } + + /// Paint the results of the last call to [`Self::logic`]. + pub fn paint(&mut self, clipped_primitives: &[egui::ClippedPrimitive]) -> Result<(), JsValue> { + let textures_delta = std::mem::take(&mut self.textures_delta); + + self.painter.paint_and_update_textures( + self.app.clear_color(&self.egui_ctx.style().visuals), + clipped_primitives, + self.egui_ctx.pixels_per_point(), + &textures_delta, + )?; + + Ok(()) + } + + fn handle_platform_output(&mut self, platform_output: egui::PlatformOutput) { + if self.egui_ctx.options(|o| o.screen_reader) { + self.screen_reader + .speak(&platform_output.events_description()); + } + + let egui::PlatformOutput { + cursor_icon, + open_url, + copied_text, + events: _, // already handled + mutable_text_under_cursor, + text_cursor_pos, + #[cfg(feature = "accesskit")] + accesskit_update: _, // not currently implemented + } = platform_output; + + super::set_cursor_icon(cursor_icon); + if let Some(open) = open_url { + super::open_url(&open.url, open.new_tab); + } + + #[cfg(web_sys_unstable_apis)] + if !copied_text.is_empty() { + super::set_clipboard_text(&copied_text); + } + + #[cfg(not(web_sys_unstable_apis))] + let _ = copied_text; + + self.mutable_text_under_cursor = mutable_text_under_cursor; + + if self.text_cursor_pos != text_cursor_pos { + super::text_agent::move_text_cursor(text_cursor_pos, self.canvas_id()); + self.text_cursor_pos = text_cursor_pos; + } + } +} + +// ---------------------------------------------------------------------------- + +#[derive(Default)] +struct LocalStorage {} + +impl epi::Storage for LocalStorage { + fn get_string(&self, key: &str) -> Option { + super::local_storage_get(key) + } + + fn set_string(&mut self, key: &str, value: String) { + super::local_storage_set(key, &value); + } + + fn flush(&mut self) {} +} diff --git a/crates/eframe/src/web/backend.rs b/crates/eframe/src/web/backend.rs index 9fce201a..dde1d897 100644 --- a/crates/eframe/src/web/backend.rs +++ b/crates/eframe/src/web/backend.rs @@ -1,10 +1,10 @@ -use std::{cell::RefCell, rc::Rc}; +use std::collections::BTreeMap; -use egui::{mutex::Mutex, TexturesDelta}; +use egui::mutex::Mutex; -use crate::{epi, App}; +use crate::epi; -use super::{web_painter::WebPainter, *}; +use super::percent_decode; // ---------------------------------------------------------------------------- @@ -24,8 +24,8 @@ impl WebInput { pub fn new_frame(&mut self, canvas_size: egui::Vec2) -> egui::RawInput { egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(Default::default(), canvas_size)), - pixels_per_point: Some(native_pixels_per_point()), // We ALWAYS use the native pixels-per-point - time: Some(now_sec()), + pixels_per_point: Some(super::native_pixels_per_point()), // We ALWAYS use the native pixels-per-point + time: Some(super::now_sec()), ..self.raw.take() } } @@ -66,7 +66,7 @@ impl NeedRepaint { pub fn repaint_after(&self, num_seconds: f64) { let mut repaint_time = self.0.lock(); - *repaint_time = repaint_time.min(now_sec() + num_seconds); + *repaint_time = repaint_time.min(super::now_sec() + num_seconds); } pub fn repaint_asap(&self) { @@ -94,11 +94,11 @@ impl IsDestroyed { // ---------------------------------------------------------------------------- -fn user_agent() -> Option { +pub fn user_agent() -> Option { web_sys::window()?.navigator().user_agent().ok() } -fn web_location() -> epi::Location { +pub fn web_location() -> epi::Location { let location = web_sys::window().unwrap().location(); let hash = percent_decode(&location.hash().unwrap_or_default()); @@ -166,579 +166,3 @@ fn test_parse_query() { BTreeMap::from_iter([("foo", ""), ("baz", "")]) ); } - -// ---------------------------------------------------------------------------- - -#[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, - painter: ActiveWebPainter, - pub(crate) input: WebInput, - app: Box, - pub(crate) needs_repaint: std::sync::Arc, - pub(crate) is_destroyed: std::sync::Arc, - last_save_time: f64, - screen_reader: super::screen_reader::ScreenReader, - pub(crate) text_cursor_pos: Option, - pub(crate) mutable_text_under_cursor: bool, - textures_delta: TexturesDelta, -} - -impl Drop for AppRunner { - fn drop(&mut self) { - log::debug!("AppRunner has fully dropped"); - } -} - -impl AppRunner { - /// # Errors - /// Failure to initialize WebGL renderer. - pub async fn new( - canvas_id: &str, - web_options: crate::WebOptions, - app_creator: epi::AppCreator, - ) -> Result { - let painter = ActiveWebPainter::new(canvas_id, &web_options).await?; - - let system_theme = if web_options.follow_system_theme { - super::system_theme() - } else { - None - }; - - let info = epi::IntegrationInfo { - web_info: epi::WebInfo { - user_agent: user_agent().unwrap_or_default(), - location: web_location(), - }, - system_theme, - cpu_usage: None, - native_pixels_per_point: Some(native_pixels_per_point()), - }; - let storage = LocalStorage::default(); - - let egui_ctx = egui::Context::default(); - egui_ctx.set_os(egui::os::OperatingSystem::from_user_agent( - &user_agent().unwrap_or_default(), - )); - load_memory(&egui_ctx); - - let theme = system_theme.unwrap_or(web_options.default_theme); - egui_ctx.set_visuals(theme.egui_visuals()); - - let app = app_creator(&epi::CreationContext { - egui_ctx: egui_ctx.clone(), - integration_info: info.clone(), - storage: Some(&storage), - - #[cfg(feature = "glow")] - gl: Some(painter.gl().clone()), - - #[cfg(all(feature = "wgpu", not(feature = "glow")))] - wgpu_render_state: painter.render_state(), - #[cfg(all(feature = "wgpu", feature = "glow"))] - wgpu_render_state: None, - }); - - let frame = epi::Frame { - info, - output: Default::default(), - storage: Some(Box::new(storage)), - - #[cfg(feature = "glow")] - gl: Some(painter.gl().clone()), - - #[cfg(all(feature = "wgpu", not(feature = "glow")))] - wgpu_render_state: painter.render_state(), - #[cfg(all(feature = "wgpu", feature = "glow"))] - wgpu_render_state: None, - }; - - let needs_repaint: std::sync::Arc = Default::default(); - { - let needs_repaint = needs_repaint.clone(); - egui_ctx.set_request_repaint_callback(move |info| { - needs_repaint.repaint_after(info.after.as_secs_f64()); - }); - } - - let mut runner = Self { - frame, - egui_ctx, - painter, - input: Default::default(), - app, - needs_repaint, - is_destroyed: Default::default(), - last_save_time: now_sec(), - screen_reader: Default::default(), - text_cursor_pos: None, - mutable_text_under_cursor: false, - textures_delta: Default::default(), - }; - - runner.input.raw.max_texture_side = Some(runner.painter.max_texture_side()); - - Ok(runner) - } - - pub fn egui_ctx(&self) -> &egui::Context { - &self.egui_ctx - } - - /// Get mutable access to the concrete [`App`] we enclose. - /// - /// This will panic if your app does not implement [`App::as_any_mut`]. - pub fn app_mut(&mut self) -> &mut ConcreteApp { - self.app - .as_any_mut() - .expect("Your app must implement `as_any_mut`, but it doesn't") - .downcast_mut::() - .unwrap() - } - - pub fn auto_save_if_needed(&mut self) { - let time_since_last_save = now_sec() - self.last_save_time; - if time_since_last_save > self.app.auto_save_interval().as_secs_f64() { - self.save(); - } - } - - pub fn save(&mut self) { - if self.app.persist_egui_memory() { - save_memory(&self.egui_ctx); - } - if let Some(storage) = self.frame.storage_mut() { - self.app.save(storage); - } - self.last_save_time = now_sec(); - } - - pub fn canvas_id(&self) -> &str { - self.painter.canvas_id() - } - - pub fn warm_up(&mut self) -> Result<(), JsValue> { - if self.app.warm_up_enabled() { - let saved_memory: egui::Memory = self.egui_ctx.memory(|m| m.clone()); - self.egui_ctx - .memory_mut(|m| m.set_everything_is_visible(true)); - self.logic()?; - self.egui_ctx.memory_mut(|m| *m = saved_memory); // We don't want to remember that windows were huge. - self.egui_ctx.clear_animations(); - } - Ok(()) - } - - fn destroy(&mut self) { - if self.is_destroyed.fetch() { - log::warn!("App was destroyed already"); - } else { - log::debug!("Destroying"); - self.painter.destroy(); - self.is_destroyed.set_true(); - } - } - - /// Returns how long to wait until the next repaint. - /// - /// Call [`Self::paint`] later to paint - pub fn logic(&mut self) -> Result<(std::time::Duration, Vec), JsValue> { - let frame_start = now_sec(); - - resize_canvas_to_screen_size(self.canvas_id(), self.app.max_size_points()); - let canvas_size = canvas_size_in_points(self.canvas_id()); - let raw_input = self.input.new_frame(canvas_size); - - let full_output = self.egui_ctx.run(raw_input, |egui_ctx| { - self.app.update(egui_ctx, &mut self.frame); - }); - let egui::FullOutput { - platform_output, - repaint_after, - textures_delta, - shapes, - } = full_output; - - self.handle_platform_output(platform_output); - self.textures_delta.append(textures_delta); - let clipped_primitives = self.egui_ctx.tessellate(shapes); - - { - let app_output = self.frame.take_app_output(); - let epi::backend::AppOutput {} = app_output; - } - - self.frame.info.cpu_usage = Some((now_sec() - frame_start) as f32); - Ok((repaint_after, clipped_primitives)) - } - - /// Paint the results of the last call to [`Self::logic`]. - pub fn paint(&mut self, clipped_primitives: &[egui::ClippedPrimitive]) -> Result<(), JsValue> { - let textures_delta = std::mem::take(&mut self.textures_delta); - - self.painter.paint_and_update_textures( - self.app.clear_color(&self.egui_ctx.style().visuals), - clipped_primitives, - self.egui_ctx.pixels_per_point(), - &textures_delta, - )?; - - Ok(()) - } - - fn handle_platform_output(&mut self, platform_output: egui::PlatformOutput) { - if self.egui_ctx.options(|o| o.screen_reader) { - self.screen_reader - .speak(&platform_output.events_description()); - } - - let egui::PlatformOutput { - cursor_icon, - open_url, - copied_text, - events: _, // already handled - mutable_text_under_cursor, - text_cursor_pos, - #[cfg(feature = "accesskit")] - accesskit_update: _, // not currently implemented - } = platform_output; - - set_cursor_icon(cursor_icon); - if let Some(open) = open_url { - super::open_url(&open.url, open.new_tab); - } - - #[cfg(web_sys_unstable_apis)] - if !copied_text.is_empty() { - set_clipboard_text(&copied_text); - } - - #[cfg(not(web_sys_unstable_apis))] - let _ = copied_text; - - self.mutable_text_under_cursor = mutable_text_under_cursor; - - if self.text_cursor_pos != text_cursor_pos { - text_agent::move_text_cursor(text_cursor_pos, self.canvas_id()); - self.text_cursor_pos = text_cursor_pos; - } - } -} - -// ---------------------------------------------------------------------------- - -/// 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. - self.unsubscribe_from_all_events(); - } - } - - 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:?}"); - } - } - } - } - - /// 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) { - self.unsubscribe_from_all_events(); - 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.try_borrow_mut().ok()?; - 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> { - 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(()) - } -} - -// ---------------------------------------------------------------------------- - -pub struct TargetEvent { - target: EventTarget, - event_name: String, - closure: Closure, -} - -pub struct IntervalHandle { - pub handle: i32, - pub closure: Closure, -} - -pub enum EventToUnsubscribe { - TargetEvent(TargetEvent), - - 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(()) - } - } - } -} - -// ---------------------------------------------------------------------------- - -/// 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. -pub async fn start_web( - canvas_id: &str, - web_options: crate::WebOptions, - app_creator: epi::AppCreator, -) -> Result { - #[cfg(not(web_sys_unstable_apis))] - log::warn!( - "eframe compiled without RUSTFLAGS='--cfg=web_sys_unstable_apis'. Copying text won't work." - ); - let follow_system_theme = web_options.follow_system_theme; - - let mut runner = AppRunner::new(canvas_id, web_options, app_creator).await?; - runner.warm_up()?; - let runner_ref = AppRunnerRef::new(runner); - - // 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)?; - } - - // Instal panic handler: - { - // Disable all event handlers on panic - let previous_hook = std::panic::take_hook(); - let panic_handler = runner_ref.panic_handler.clone(); - - std::panic::set_hook(Box::new(move |panic_info| { - log::info!("eframe detected a panic"); - panic_handler.lock().on_panic(panic_info); - - // Propagate panic info to the previously registered panic hook - previous_hook(panic_info); - })); - } - - Ok(runner_ref) -} - -// ---------------------------------------------------------------------------- - -#[derive(Default)] -struct LocalStorage {} - -impl epi::Storage for LocalStorage { - fn get_string(&self, key: &str) -> Option { - local_storage_get(key) - } - - fn set_string(&mut self, key: &str, value: String) { - local_storage_set(key, &value); - } - - fn flush(&mut self) {} -} diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index 5294ff26..f44adb69 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -1,30 +1,11 @@ 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) -> 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.auto_save_if_needed(); - } - Ok(()) - } - - 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)); - window.request_animation_frame(closure.as_ref().unchecked_ref())?; - closure.forget(); // We must forget it, or else the callback is canceled on drop - Ok(()) - } - +fn paint_and_schedule(runner_ref: &WebRunner) -> Result<(), JsValue> { // Only paint and schedule if there has been no panic if let Some(mut runner_lock) = runner_ref.try_lock() { paint_if_needed(&mut runner_lock)?; @@ -35,7 +16,30 @@ pub fn paint_and_schedule(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { Ok(()) } -pub fn install_document_events(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.auto_save_if_needed(); + } + Ok(()) +} + +pub fn request_animation_frame(runner_ref: WebRunner) -> Result<(), JsValue> { + let window = web_sys::window().unwrap(); + 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(()) +} + +// ------------------------------------------------------------------------ + +pub fn install_document_events(runner_ref: &WebRunner) -> Result<(), JsValue> { let document = web_sys::window().unwrap().document().unwrap(); { @@ -185,7 +189,7 @@ pub fn install_document_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> Ok(()) } -pub fn install_window_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { +pub fn install_window_events(runner_ref: &WebRunner) -> Result<(), JsValue> { let window = web_sys::window().unwrap(); // Save-on-close @@ -207,7 +211,7 @@ pub fn install_window_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { Ok(()) } -pub fn install_color_scheme_change_event(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { +pub fn install_color_scheme_change_event(runner_ref: &WebRunner) -> Result<(), JsValue> { let window = web_sys::window().unwrap(); if let Some(media_query_list) = prefers_color_scheme_dark(&window)? { @@ -226,7 +230,7 @@ pub fn install_color_scheme_change_event(runner_ref: &AppRunnerRef) -> Result<() Ok(()) } -pub fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { +pub fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValue> { let canvas = canvas_element(runner_ref.try_lock().unwrap().canvas_id()).unwrap(); { diff --git a/crates/eframe/src/web/mod.rs b/crates/eframe/src/web/mod.rs index 12b99144..5cea5d61 100644 --- a/crates/eframe/src/web/mod.rs +++ b/crates/eframe/src/web/mod.rs @@ -2,15 +2,21 @@ #![allow(clippy::missing_errors_doc)] // So many `-> Result<_, JsValue>` +mod app_runner; pub mod backend; mod events; mod input; +mod panic_handler; pub mod screen_reader; pub mod storage; mod text_agent; mod web_logger; +mod web_runner; +pub(crate) use app_runner::AppRunner; +pub use panic_handler::{PanicHandler, PanicSummary}; pub use web_logger::WebLogger; +pub use web_runner::WebRunner; #[cfg(not(any(feature = "glow", feature = "wgpu")))] compile_error!("You must enable either the 'glow' or 'wgpu' feature"); @@ -31,12 +37,9 @@ pub use backend::*; pub use events::*; pub use storage::*; -use std::collections::BTreeMap; -use std::sync::Arc; - use egui::Vec2; use wasm_bindgen::prelude::*; -use web_sys::{EventTarget, MediaQueryList}; +use web_sys::MediaQueryList; use input::*; diff --git a/crates/eframe/src/web/panic_handler.rs b/crates/eframe/src/web/panic_handler.rs new file mode 100644 index 00000000..a22bca9d --- /dev/null +++ b/crates/eframe/src/web/panic_handler.rs @@ -0,0 +1,95 @@ +use std::sync::Arc; + +use egui::mutex::Mutex; +use wasm_bindgen::prelude::*; + +/// Detects panics, logs them using `console.error`, and stores the panics message and callstack. +/// +/// This lets you query `PanicHandler` for the panic message (if any) so you can show it in the HTML. +/// +/// Chep to clone (ref-counted). +#[derive(Clone)] +pub struct PanicHandler(Arc>); + +impl PanicHandler { + /// Install a panic hook. + pub fn install() -> Self { + let handler = Self(Arc::new(Mutex::new(Default::default()))); + + let handler_clone = handler.clone(); + let previous_hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |panic_info| { + let summary = PanicSummary::new(panic_info); + + // Log it using console.error + error(format!( + "{}\n\nStack:\n\n{}", + summary.message(), + summary.callstack() + )); + + // Remember the summary: + handler_clone.0.lock().summary = Some(summary); + + // Propagate panic info to the previously registered panic hook + previous_hook(panic_info); + })); + + handler + } + + /// Has there been a panic? + pub fn has_panicked(&self) -> bool { + self.0.lock().summary.is_some() + } + + /// What was the panic message and callstack? + pub fn panic_summary(&self) -> Option { + self.0.lock().summary.clone() + } +} + +#[derive(Clone, Default)] +struct PanicHandlerInner { + summary: Option, +} + +/// Contains a summary about a panics. +/// +/// This is basically a human-readable version of [`std::panic::PanicInfo`] +/// with an added callstack. +#[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() + } +} + +#[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; +} diff --git a/crates/eframe/src/web/text_agent.rs b/crates/eframe/src/web/text_agent.rs index 66e08ea8..579f98c7 100644 --- a/crates/eframe/src/web/text_agent.rs +++ b/crates/eframe/src/web/text_agent.rs @@ -5,7 +5,7 @@ use std::{cell::Cell, rc::Rc}; use wasm_bindgen::prelude::*; -use super::{canvas_element, AppRunner, AppRunnerRef}; +use super::{canvas_element, AppRunner, WebRunner}; static AGENT_ID: &str = "egui_text_agent"; @@ -21,7 +21,7 @@ pub fn text_agent() -> web_sys::HtmlInputElement { } /// Text event handler, -pub fn install_text_agent(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { +pub fn install_text_agent(runner_ref: &WebRunner) -> Result<(), JsValue> { let window = web_sys::window().unwrap(); let document = window.document().unwrap(); let body = document.body().expect("document should have a body"); diff --git a/crates/eframe/src/web/web_runner.rs b/crates/eframe/src/web/web_runner.rs new file mode 100644 index 00000000..2c039544 --- /dev/null +++ b/crates/eframe/src/web/web_runner.rs @@ -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>>, + + /// 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 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 { + 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> { + 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 { + 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(()) + } + } + } +} diff --git a/crates/egui_demo_app/Cargo.toml b/crates/egui_demo_app/Cargo.toml index 0c011e7b..9c92632f 100644 --- a/crates/egui_demo_app/Cargo.toml +++ b/crates/egui_demo_app/Cargo.toml @@ -62,7 +62,6 @@ env_logger = "0.10" # web: [target.'cfg(target_arch = "wasm32")'.dependencies] -console_error_panic_hook = "0.1.6" wasm-bindgen = "=0.2.84" wasm-bindgen-futures = "0.4" web-sys = "0.3" diff --git a/crates/egui_demo_app/src/web.rs b/crates/egui_demo_app/src/web.rs index 7ffe99d3..231aaf30 100644 --- a/crates/egui_demo_app/src/web.rs +++ b/crates/egui_demo_app/src/web.rs @@ -1,38 +1,41 @@ use eframe::{ wasm_bindgen::{self, prelude::*}, - web::AppRunnerRef, + web::WebRunner, }; use crate::WrapApp; +/// Our handle to the web app from JavaScript. +#[derive(Clone)] #[wasm_bindgen] pub struct WebHandle { - runner: AppRunnerRef, + runner: WebRunner, } #[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. + /// Installs a panic hook, then returns. + #[allow(clippy::new_without_default)] #[wasm_bindgen(constructor)] - pub async fn new(canvas_id: &str) -> Result { - // Redirect tracing to console.log and friends: + pub fn new() -> Self { + // Redirect [`log`] message 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(); + Self { + runner: WebRunner::new(), + } + } - 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 }) + /// Call this once from JavaScript to start your app. + #[wasm_bindgen] + pub async fn start(&self, canvas_id: &str) -> Result<(), wasm_bindgen::JsValue> { + self.runner + .start( + canvas_id, + eframe::WebOptions::default(), + Box::new(|cc| Box::new(WrapApp::new(cc))), + ) + .await } #[wasm_bindgen] @@ -40,9 +43,18 @@ impl WebHandle { self.runner.destroy(); } + /// Example on how to call into your app from JavaScript. + #[wasm_bindgen] + pub fn example(&self) { + if let Some(_app) = self.runner.app_mut::() { + // _app.example(); + } + } + + /// The JavaScript can check whether or not your app has crashed: #[wasm_bindgen] pub fn has_panicked(&self) -> bool { - self.runner.panic_summary().is_some() + self.runner.has_panicked() } #[wasm_bindgen] diff --git a/docs/egui_demo_app.js b/docs/egui_demo_app.js index 5077f64f..46e47459 100644 --- a/docs/egui_demo_app.js +++ b/docs/egui_demo_app.js @@ -280,7 +280,7 @@ function handleError(f, args) { wasm.__wbindgen_exn_store(addHeapObject(e)); } } -function __wbg_adapter_584(arg0, arg1, arg2, arg3) { +function __wbg_adapter_580(arg0, arg1, arg2, arg3) { wasm.wasm_bindgen__convert__closures__invoke2_mut__h125af29ab38d9781(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3)); } @@ -308,16 +308,21 @@ class WebHandle { wasm.__wbg_webhandle_free(ptr); } /** - * 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. - * @param {string} canvas_id + * Installs a panic hook, then returns. */ - constructor(canvas_id) { + constructor() { + const ret = wasm.webhandle_new(); + return WebHandle.__wrap(ret); + } + /** + * Call this once from JavaScript to start your app. + * @param {string} canvas_id + * @returns {Promise} + */ + start(canvas_id) { const ptr0 = passStringToWasm0(canvas_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); const len0 = WASM_VECTOR_LEN; - const ret = wasm.webhandle_new(ptr0, len0); + const ret = wasm.webhandle_start(this.ptr, ptr0, len0); return takeObject(ret); } /** @@ -326,6 +331,13 @@ class WebHandle { wasm.webhandle_destroy(this.ptr); } /** + * Example on how to call into your app from JavaScript. + */ + example() { + wasm.webhandle_example(this.ptr); + } + /** + * The JavaScript can check wether or not your app has crashed: * @returns {boolean} */ has_panicked() { @@ -407,10 +419,6 @@ async function load(module, imports) { function getImports() { const imports = {}; imports.wbg = {}; - imports.wbg.__wbg_webhandle_new = function(arg0) { - const ret = WebHandle.__wrap(arg0); - return addHeapObject(ret); - }; imports.wbg.__wbindgen_object_drop_ref = function(arg0) { takeObject(arg0); }; @@ -427,24 +435,6 @@ function getImports() { const ret = getStringFromWasm0(arg0, arg1); return addHeapObject(ret); }; - imports.wbg.__wbg_new_abda76e883ba8a5f = function() { - const ret = new Error(); - return addHeapObject(ret); - }; - imports.wbg.__wbg_stack_658279fe44541cf6 = function(arg0, arg1) { - const ret = getObject(arg1).stack; - const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len0 = WASM_VECTOR_LEN; - getInt32Memory0()[arg0 / 4 + 1] = len0; - getInt32Memory0()[arg0 / 4 + 0] = ptr0; - }; - imports.wbg.__wbg_error_f851667af71bcfc6 = function(arg0, arg1) { - try { - console.error(getStringFromWasm0(arg0, arg1)); - } finally { - wasm.__wbindgen_free(arg0, arg1); - } - }; imports.wbg.__wbindgen_string_get = function(arg0, arg1) { const obj = getObject(arg1); const ret = typeof(obj) === 'string' ? obj : undefined; @@ -471,6 +461,13 @@ function getImports() { imports.wbg.__wbg_warn_8b4e19d4032139f0 = function(arg0, arg1) { console.warn(getStringFromWasm0(arg0, arg1)); }; + imports.wbg.__wbg_error_e62b64b85c2bc545 = function(arg0, arg1) { + try { + console.error(getStringFromWasm0(arg0, arg1)); + } finally { + wasm.__wbindgen_free(arg0, arg1); + } + }; imports.wbg.__wbg_new_40620131643ca1cf = function() { const ret = new Error(); return addHeapObject(ret); @@ -1573,7 +1570,7 @@ function getImports() { const a = state0.a; state0.a = 0; try { - return __wbg_adapter_584(a, state0.b, arg0, arg1); + return __wbg_adapter_580(a, state0.b, arg0, arg1); } finally { state0.a = a; } @@ -1657,28 +1654,28 @@ function getImports() { const ret = wasm.memory; return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper2873 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 981, __wbg_adapter_28); + imports.wbg.__wbindgen_closure_wrapper2865 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 971, __wbg_adapter_28); return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper2874 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 981, __wbg_adapter_31); + imports.wbg.__wbindgen_closure_wrapper2866 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 971, __wbg_adapter_31); return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper2877 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 981, __wbg_adapter_34); + imports.wbg.__wbindgen_closure_wrapper2869 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 971, __wbg_adapter_34); return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper3272 = function(arg0, arg1, arg2) { - const ret = makeClosure(arg0, arg1, 1143, __wbg_adapter_37); + imports.wbg.__wbindgen_closure_wrapper3248 = function(arg0, arg1, arg2) { + const ret = makeClosure(arg0, arg1, 1129, __wbg_adapter_37); return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper3274 = function(arg0, arg1, arg2) { - const ret = makeClosure(arg0, arg1, 1143, __wbg_adapter_37); + imports.wbg.__wbindgen_closure_wrapper3250 = function(arg0, arg1, arg2) { + const ret = makeClosure(arg0, arg1, 1129, __wbg_adapter_37); return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper3317 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 1166, __wbg_adapter_42); + imports.wbg.__wbindgen_closure_wrapper3293 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 1152, __wbg_adapter_42); return addHeapObject(ret); }; diff --git a/docs/egui_demo_app_bg.wasm b/docs/egui_demo_app_bg.wasm index 95da7084..37d69007 100644 Binary files a/docs/egui_demo_app_bg.wasm and b/docs/egui_demo_app_bg.wasm differ diff --git a/docs/index.html b/docs/index.html index 8d6e7c81..afe4cc64 100644 --- a/docs/index.html +++ b/docs/index.html @@ -91,7 +91,6 @@ transform: rotate(360deg); } } - @@ -133,17 +132,7 @@ function on_wasm_loaded() { console.debug("Wasm loaded. Starting app…"); - // This call installs a bunch of callbacks and then returns: - let handle = new wasm_bindgen.WebHandle("the_canvas_id"); - handle.then(on_app_started).catch(on_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 = ''; + let handle = new wasm_bindgen.WebHandle(); function check_for_panic() { if (handle.has_panicked()) { @@ -159,6 +148,9 @@

The egui app has crashed.

+

+ ${handle.panic_message()} +

See the console for details.

@@ -172,6 +164,16 @@ } check_for_panic(); + + handle.start("the_canvas_id").then(on_app_started).catch(on_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 on_error(error) { diff --git a/docs/multiple_apps.html b/docs/multiple_apps.html index 2e38f259..14d0d5cf 100644 --- a/docs/multiple_apps.html +++ b/docs/multiple_apps.html @@ -100,7 +100,6 @@ transform: rotate(360deg); } } - @@ -158,11 +157,11 @@ // This call installs a bunch of callbacks and then returns: - const handle_one = new wasm_bindgen.WebHandle("canvas_id_one"); - const handle_two = new wasm_bindgen.WebHandle("canvas_id_two"); + const handle_one = new wasm_bindgen.WebHandle(); + const handle_two = new wasm_bindgen.WebHandle(); - Promise.all([handle_one, handle_two]).then((handles) => { - on_apps_started(handles[0], handles[1]) + Promise.all([handle_one.start("canvas_id_one"), handle_two.start("canvas_id_two")]).then((handles) => { + on_apps_started(handle_one, handle_two) }).catch(on_error); }