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",
|
"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]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.9.3"
|
version = "0.9.3"
|
||||||
|
|
@ -1281,7 +1271,6 @@ version = "0.21.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"chrono",
|
"chrono",
|
||||||
"console_error_panic_hook",
|
|
||||||
"eframe",
|
"eframe",
|
||||||
"egui",
|
"egui",
|
||||||
"egui_demo_lib",
|
"egui_demo_lib",
|
||||||
|
|
|
||||||
|
|
@ -42,15 +42,74 @@
|
||||||
//!
|
//!
|
||||||
//! ## Usage, web:
|
//! ## Usage, web:
|
||||||
//! ``` no_run
|
//! ``` no_run
|
||||||
//! #[cfg(target_arch = "wasm32")]
|
//! # #[cfg(target_arch = "wasm32")]
|
||||||
//! use wasm_bindgen::prelude::*;
|
//! use wasm_bindgen::prelude::*;
|
||||||
//!
|
//!
|
||||||
//! /// Call this once from the HTML.
|
//! /// Your handle to the web app from JavaScript.
|
||||||
//! #[cfg(target_arch = "wasm32")]
|
//! # #[cfg(target_arch = "wasm32")]
|
||||||
|
//! #[derive(Clone)]
|
||||||
//! #[wasm_bindgen]
|
//! #[wasm_bindgen]
|
||||||
//! pub async fn start(canvas_id: &str) -> Result<AppRunnerRef, eframe::wasm_bindgen::JsValue> {
|
//! pub struct WebHandle {
|
||||||
//! let web_options = eframe::WebOptions::default();
|
//! runner: WebRunner,
|
||||||
//! eframe::start_web(canvas_id, web_options, Box::new(|cc| Box::new(MyEguiApp::new(cc)))).await
|
//! }
|
||||||
|
//!
|
||||||
|
//! # #[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;
|
pub mod web;
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
pub use web::start_web;
|
pub use web::WebRunner;
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// When compiling natively
|
// 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 {
|
pub fn new_frame(&mut self, canvas_size: egui::Vec2) -> egui::RawInput {
|
||||||
egui::RawInput {
|
egui::RawInput {
|
||||||
screen_rect: Some(egui::Rect::from_min_size(Default::default(), canvas_size)),
|
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
|
pixels_per_point: Some(super::native_pixels_per_point()), // We ALWAYS use the native pixels-per-point
|
||||||
time: Some(now_sec()),
|
time: Some(super::now_sec()),
|
||||||
..self.raw.take()
|
..self.raw.take()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -66,7 +66,7 @@ impl NeedRepaint {
|
||||||
|
|
||||||
pub fn repaint_after(&self, num_seconds: f64) {
|
pub fn repaint_after(&self, num_seconds: f64) {
|
||||||
let mut repaint_time = self.0.lock();
|
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) {
|
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()
|
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 location = web_sys::window().unwrap().location();
|
||||||
|
|
||||||
let hash = percent_decode(&location.hash().unwrap_or_default());
|
let hash = percent_decode(&location.hash().unwrap_or_default());
|
||||||
|
|
@ -166,579 +166,3 @@ fn test_parse_query() {
|
||||||
BTreeMap::from_iter([("foo", ""), ("baz", "")])
|
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::*;
|
use super::*;
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Calls `request_animation_frame` to schedule repaint.
|
/// Calls `request_animation_frame` to schedule repaint.
|
||||||
///
|
///
|
||||||
/// It will only paint if needed, but will always call `request_animation_frame` immediately.
|
/// 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_and_schedule(runner_ref: &WebRunner) -> 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(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only paint and schedule if there has been no panic
|
// Only paint and schedule if there has been no panic
|
||||||
if let Some(mut runner_lock) = runner_ref.try_lock() {
|
if let Some(mut runner_lock) = runner_ref.try_lock() {
|
||||||
paint_if_needed(&mut runner_lock)?;
|
paint_if_needed(&mut runner_lock)?;
|
||||||
|
|
@ -35,7 +16,30 @@ pub fn paint_and_schedule(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
|
||||||
Ok(())
|
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();
|
let document = web_sys::window().unwrap().document().unwrap();
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
@ -185,7 +189,7 @@ pub fn install_document_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue>
|
||||||
Ok(())
|
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();
|
let window = web_sys::window().unwrap();
|
||||||
|
|
||||||
// Save-on-close
|
// Save-on-close
|
||||||
|
|
@ -207,7 +211,7 @@ pub fn install_window_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
|
||||||
Ok(())
|
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();
|
let window = web_sys::window().unwrap();
|
||||||
|
|
||||||
if let Some(media_query_list) = prefers_color_scheme_dark(&window)? {
|
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(())
|
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();
|
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>`
|
#![allow(clippy::missing_errors_doc)] // So many `-> Result<_, JsValue>`
|
||||||
|
|
||||||
|
mod app_runner;
|
||||||
pub mod backend;
|
pub mod backend;
|
||||||
mod events;
|
mod events;
|
||||||
mod input;
|
mod input;
|
||||||
|
mod panic_handler;
|
||||||
pub mod screen_reader;
|
pub mod screen_reader;
|
||||||
pub mod storage;
|
pub mod storage;
|
||||||
mod text_agent;
|
mod text_agent;
|
||||||
mod web_logger;
|
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_logger::WebLogger;
|
||||||
|
pub use web_runner::WebRunner;
|
||||||
|
|
||||||
#[cfg(not(any(feature = "glow", feature = "wgpu")))]
|
#[cfg(not(any(feature = "glow", feature = "wgpu")))]
|
||||||
compile_error!("You must enable either the 'glow' or 'wgpu' feature");
|
compile_error!("You must enable either the 'glow' or 'wgpu' feature");
|
||||||
|
|
@ -31,12 +37,9 @@ pub use backend::*;
|
||||||
pub use events::*;
|
pub use events::*;
|
||||||
pub use storage::*;
|
pub use storage::*;
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use egui::Vec2;
|
use egui::Vec2;
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
use web_sys::{EventTarget, MediaQueryList};
|
use web_sys::MediaQueryList;
|
||||||
|
|
||||||
use input::*;
|
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 wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
use super::{canvas_element, AppRunner, AppRunnerRef};
|
use super::{canvas_element, AppRunner, WebRunner};
|
||||||
|
|
||||||
static AGENT_ID: &str = "egui_text_agent";
|
static AGENT_ID: &str = "egui_text_agent";
|
||||||
|
|
||||||
|
|
@ -21,7 +21,7 @@ pub fn text_agent() -> web_sys::HtmlInputElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Text event handler,
|
/// 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 window = web_sys::window().unwrap();
|
||||||
let document = window.document().unwrap();
|
let document = window.document().unwrap();
|
||||||
let body = document.body().expect("document should have a body");
|
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:
|
# web:
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
console_error_panic_hook = "0.1.6"
|
|
||||||
wasm-bindgen = "=0.2.84"
|
wasm-bindgen = "=0.2.84"
|
||||||
wasm-bindgen-futures = "0.4"
|
wasm-bindgen-futures = "0.4"
|
||||||
web-sys = "0.3"
|
web-sys = "0.3"
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,41 @@
|
||||||
use eframe::{
|
use eframe::{
|
||||||
wasm_bindgen::{self, prelude::*},
|
wasm_bindgen::{self, prelude::*},
|
||||||
web::AppRunnerRef,
|
web::WebRunner,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::WrapApp;
|
use crate::WrapApp;
|
||||||
|
|
||||||
|
/// Our handle to the web app from JavaScript.
|
||||||
|
#[derive(Clone)]
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub struct WebHandle {
|
pub struct WebHandle {
|
||||||
runner: AppRunnerRef,
|
runner: WebRunner,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
impl WebHandle {
|
impl WebHandle {
|
||||||
/// This is the entry-point for all the web-assembly.
|
/// Installs a panic hook, then returns.
|
||||||
///
|
#[allow(clippy::new_without_default)]
|
||||||
/// This is called once from the HTML.
|
|
||||||
/// It loads the app, installs some callbacks, then returns.
|
|
||||||
#[wasm_bindgen(constructor)]
|
#[wasm_bindgen(constructor)]
|
||||||
pub async fn new(canvas_id: &str) -> Result<WebHandle, wasm_bindgen::JsValue> {
|
pub fn new() -> Self {
|
||||||
// Redirect tracing to console.log and friends:
|
// Redirect [`log`] message to `console.log` and friends:
|
||||||
eframe::web::WebLogger::init(log::LevelFilter::Debug).ok();
|
eframe::web::WebLogger::init(log::LevelFilter::Debug).ok();
|
||||||
|
|
||||||
// Make sure panics are logged using `console.error`.
|
Self {
|
||||||
console_error_panic_hook::set_once();
|
runner: WebRunner::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let web_options = eframe::WebOptions::default();
|
/// Call this once from JavaScript to start your app.
|
||||||
let runner = eframe::start_web(
|
#[wasm_bindgen]
|
||||||
canvas_id,
|
pub async fn start(&self, canvas_id: &str) -> Result<(), wasm_bindgen::JsValue> {
|
||||||
web_options,
|
self.runner
|
||||||
Box::new(|cc| Box::new(WrapApp::new(cc))),
|
.start(
|
||||||
)
|
canvas_id,
|
||||||
.await?;
|
eframe::WebOptions::default(),
|
||||||
|
Box::new(|cc| Box::new(WrapApp::new(cc))),
|
||||||
Ok(WebHandle { runner })
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
|
|
@ -40,9 +43,18 @@ impl WebHandle {
|
||||||
self.runner.destroy();
|
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]
|
#[wasm_bindgen]
|
||||||
pub fn has_panicked(&self) -> bool {
|
pub fn has_panicked(&self) -> bool {
|
||||||
self.runner.panic_summary().is_some()
|
self.runner.has_panicked()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
|
|
|
||||||
|
|
@ -280,7 +280,7 @@ function handleError(f, args) {
|
||||||
wasm.__wbindgen_exn_store(addHeapObject(e));
|
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));
|
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);
|
wasm.__wbg_webhandle_free(ptr);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* This is the entry-point for all the web-assembly.
|
* Installs a panic hook, then returns.
|
||||||
*
|
|
||||||
* This is called once from the HTML.
|
|
||||||
* It loads the app, installs some callbacks, then returns.
|
|
||||||
* @param {string} canvas_id
|
|
||||||
*/
|
*/
|
||||||
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 ptr0 = passStringToWasm0(canvas_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
const len0 = WASM_VECTOR_LEN;
|
const len0 = WASM_VECTOR_LEN;
|
||||||
const ret = wasm.webhandle_new(ptr0, len0);
|
const ret = wasm.webhandle_start(this.ptr, ptr0, len0);
|
||||||
return takeObject(ret);
|
return takeObject(ret);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
|
|
@ -326,6 +331,13 @@ class WebHandle {
|
||||||
wasm.webhandle_destroy(this.ptr);
|
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}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
has_panicked() {
|
has_panicked() {
|
||||||
|
|
@ -407,10 +419,6 @@ async function load(module, imports) {
|
||||||
function getImports() {
|
function getImports() {
|
||||||
const imports = {};
|
const imports = {};
|
||||||
imports.wbg = {};
|
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) {
|
imports.wbg.__wbindgen_object_drop_ref = function(arg0) {
|
||||||
takeObject(arg0);
|
takeObject(arg0);
|
||||||
};
|
};
|
||||||
|
|
@ -427,24 +435,6 @@ function getImports() {
|
||||||
const ret = getStringFromWasm0(arg0, arg1);
|
const ret = getStringFromWasm0(arg0, arg1);
|
||||||
return addHeapObject(ret);
|
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) {
|
imports.wbg.__wbindgen_string_get = function(arg0, arg1) {
|
||||||
const obj = getObject(arg1);
|
const obj = getObject(arg1);
|
||||||
const ret = typeof(obj) === 'string' ? obj : undefined;
|
const ret = typeof(obj) === 'string' ? obj : undefined;
|
||||||
|
|
@ -471,6 +461,13 @@ function getImports() {
|
||||||
imports.wbg.__wbg_warn_8b4e19d4032139f0 = function(arg0, arg1) {
|
imports.wbg.__wbg_warn_8b4e19d4032139f0 = function(arg0, arg1) {
|
||||||
console.warn(getStringFromWasm0(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() {
|
imports.wbg.__wbg_new_40620131643ca1cf = function() {
|
||||||
const ret = new Error();
|
const ret = new Error();
|
||||||
return addHeapObject(ret);
|
return addHeapObject(ret);
|
||||||
|
|
@ -1573,7 +1570,7 @@ function getImports() {
|
||||||
const a = state0.a;
|
const a = state0.a;
|
||||||
state0.a = 0;
|
state0.a = 0;
|
||||||
try {
|
try {
|
||||||
return __wbg_adapter_584(a, state0.b, arg0, arg1);
|
return __wbg_adapter_580(a, state0.b, arg0, arg1);
|
||||||
} finally {
|
} finally {
|
||||||
state0.a = a;
|
state0.a = a;
|
||||||
}
|
}
|
||||||
|
|
@ -1657,28 +1654,28 @@ function getImports() {
|
||||||
const ret = wasm.memory;
|
const ret = wasm.memory;
|
||||||
return addHeapObject(ret);
|
return addHeapObject(ret);
|
||||||
};
|
};
|
||||||
imports.wbg.__wbindgen_closure_wrapper2873 = function(arg0, arg1, arg2) {
|
imports.wbg.__wbindgen_closure_wrapper2865 = function(arg0, arg1, arg2) {
|
||||||
const ret = makeMutClosure(arg0, arg1, 981, __wbg_adapter_28);
|
const ret = makeMutClosure(arg0, arg1, 971, __wbg_adapter_28);
|
||||||
return addHeapObject(ret);
|
return addHeapObject(ret);
|
||||||
};
|
};
|
||||||
imports.wbg.__wbindgen_closure_wrapper2874 = function(arg0, arg1, arg2) {
|
imports.wbg.__wbindgen_closure_wrapper2866 = function(arg0, arg1, arg2) {
|
||||||
const ret = makeMutClosure(arg0, arg1, 981, __wbg_adapter_31);
|
const ret = makeMutClosure(arg0, arg1, 971, __wbg_adapter_31);
|
||||||
return addHeapObject(ret);
|
return addHeapObject(ret);
|
||||||
};
|
};
|
||||||
imports.wbg.__wbindgen_closure_wrapper2877 = function(arg0, arg1, arg2) {
|
imports.wbg.__wbindgen_closure_wrapper2869 = function(arg0, arg1, arg2) {
|
||||||
const ret = makeMutClosure(arg0, arg1, 981, __wbg_adapter_34);
|
const ret = makeMutClosure(arg0, arg1, 971, __wbg_adapter_34);
|
||||||
return addHeapObject(ret);
|
return addHeapObject(ret);
|
||||||
};
|
};
|
||||||
imports.wbg.__wbindgen_closure_wrapper3272 = function(arg0, arg1, arg2) {
|
imports.wbg.__wbindgen_closure_wrapper3248 = function(arg0, arg1, arg2) {
|
||||||
const ret = makeClosure(arg0, arg1, 1143, __wbg_adapter_37);
|
const ret = makeClosure(arg0, arg1, 1129, __wbg_adapter_37);
|
||||||
return addHeapObject(ret);
|
return addHeapObject(ret);
|
||||||
};
|
};
|
||||||
imports.wbg.__wbindgen_closure_wrapper3274 = function(arg0, arg1, arg2) {
|
imports.wbg.__wbindgen_closure_wrapper3250 = function(arg0, arg1, arg2) {
|
||||||
const ret = makeClosure(arg0, arg1, 1143, __wbg_adapter_37);
|
const ret = makeClosure(arg0, arg1, 1129, __wbg_adapter_37);
|
||||||
return addHeapObject(ret);
|
return addHeapObject(ret);
|
||||||
};
|
};
|
||||||
imports.wbg.__wbindgen_closure_wrapper3317 = function(arg0, arg1, arg2) {
|
imports.wbg.__wbindgen_closure_wrapper3293 = function(arg0, arg1, arg2) {
|
||||||
const ret = makeMutClosure(arg0, arg1, 1166, __wbg_adapter_42);
|
const ret = makeMutClosure(arg0, arg1, 1152, __wbg_adapter_42);
|
||||||
return addHeapObject(ret);
|
return addHeapObject(ret);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -91,7 +91,6 @@
|
||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
@ -133,17 +132,7 @@
|
||||||
function on_wasm_loaded() {
|
function on_wasm_loaded() {
|
||||||
console.debug("Wasm loaded. Starting app…");
|
console.debug("Wasm loaded. Starting app…");
|
||||||
|
|
||||||
// This call installs a bunch of callbacks and then returns:
|
let handle = new wasm_bindgen.WebHandle();
|
||||||
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 = '';
|
|
||||||
|
|
||||||
function check_for_panic() {
|
function check_for_panic() {
|
||||||
if (handle.has_panicked()) {
|
if (handle.has_panicked()) {
|
||||||
|
|
@ -159,6 +148,9 @@
|
||||||
<p>
|
<p>
|
||||||
The egui app has crashed.
|
The egui app has crashed.
|
||||||
</p>
|
</p>
|
||||||
|
<p style="font-size:10px" align="left">
|
||||||
|
${handle.panic_message()}
|
||||||
|
</p>
|
||||||
<p style="font-size:14px">
|
<p style="font-size:14px">
|
||||||
See the console for details.
|
See the console for details.
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -172,6 +164,16 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
check_for_panic();
|
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) {
|
function on_error(error) {
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,6 @@
|
||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
@ -158,11 +157,11 @@
|
||||||
|
|
||||||
// This call installs a bunch of callbacks and then returns:
|
// This call installs a bunch of callbacks and then returns:
|
||||||
|
|
||||||
const handle_one = new wasm_bindgen.WebHandle("canvas_id_one");
|
const handle_one = new wasm_bindgen.WebHandle();
|
||||||
const handle_two = new wasm_bindgen.WebHandle("canvas_id_two");
|
const handle_two = new wasm_bindgen.WebHandle();
|
||||||
|
|
||||||
Promise.all([handle_one, handle_two]).then((handles) => {
|
Promise.all([handle_one.start("canvas_id_one"), handle_two.start("canvas_id_two")]).then((handles) => {
|
||||||
on_apps_started(handles[0], handles[1])
|
on_apps_started(handle_one, handle_two)
|
||||||
}).catch(on_error);
|
}).catch(on_error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue