eframe web: detect and report panics during startup (#2992)

* Detect panics during initialization and show them to the user

* PanicHandler now also logs the panics

* Add example of how to call into your app from JS

* Refactor: break out AppRunner and AppRunnerRef to own files

* Hide AppRunner

* Simplify user code

* AppRunnerRef -> WebRunner

* Better docs

* Don't paint until first animation frame

* Update multiple_apps.html

* Update web demo

* Cleanup and fixes

* left-align panic message in html
This commit is contained in:
Emil Ernerfeldt 2023-05-16 22:22:09 +02:00 committed by GitHub
parent ff8e4826b3
commit ea71b7f20b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 791 additions and 716 deletions

11
Cargo.lock generated
View File

@ -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",

View File

@ -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<AppRunnerRef, eframe::wasm_bindgen::JsValue> {
//! 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::<MyEguiApp>() {
//! 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<String> {
//! self.runner.panic_summary().map(|s| s.message())
//! }
//!
//! #[wasm_bindgen]
//! pub fn panic_callstack(&self) -> Option<String> {
//! 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

View File

@ -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<dyn epi::App>,
pub(crate) needs_repaint: std::sync::Arc<NeedRepaint>,
last_save_time: f64,
screen_reader: super::screen_reader::ScreenReader,
pub(crate) text_cursor_pos: Option<egui::Pos2>,
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<Self, String> {
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<NeedRepaint> = 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<ConcreteApp: 'static + App>(&mut self) -> &mut ConcreteApp {
self.app
.as_any_mut()
.expect("Your app must implement `as_any_mut`, but it doesn't")
.downcast_mut::<ConcreteApp>()
.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<egui::ClippedPrimitive>) {
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<String> {
super::local_storage_get(key)
}
fn set_string(&mut self, key: &str, value: String) {
super::local_storage_set(key, &value);
}
fn flush(&mut self) {}
}

View File

@ -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<String> {
pub fn user_agent() -> Option<String> {
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<PanicSummary>,
}
impl PanicHandler {
pub fn has_panicked(&self) -> bool {
self.summary.is_some()
}
pub fn panic_summary(&self) -> Option<PanicSummary> {
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<dyn epi::App>,
pub(crate) needs_repaint: std::sync::Arc<NeedRepaint>,
pub(crate) is_destroyed: std::sync::Arc<IsDestroyed>,
last_save_time: f64,
screen_reader: super::screen_reader::ScreenReader,
pub(crate) text_cursor_pos: Option<egui::Pos2>,
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<Self, String> {
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<NeedRepaint> = 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<ConcreteApp: 'static + App>(&mut self) -> &mut ConcreteApp {
self.app
.as_any_mut()
.expect("Your app must implement `as_any_mut`, but it doesn't")
.downcast_mut::<ConcreteApp>()
.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<egui::ClippedPrimitive>), 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<RefCell<AppRunner>>,
/// Have we ever panicked?
panic_handler: Arc<Mutex<PanicHandler>>,
/// 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<RefCell<Vec<EventToUnsubscribe>>>,
}
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<PanicSummary> {
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<std::cell::RefMut<'_, AppRunner>> {
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<E: wasm_bindgen::JsCast>(
&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::<E>();
closure(event, &mut runner_lock);
}
}) as Box<dyn FnMut(web_sys::Event)>);
// Add the event listener to the target
target.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?;
let handle = TargetEvent {
target: target.clone(),
event_name: event_name.to_owned(),
closure,
};
// Remember it so we unsubscribe on panic.
// Otherwise we get calls into `self.runner` after it has been poisoned by a panic.
self.events_to_unsubscribe
.borrow_mut()
.push(EventToUnsubscribe::TargetEvent(handle));
Ok(())
}
}
// ----------------------------------------------------------------------------
pub struct TargetEvent {
target: EventTarget,
event_name: String,
closure: Closure<dyn FnMut(web_sys::Event)>,
}
pub struct IntervalHandle {
pub handle: i32,
pub closure: Closure<dyn FnMut()>,
}
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<WebHandle, eframe::wasm_bindgen::JsValue> {
/// 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<AppRunnerRef, JsValue> {
#[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<String> {
local_storage_get(key)
}
fn set_string(&mut self, key: &str, value: String) {
local_storage_set(key, &value);
}
fn flush(&mut self) {}
}

View File

@ -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();
{

View File

@ -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::*;

View File

@ -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<Mutex<PanicHandlerInner>>);
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<PanicSummary> {
self.0.lock().summary.clone()
}
}
#[derive(Clone, Default)]
struct PanicHandlerInner {
summary: Option<PanicSummary>,
}
/// 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;
}

View File

@ -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");

View File

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

View File

@ -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"

View File

@ -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<WebHandle, wasm_bindgen::JsValue> {
// 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::<WrapApp>() {
// _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]

View File

@ -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<void>}
*/
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);
};

Binary file not shown.

View File

@ -91,7 +91,6 @@
transform: rotate(360deg);
}
}
</style>
</head>
@ -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 @@
<p>
The egui app has crashed.
</p>
<p style="font-size:10px" align="left">
${handle.panic_message()}
</p>
<p style="font-size:14px">
See the console for details.
</p>
@ -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) {

View File

@ -100,7 +100,6 @@
transform: rotate(360deg);
}
}
</style>
</head>
@ -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);
}