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:
parent
ff8e4826b3
commit
ea71b7f20b
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {}
|
||||
}
|
||||
|
|
@ -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) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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.
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue