eframe web: Better panic handling (#2942)

* Refactor: remove extra store of events

* Remove unnecessary extra function

* Refactor: simplify event registering

* Store panic summary

* egui_demo_app: move web-part to own module

* index.html: await

* Properly unsubscribe from events on panic

* Better error handling

* Demo app html: hide the wasm canvas and show an error message on panic

* egui_demo_app: add panic button to test panic response on web

* fix typo

* Use a constructor to create WebHandle

* Refactor: less use of locks in the interfaces

* More consistent naming
This commit is contained in:
Emil Ernerfeldt 2023-04-21 08:33:01 +02:00 committed by GitHub
parent 7f2de426d2
commit ac50fa0d94
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 703 additions and 650 deletions

View File

@ -81,61 +81,17 @@ pub use epi::*;
// ----------------------------------------------------------------------------
// When compiling for web
#[cfg(target_arch = "wasm32")]
pub mod web;
#[cfg(target_arch = "wasm32")]
pub use wasm_bindgen;
#[cfg(target_arch = "wasm32")]
use web::AppRunnerRef;
#[cfg(target_arch = "wasm32")]
pub use web_sys;
/// Install event listeners to register different input events
/// and start running the given app.
///
/// ``` no_run
/// #[cfg(target_arch = "wasm32")]
/// use wasm_bindgen::prelude::*;
///
/// /// This is the entry-point for all the web-assembly.
/// /// This is called from the HTML.
/// /// It loads the app, installs some callbacks, then returns.
/// /// It returns a handle to the running app that can be stopped calling `AppRunner::stop_web`.
/// /// You can add more callbacks like this if you want to call in to your code.
/// #[cfg(target_arch = "wasm32")]
/// #[wasm_bindgen]
/// pub struct WebHandle {
/// handle: AppRunnerRef,
/// }
/// #[cfg(target_arch = "wasm32")]
/// #[wasm_bindgen]
/// pub async fn start(canvas_id: &str) -> Result<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.
#[cfg(target_arch = "wasm32")]
pub async fn start_web(
canvas_id: &str,
web_options: WebOptions,
app_creator: AppCreator,
) -> std::result::Result<AppRunnerRef, wasm_bindgen::JsValue> {
let handle = web::start(canvas_id, web_options, app_creator).await?;
pub mod web;
Ok(handle)
}
#[cfg(target_arch = "wasm32")]
pub use web::start_web;
// ----------------------------------------------------------------------------
// When compiling natively

View File

@ -1,7 +1,6 @@
use egui::{
mutex::{Mutex, MutexGuard},
TexturesDelta,
};
use std::{cell::RefCell, rc::Rc};
use egui::{mutex::Mutex, TexturesDelta};
use crate::{epi, App};
@ -170,6 +169,64 @@ fn test_parse_query() {
// ----------------------------------------------------------------------------
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn error(msg: String);
type Error;
#[wasm_bindgen(constructor)]
fn new() -> Error;
#[wasm_bindgen(structural, method, getter)]
fn stack(error: &Error) -> String;
}
#[derive(Clone, Debug)]
pub struct PanicSummary {
message: String,
callstack: String,
}
impl PanicSummary {
pub fn new(info: &std::panic::PanicInfo<'_>) -> Self {
let message = info.to_string();
let callstack = Error::new().stack();
Self { message, callstack }
}
pub fn message(&self) -> String {
self.message.clone()
}
pub fn callstack(&self) -> String {
self.callstack.clone()
}
}
/// Handle to information about any panic than has occurred
#[derive(Clone, Default)]
pub struct PanicHandler {
summary: Option<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,
@ -183,7 +240,6 @@ pub struct AppRunner {
pub(crate) text_cursor_pos: Option<egui::Pos2>,
pub(crate) mutable_text_under_cursor: bool,
textures_delta: TexturesDelta,
pub events_to_unsubscribe: Vec<EventToUnsubscribe>,
}
impl Drop for AppRunner {
@ -277,7 +333,6 @@ impl AppRunner {
text_cursor_pos: None,
mutable_text_under_cursor: false,
textures_delta: Default::default(),
events_to_unsubscribe: Default::default(),
};
runner.input.raw.max_texture_side = Some(runner.painter.max_texture_side());
@ -333,21 +388,13 @@ impl AppRunner {
Ok(())
}
pub fn destroy(&mut self) -> Result<(), JsValue> {
let is_destroyed_already = self.is_destroyed.fetch();
if is_destroyed_already {
fn destroy(&mut self) {
if self.is_destroyed.fetch() {
log::warn!("App was destroyed already");
Ok(())
} else {
log::debug!("Destroying");
for x in self.events_to_unsubscribe.drain(..) {
x.unsubscribe()?;
}
self.painter.destroy();
self.is_destroyed.set_true();
Ok(())
}
}
@ -439,7 +486,130 @@ impl AppRunner {
// ----------------------------------------------------------------------------
pub type AppRunnerRef = Arc<Mutex<AppRunner>>;
/// 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.
let events_to_unsubscribe: Vec<_> =
std::mem::take(&mut *self.events_to_unsubscribe.borrow_mut());
if !events_to_unsubscribe.is_empty() {
log::debug!(
"Unsubscribing from {} events due to panic",
events_to_unsubscribe.len()
);
for x in events_to_unsubscribe {
if let Err(err) = x.unsubscribe() {
log::error!("Failed to unsubscribe from event: {err:?}");
}
}
}
}
}
/// Returns true if there has been a panic.
pub fn has_panicked(&self) -> bool {
self.unsubscribe_if_panicked();
self.panic_handler.lock().has_panicked()
}
/// Returns `Some` if there has been a panic.
pub fn panic_summary(&self) -> Option<PanicSummary> {
self.unsubscribe_if_panicked();
self.panic_handler.lock().panic_summary()
}
pub fn destroy(&self) {
if let Some(mut runner) = self.try_lock() {
runner.destroy();
}
}
/// Returns `None` if there has been a panic, or if we have been destroyed.
/// In that case, just return to JS.
pub fn try_lock(&self) -> Option<std::cell::RefMut<'_, AppRunner>> {
if self.has_panicked() {
None
} else {
let lock = self.runner.borrow_mut();
if lock.is_destroyed.fetch() {
None
} else {
Some(lock)
}
}
}
/// Convenience function to reduce boilerplate and ensure that all event handlers
/// are dealt with in the same way
pub fn add_event_listener<E: wasm_bindgen::JsCast>(
&self,
target: &EventTarget,
event_name: &'static str,
mut closure: impl FnMut(E, &mut AppRunner) + 'static,
) -> Result<(), JsValue> {
// Create a JS closure based on the FnMut provided
let closure = Closure::wrap({
// Clone atomics
let runner_ref = self.clone();
Box::new(move |event: web_sys::Event| {
// Only call the wrapped closure if the egui code has not panicked
if let Some(mut runner_lock) = runner_ref.try_lock() {
// Cast the event to the expected event type
let event = event.unchecked_into::<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,
@ -454,7 +624,7 @@ pub struct IntervalHandle {
pub enum EventToUnsubscribe {
TargetEvent(TargetEvent),
#[allow(dead_code)]
IntervalHandle(IntervalHandle),
}
@ -477,61 +647,42 @@ impl EventToUnsubscribe {
}
}
pub struct AppRunnerContainer {
pub runner: AppRunnerRef,
/// Set to `true` if there is a panic.
/// Used to ignore callbacks after a panic.
pub panicked: Arc<AtomicBool>,
pub events: Vec<EventToUnsubscribe>,
}
impl AppRunnerContainer {
/// Convenience function to reduce boilerplate and ensure that all event handlers
/// are dealt with in the same way
pub fn add_event_listener<E: wasm_bindgen::JsCast>(
&mut self,
target: &EventTarget,
event_name: &'static str,
mut closure: impl FnMut(E, MutexGuard<'_, AppRunner>) + 'static,
) -> Result<(), JsValue> {
// Create a JS closure based on the FnMut provided
let closure = Closure::wrap({
// Clone atomics
let runner_ref = self.runner.clone();
let panicked = self.panicked.clone();
Box::new(move |event: web_sys::Event| {
// Only call the wrapped closure if the egui code has not panicked
if !panicked.load(Ordering::SeqCst) {
// Cast the event to the expected event type
let event = event.unchecked_into::<E>();
closure(event, runner_ref.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,
};
self.events.push(EventToUnsubscribe::TargetEvent(handle));
Ok(())
}
}
// ----------------------------------------------------------------------------
/// Install event listeners to register different input events
/// and start running the given app.
pub async fn start(
///
/// ``` no_run
/// #[cfg(target_arch = "wasm32")]
/// use wasm_bindgen::prelude::*;
///
/// /// This is the entry-point for all the web-assembly.
/// /// This is called from the HTML.
/// /// It loads the app, installs some callbacks, then returns.
/// /// It returns a handle to the running app that can be stopped calling `AppRunner::stop_web`.
/// /// You can add more callbacks like this if you want to call in to your code.
/// #[cfg(target_arch = "wasm32")]
/// #[wasm_bindgen]
/// pub struct WebHandle {
/// handle: AppRunnerRef,
/// }
/// #[cfg(target_arch = "wasm32")]
/// #[wasm_bindgen]
/// pub async fn start(canvas_id: &str) -> Result<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,
@ -544,43 +695,36 @@ pub async fn start(
let mut runner = AppRunner::new(canvas_id, web_options, app_creator).await?;
runner.warm_up()?;
start_runner(runner, follow_system_theme)
}
let runner_ref = AppRunnerRef::new(runner);
/// Install event listeners to register different input events
/// and starts running the given [`AppRunner`].
fn start_runner(app_runner: AppRunner, follow_system_theme: bool) -> Result<AppRunnerRef, JsValue> {
let mut runner_container = AppRunnerContainer {
runner: Arc::new(Mutex::new(app_runner)),
panicked: Arc::new(AtomicBool::new(false)),
events: Vec::with_capacity(20),
};
super::events::install_canvas_events(&mut runner_container)?;
super::events::install_document_events(&mut runner_container)?;
super::events::install_window_events(&mut runner_container)?;
text_agent::install_text_agent(&mut runner_container)?;
if follow_system_theme {
super::events::install_color_scheme_change_event(&mut runner_container)?;
// Install events:
{
super::events::install_canvas_events(&runner_ref)?;
super::events::install_document_events(&runner_ref)?;
super::events::install_window_events(&runner_ref)?;
text_agent::install_text_agent(&runner_ref)?;
if follow_system_theme {
super::events::install_color_scheme_change_event(&runner_ref)?;
}
super::events::paint_and_schedule(&runner_ref)?;
}
super::events::paint_and_schedule(&runner_container.runner, runner_container.panicked.clone())?;
// Instal panic handler:
{
// Disable all event handlers on panic
let previous_hook = std::panic::take_hook();
let panic_handler = runner_ref.panic_handler.clone();
// Disable all event handlers on panic
let previous_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |panic_info| {
log::info!("eframe detected a panic");
panic_handler.lock().on_panic(panic_info);
runner_container.runner.lock().events_to_unsubscribe = runner_container.events;
// Propagate panic info to the previously registered panic hook
previous_hook(panic_info);
}));
}
std::panic::set_hook(Box::new(move |panic_info| {
log::info!("egui disabled all event handlers due to panic");
runner_container.panicked.store(true, SeqCst);
// Propagate panic info to the previously registered panic hook
previous_hook(panic_info);
}));
Ok(runner_container.runner)
Ok(runner_ref)
}
// ----------------------------------------------------------------------------

View File

@ -1,99 +1,80 @@
use std::sync::atomic::{AtomicBool, Ordering};
use egui::Key;
use super::*;
/// Calls `request_animation_frame` to schedule repaint.
///
/// It will only paint if needed, but will always call `request_animation_frame` immediately.
pub fn paint_and_schedule(
runner_ref: &AppRunnerRef,
panicked: Arc<AtomicBool>,
) -> Result<(), JsValue> {
struct IsDestroyed(pub bool);
fn paint_if_needed(runner_ref: &AppRunnerRef) -> Result<IsDestroyed, JsValue> {
let mut runner_lock = runner_ref.lock();
let is_destroyed = runner_lock.is_destroyed.fetch();
if !is_destroyed && runner_lock.needs_repaint.when_to_repaint() <= now_sec() {
runner_lock.needs_repaint.clear();
let (repaint_after, clipped_primitives) = runner_lock.logic()?;
runner_lock.paint(&clipped_primitives)?;
runner_lock
pub fn paint_and_schedule(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
fn paint_if_needed(runner: &mut AppRunner) -> Result<(), JsValue> {
if runner.needs_repaint.when_to_repaint() <= now_sec() {
runner.needs_repaint.clear();
let (repaint_after, clipped_primitives) = runner.logic()?;
runner.paint(&clipped_primitives)?;
runner
.needs_repaint
.repaint_after(repaint_after.as_secs_f64());
runner_lock.auto_save_if_needed();
runner.auto_save_if_needed();
}
Ok(IsDestroyed(is_destroyed))
Ok(())
}
fn request_animation_frame(
runner_ref: AppRunnerRef,
panicked: Arc<AtomicBool>,
) -> Result<(), JsValue> {
fn request_animation_frame(runner_ref: AppRunnerRef) -> Result<(), JsValue> {
let window = web_sys::window().unwrap();
let closure = Closure::once(move || paint_and_schedule(&runner_ref, panicked));
let closure = Closure::once(move || paint_and_schedule(&runner_ref));
window.request_animation_frame(closure.as_ref().unchecked_ref())?;
closure.forget(); // We must forget it, or else the callback is canceled on drop
Ok(())
}
// Only paint and schedule if there has been no panic
if !panicked.load(Ordering::SeqCst) {
let is_destroyed = paint_if_needed(runner_ref)?;
if !is_destroyed.0 {
request_animation_frame(runner_ref.clone(), panicked)?;
}
if let Some(mut runner_lock) = runner_ref.try_lock() {
paint_if_needed(&mut runner_lock)?;
drop(runner_lock);
request_animation_frame(runner_ref.clone())?;
}
Ok(())
}
pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> {
pub fn install_document_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
let document = web_sys::window().unwrap().document().unwrap();
{
// Avoid sticky modifier keys on alt-tab:
for event_name in ["blur", "focus"] {
let closure =
move |_event: web_sys::MouseEvent,
mut runner_lock: egui::mutex::MutexGuard<'_, AppRunner>| {
let has_focus = event_name == "focus";
let closure = move |_event: web_sys::MouseEvent, runner: &mut AppRunner| {
let has_focus = event_name == "focus";
if !has_focus {
// We lost focus - good idea to save
runner_lock.save();
}
if !has_focus {
// We lost focus - good idea to save
runner.save();
}
runner_lock.input.on_web_page_focus_change(has_focus);
runner_lock.egui_ctx().request_repaint();
// log::debug!("{event_name:?}");
};
runner.input.on_web_page_focus_change(has_focus);
runner.egui_ctx().request_repaint();
// log::debug!("{event_name:?}");
};
runner_container.add_event_listener(&document, event_name, closure)?;
runner_ref.add_event_listener(&document, event_name, closure)?;
}
}
runner_container.add_event_listener(
runner_ref.add_event_listener(
&document,
"keydown",
|event: web_sys::KeyboardEvent, mut runner_lock| {
|event: web_sys::KeyboardEvent, runner| {
if event.is_composing() || event.key_code() == 229 {
// https://web.archive.org/web/20200526195704/https://www.fxsitecompat.dev/en-CA/docs/2018/keydown-and-keyup-events-are-now-fired-during-ime-composition/
return;
}
let modifiers = modifiers_from_event(&event);
runner_lock.input.raw.modifiers = modifiers;
runner.input.raw.modifiers = modifiers;
let key = event.key();
let egui_key = translate_key(&key);
if let Some(key) = egui_key {
runner_lock.input.raw.events.push(egui::Event::Key {
runner.input.raw.events.push(egui::Event::Key {
key,
pressed: true,
repeat: false, // egui will fill this in for us!
@ -106,18 +87,18 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res
// When text agent is shown, it sends text event instead.
&& text_agent::text_agent().hidden()
{
runner_lock.input.raw.events.push(egui::Event::Text(key));
runner.input.raw.events.push(egui::Event::Text(key));
}
runner_lock.needs_repaint.repaint_asap();
runner.needs_repaint.repaint_asap();
let egui_wants_keyboard = runner_lock.egui_ctx().wants_keyboard_input();
let egui_wants_keyboard = runner.egui_ctx().wants_keyboard_input();
#[allow(clippy::if_same_then_else)]
let prevent_default = if egui_key == Some(Key::Tab) {
let prevent_default = if egui_key == Some(egui::Key::Tab) {
// Always prevent moving cursor to url bar.
// egui wants to use tab to move to the next text field.
true
} else if egui_key == Some(Key::P) {
} else if egui_key == Some(egui::Key::P) {
#[allow(clippy::needless_bool)]
if modifiers.ctrl || modifiers.command || modifiers.mac_cmd {
true // Prevent ctrl-P opening the print dialog. Users may want to use it for a command palette.
@ -152,35 +133,35 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res
},
)?;
runner_container.add_event_listener(
runner_ref.add_event_listener(
&document,
"keyup",
|event: web_sys::KeyboardEvent, mut runner_lock| {
|event: web_sys::KeyboardEvent, runner| {
let modifiers = modifiers_from_event(&event);
runner_lock.input.raw.modifiers = modifiers;
runner.input.raw.modifiers = modifiers;
if let Some(key) = translate_key(&event.key()) {
runner_lock.input.raw.events.push(egui::Event::Key {
runner.input.raw.events.push(egui::Event::Key {
key,
pressed: false,
repeat: false,
modifiers,
});
}
runner_lock.needs_repaint.repaint_asap();
runner.needs_repaint.repaint_asap();
},
)?;
#[cfg(web_sys_unstable_apis)]
runner_container.add_event_listener(
runner_ref.add_event_listener(
&document,
"paste",
|event: web_sys::ClipboardEvent, mut runner_lock| {
|event: web_sys::ClipboardEvent, runner| {
if let Some(data) = event.clipboard_data() {
if let Ok(text) = data.get_data("text") {
let text = text.replace("\r\n", "\n");
if !text.is_empty() {
runner_lock.input.raw.events.push(egui::Event::Paste(text));
runner_lock.needs_repaint.repaint_asap();
runner.input.raw.events.push(egui::Event::Paste(text));
runner.needs_repaint.repaint_asap();
}
event.stop_propagation();
event.prevent_default();
@ -190,76 +171,54 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res
)?;
#[cfg(web_sys_unstable_apis)]
runner_container.add_event_listener(
&document,
"cut",
|_: web_sys::ClipboardEvent, mut runner_lock| {
runner_lock.input.raw.events.push(egui::Event::Cut);
runner_lock.needs_repaint.repaint_asap();
},
)?;
runner_ref.add_event_listener(&document, "cut", |_: web_sys::ClipboardEvent, runner| {
runner.input.raw.events.push(egui::Event::Cut);
runner.needs_repaint.repaint_asap();
})?;
#[cfg(web_sys_unstable_apis)]
runner_container.add_event_listener(
&document,
"copy",
|_: web_sys::ClipboardEvent, mut runner_lock| {
runner_lock.input.raw.events.push(egui::Event::Copy);
runner_lock.needs_repaint.repaint_asap();
},
)?;
runner_ref.add_event_listener(&document, "copy", |_: web_sys::ClipboardEvent, runner| {
runner.input.raw.events.push(egui::Event::Copy);
runner.needs_repaint.repaint_asap();
})?;
Ok(())
}
pub fn install_window_events(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> {
pub fn install_window_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
let window = web_sys::window().unwrap();
// Save-on-close
runner_container.add_event_listener(
&window,
"onbeforeunload",
|_: web_sys::Event, mut runner_lock| {
runner_lock.save();
},
)?;
runner_ref.add_event_listener(&window, "onbeforeunload", |_: web_sys::Event, runner| {
runner.save();
})?;
for event_name in &["load", "pagehide", "pageshow", "resize"] {
runner_container.add_event_listener(
&window,
event_name,
|_: web_sys::Event, runner_lock| {
runner_lock.needs_repaint.repaint_asap();
},
)?;
runner_ref.add_event_listener(&window, event_name, |_: web_sys::Event, runner| {
runner.needs_repaint.repaint_asap();
})?;
}
runner_container.add_event_listener(
&window,
"hashchange",
|_: web_sys::Event, mut runner_lock| {
// `epi::Frame::info(&self)` clones `epi::IntegrationInfo`, but we need to modify the original here
runner_lock.frame.info.web_info.location.hash = location_hash();
},
)?;
runner_ref.add_event_listener(&window, "hashchange", |_: web_sys::Event, runner| {
// `epi::Frame::info(&self)` clones `epi::IntegrationInfo`, but we need to modify the original here
runner.frame.info.web_info.location.hash = location_hash();
})?;
Ok(())
}
pub fn install_color_scheme_change_event(
runner_container: &mut AppRunnerContainer,
) -> Result<(), JsValue> {
pub fn install_color_scheme_change_event(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
let window = web_sys::window().unwrap();
if let Some(media_query_list) = prefers_color_scheme_dark(&window)? {
runner_container.add_event_listener::<web_sys::MediaQueryListEvent>(
runner_ref.add_event_listener::<web_sys::MediaQueryListEvent>(
&media_query_list,
"change",
|event, mut runner_lock| {
|event, runner| {
let theme = theme_from_dark_mode(event.matches());
runner_lock.frame.info.system_theme = Some(theme);
runner_lock.egui_ctx().set_visuals(theme.egui_visuals());
runner_lock.needs_repaint.repaint_asap();
runner.frame.info.system_theme = Some(theme);
runner.egui_ctx().set_visuals(theme.egui_visuals());
runner.needs_repaint.repaint_asap();
},
)?;
}
@ -267,8 +226,8 @@ pub fn install_color_scheme_change_event(
Ok(())
}
pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> {
let canvas = canvas_element(runner_container.runner.lock().canvas_id()).unwrap();
pub fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
let canvas = canvas_element(runner_ref.try_lock().unwrap().canvas_id()).unwrap();
{
let prevent_default_events = [
@ -280,285 +239,231 @@ pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Resul
];
for event_name in prevent_default_events {
let closure =
move |event: web_sys::MouseEvent,
mut _runner_lock: egui::mutex::MutexGuard<'_, AppRunner>| {
event.prevent_default();
// event.stop_propagation();
// log::debug!("Preventing event {event_name:?}");
};
let closure = move |event: web_sys::MouseEvent, _runner: &mut AppRunner| {
event.prevent_default();
// event.stop_propagation();
// log::debug!("Preventing event {event_name:?}");
};
runner_container.add_event_listener(&canvas, event_name, closure)?;
runner_ref.add_event_listener(&canvas, event_name, closure)?;
}
}
runner_container.add_event_listener(
runner_ref.add_event_listener(
&canvas,
"mousedown",
|event: web_sys::MouseEvent, mut runner_lock: egui::mutex::MutexGuard<'_, AppRunner>| {
|event: web_sys::MouseEvent, runner: &mut AppRunner| {
if let Some(button) = button_from_mouse_event(&event) {
let pos = pos_from_mouse_event(runner_lock.canvas_id(), &event);
let modifiers = runner_lock.input.raw.modifiers;
runner_lock
.input
.raw
.events
.push(egui::Event::PointerButton {
pos,
button,
pressed: true,
modifiers,
});
runner_lock.needs_repaint.repaint_asap();
let pos = pos_from_mouse_event(runner.canvas_id(), &event);
let modifiers = runner.input.raw.modifiers;
runner.input.raw.events.push(egui::Event::PointerButton {
pos,
button,
pressed: true,
modifiers,
});
runner.needs_repaint.repaint_asap();
}
event.stop_propagation();
// Note: prevent_default breaks VSCode tab focusing, hence why we don't call it here.
},
)?;
runner_container.add_event_listener(
runner_ref.add_event_listener(
&canvas,
"mousemove",
|event: web_sys::MouseEvent, mut runner_lock| {
let pos = pos_from_mouse_event(runner_lock.canvas_id(), &event);
runner_lock
.input
.raw
.events
.push(egui::Event::PointerMoved(pos));
runner_lock.needs_repaint.repaint_asap();
|event: web_sys::MouseEvent, runner| {
let pos = pos_from_mouse_event(runner.canvas_id(), &event);
runner.input.raw.events.push(egui::Event::PointerMoved(pos));
runner.needs_repaint.repaint_asap();
event.stop_propagation();
event.prevent_default();
},
)?;
runner_container.add_event_listener(
&canvas,
"mouseup",
|event: web_sys::MouseEvent, mut runner_lock| {
if let Some(button) = button_from_mouse_event(&event) {
let pos = pos_from_mouse_event(runner_lock.canvas_id(), &event);
let modifiers = runner_lock.input.raw.modifiers;
runner_lock
.input
.raw
.events
.push(egui::Event::PointerButton {
pos,
button,
pressed: false,
modifiers,
});
runner_lock.needs_repaint.repaint_asap();
runner_ref.add_event_listener(&canvas, "mouseup", |event: web_sys::MouseEvent, runner| {
if let Some(button) = button_from_mouse_event(&event) {
let pos = pos_from_mouse_event(runner.canvas_id(), &event);
let modifiers = runner.input.raw.modifiers;
runner.input.raw.events.push(egui::Event::PointerButton {
pos,
button,
pressed: false,
modifiers,
});
runner.needs_repaint.repaint_asap();
text_agent::update_text_agent(runner_lock);
}
event.stop_propagation();
event.prevent_default();
},
)?;
text_agent::update_text_agent(runner);
}
event.stop_propagation();
event.prevent_default();
})?;
runner_container.add_event_listener(
runner_ref.add_event_listener(
&canvas,
"mouseleave",
|event: web_sys::MouseEvent, mut runner_lock| {
runner_lock.input.raw.events.push(egui::Event::PointerGone);
runner_lock.needs_repaint.repaint_asap();
|event: web_sys::MouseEvent, runner| {
runner.input.raw.events.push(egui::Event::PointerGone);
runner.needs_repaint.repaint_asap();
event.stop_propagation();
event.prevent_default();
},
)?;
runner_container.add_event_listener(
runner_ref.add_event_listener(
&canvas,
"touchstart",
|event: web_sys::TouchEvent, mut runner_lock| {
let mut latest_touch_pos_id = runner_lock.input.latest_touch_pos_id;
let pos =
pos_from_touch_event(runner_lock.canvas_id(), &event, &mut latest_touch_pos_id);
runner_lock.input.latest_touch_pos_id = latest_touch_pos_id;
runner_lock.input.latest_touch_pos = Some(pos);
let modifiers = runner_lock.input.raw.modifiers;
runner_lock
.input
.raw
.events
.push(egui::Event::PointerButton {
pos,
button: egui::PointerButton::Primary,
pressed: true,
modifiers,
});
push_touches(&mut runner_lock, egui::TouchPhase::Start, &event);
runner_lock.needs_repaint.repaint_asap();
event.stop_propagation();
event.prevent_default();
},
)?;
runner_container.add_event_listener(
&canvas,
"touchmove",
|event: web_sys::TouchEvent, mut runner_lock| {
let mut latest_touch_pos_id = runner_lock.input.latest_touch_pos_id;
let pos =
pos_from_touch_event(runner_lock.canvas_id(), &event, &mut latest_touch_pos_id);
runner_lock.input.latest_touch_pos_id = latest_touch_pos_id;
runner_lock.input.latest_touch_pos = Some(pos);
runner_lock
.input
.raw
.events
.push(egui::Event::PointerMoved(pos));
push_touches(&mut runner_lock, egui::TouchPhase::Move, &event);
runner_lock.needs_repaint.repaint_asap();
event.stop_propagation();
event.prevent_default();
},
)?;
runner_container.add_event_listener(
&canvas,
"touchend",
|event: web_sys::TouchEvent, mut runner_lock| {
if let Some(pos) = runner_lock.input.latest_touch_pos {
let modifiers = runner_lock.input.raw.modifiers;
// First release mouse to click:
runner_lock
.input
.raw
.events
.push(egui::Event::PointerButton {
pos,
button: egui::PointerButton::Primary,
pressed: false,
modifiers,
});
// Then remove hover effect:
runner_lock.input.raw.events.push(egui::Event::PointerGone);
push_touches(&mut runner_lock, egui::TouchPhase::End, &event);
runner_lock.needs_repaint.repaint_asap();
event.stop_propagation();
event.prevent_default();
}
// Finally, focus or blur text agent to toggle mobile keyboard:
text_agent::update_text_agent(runner_lock);
},
)?;
runner_container.add_event_listener(
&canvas,
"touchcancel",
|event: web_sys::TouchEvent, mut runner_lock| {
push_touches(&mut runner_lock, egui::TouchPhase::Cancel, &event);
event.stop_propagation();
event.prevent_default();
},
)?;
runner_container.add_event_listener(
&canvas,
"wheel",
|event: web_sys::WheelEvent, mut runner_lock| {
let unit = match event.delta_mode() {
web_sys::WheelEvent::DOM_DELTA_PIXEL => egui::MouseWheelUnit::Point,
web_sys::WheelEvent::DOM_DELTA_LINE => egui::MouseWheelUnit::Line,
web_sys::WheelEvent::DOM_DELTA_PAGE => egui::MouseWheelUnit::Page,
_ => return,
};
// delta sign is flipped to match native (winit) convention.
let delta = -egui::vec2(event.delta_x() as f32, event.delta_y() as f32);
let modifiers = runner_lock.input.raw.modifiers;
runner_lock.input.raw.events.push(egui::Event::MouseWheel {
unit,
delta,
|event: web_sys::TouchEvent, runner| {
let mut latest_touch_pos_id = runner.input.latest_touch_pos_id;
let pos = pos_from_touch_event(runner.canvas_id(), &event, &mut latest_touch_pos_id);
runner.input.latest_touch_pos_id = latest_touch_pos_id;
runner.input.latest_touch_pos = Some(pos);
let modifiers = runner.input.raw.modifiers;
runner.input.raw.events.push(egui::Event::PointerButton {
pos,
button: egui::PointerButton::Primary,
pressed: true,
modifiers,
});
let scroll_multiplier = match unit {
egui::MouseWheelUnit::Page => canvas_size_in_points(runner_lock.canvas_id()).y,
egui::MouseWheelUnit::Line => {
#[allow(clippy::let_and_return)]
let points_per_scroll_line = 8.0; // Note that this is intentionally different from what we use in winit.
points_per_scroll_line
}
egui::MouseWheelUnit::Point => 1.0,
};
let mut delta = scroll_multiplier * delta;
// Report a zoom event in case CTRL (on Windows or Linux) or CMD (on Mac) is pressed.
// This if-statement is equivalent to how `Modifiers.command` is determined in
// `modifiers_from_event()`, but we cannot directly use that fn for a [`WheelEvent`].
if event.ctrl_key() || event.meta_key() {
let factor = (delta.y / 200.0).exp();
runner_lock.input.raw.events.push(egui::Event::Zoom(factor));
} else {
if event.shift_key() {
// Treat as horizontal scrolling.
// Note: one Mac we already get horizontal scroll events when shift is down.
delta = egui::vec2(delta.x + delta.y, 0.0);
}
runner_lock
.input
.raw
.events
.push(egui::Event::Scroll(delta));
}
runner_lock.needs_repaint.repaint_asap();
push_touches(runner, egui::TouchPhase::Start, &event);
runner.needs_repaint.repaint_asap();
event.stop_propagation();
event.prevent_default();
},
)?;
runner_container.add_event_listener(
runner_ref.add_event_listener(
&canvas,
"dragover",
|event: web_sys::DragEvent, mut runner_lock| {
if let Some(data_transfer) = event.data_transfer() {
runner_lock.input.raw.hovered_files.clear();
for i in 0..data_transfer.items().length() {
if let Some(item) = data_transfer.items().get(i) {
runner_lock.input.raw.hovered_files.push(egui::HoveredFile {
mime: item.type_(),
..Default::default()
});
}
}
runner_lock.needs_repaint.repaint_asap();
event.stop_propagation();
event.prevent_default();
}
},
)?;
"touchmove",
|event: web_sys::TouchEvent, runner| {
let mut latest_touch_pos_id = runner.input.latest_touch_pos_id;
let pos = pos_from_touch_event(runner.canvas_id(), &event, &mut latest_touch_pos_id);
runner.input.latest_touch_pos_id = latest_touch_pos_id;
runner.input.latest_touch_pos = Some(pos);
runner.input.raw.events.push(egui::Event::PointerMoved(pos));
runner_container.add_event_listener(
&canvas,
"dragleave",
|event: web_sys::DragEvent, mut runner_lock| {
runner_lock.input.raw.hovered_files.clear();
runner_lock.needs_repaint.repaint_asap();
push_touches(runner, egui::TouchPhase::Move, &event);
runner.needs_repaint.repaint_asap();
event.stop_propagation();
event.prevent_default();
},
)?;
runner_container.add_event_listener(&canvas, "drop", {
let runner_ref = runner_container.runner.clone();
runner_ref.add_event_listener(&canvas, "touchend", |event: web_sys::TouchEvent, runner| {
if let Some(pos) = runner.input.latest_touch_pos {
let modifiers = runner.input.raw.modifiers;
// First release mouse to click:
runner.input.raw.events.push(egui::Event::PointerButton {
pos,
button: egui::PointerButton::Primary,
pressed: false,
modifiers,
});
// Then remove hover effect:
runner.input.raw.events.push(egui::Event::PointerGone);
move |event: web_sys::DragEvent, mut runner_lock| {
push_touches(runner, egui::TouchPhase::End, &event);
runner.needs_repaint.repaint_asap();
event.stop_propagation();
event.prevent_default();
}
// Finally, focus or blur text agent to toggle mobile keyboard:
text_agent::update_text_agent(runner);
})?;
runner_ref.add_event_listener(
&canvas,
"touchcancel",
|event: web_sys::TouchEvent, runner| {
push_touches(runner, egui::TouchPhase::Cancel, &event);
event.stop_propagation();
event.prevent_default();
},
)?;
runner_ref.add_event_listener(&canvas, "wheel", |event: web_sys::WheelEvent, runner| {
let unit = match event.delta_mode() {
web_sys::WheelEvent::DOM_DELTA_PIXEL => egui::MouseWheelUnit::Point,
web_sys::WheelEvent::DOM_DELTA_LINE => egui::MouseWheelUnit::Line,
web_sys::WheelEvent::DOM_DELTA_PAGE => egui::MouseWheelUnit::Page,
_ => return,
};
// delta sign is flipped to match native (winit) convention.
let delta = -egui::vec2(event.delta_x() as f32, event.delta_y() as f32);
let modifiers = runner.input.raw.modifiers;
runner.input.raw.events.push(egui::Event::MouseWheel {
unit,
delta,
modifiers,
});
let scroll_multiplier = match unit {
egui::MouseWheelUnit::Page => canvas_size_in_points(runner.canvas_id()).y,
egui::MouseWheelUnit::Line => {
#[allow(clippy::let_and_return)]
let points_per_scroll_line = 8.0; // Note that this is intentionally different from what we use in winit.
points_per_scroll_line
}
egui::MouseWheelUnit::Point => 1.0,
};
let mut delta = scroll_multiplier * delta;
// Report a zoom event in case CTRL (on Windows or Linux) or CMD (on Mac) is pressed.
// This if-statement is equivalent to how `Modifiers.command` is determined in
// `modifiers_from_event()`, but we cannot directly use that fn for a [`WheelEvent`].
if event.ctrl_key() || event.meta_key() {
let factor = (delta.y / 200.0).exp();
runner.input.raw.events.push(egui::Event::Zoom(factor));
} else {
if event.shift_key() {
// Treat as horizontal scrolling.
// Note: one Mac we already get horizontal scroll events when shift is down.
delta = egui::vec2(delta.x + delta.y, 0.0);
}
runner.input.raw.events.push(egui::Event::Scroll(delta));
}
runner.needs_repaint.repaint_asap();
event.stop_propagation();
event.prevent_default();
})?;
runner_ref.add_event_listener(&canvas, "dragover", |event: web_sys::DragEvent, runner| {
if let Some(data_transfer) = event.data_transfer() {
runner.input.raw.hovered_files.clear();
for i in 0..data_transfer.items().length() {
if let Some(item) = data_transfer.items().get(i) {
runner.input.raw.hovered_files.push(egui::HoveredFile {
mime: item.type_(),
..Default::default()
});
}
}
runner.needs_repaint.repaint_asap();
event.stop_propagation();
event.prevent_default();
}
})?;
runner_ref.add_event_listener(&canvas, "dragleave", |event: web_sys::DragEvent, runner| {
runner.input.raw.hovered_files.clear();
runner.needs_repaint.repaint_asap();
event.stop_propagation();
event.prevent_default();
})?;
runner_ref.add_event_listener(&canvas, "drop", {
let runner_ref = runner_ref.clone();
move |event: web_sys::DragEvent, runner| {
if let Some(data_transfer) = event.data_transfer() {
runner_lock.input.raw.hovered_files.clear();
runner_lock.needs_repaint.repaint_asap();
// Unlock the runner so it can be locked after a future await point
drop(runner_lock);
runner.input.raw.hovered_files.clear();
runner.needs_repaint.repaint_asap();
if let Some(files) = data_transfer.files() {
for i in 0..files.length() {
@ -578,17 +483,17 @@ pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Resul
let bytes = js_sys::Uint8Array::new(&array_buffer).to_vec();
log::debug!("Loaded {:?} ({} bytes).", name, bytes.len());
// Re-lock the mutex on the other side of the await point
let mut runner_lock = runner_ref.lock();
runner_lock.input.raw.dropped_files.push(
egui::DroppedFile {
name,
last_modified: Some(last_modified),
bytes: Some(bytes.into()),
..Default::default()
},
);
runner_lock.needs_repaint.repaint_asap();
if let Some(mut runner_lock) = runner_ref.try_lock() {
runner_lock.input.raw.dropped_files.push(
egui::DroppedFile {
name,
last_modified: Some(last_modified),
bytes: Some(bytes.into()),
..Default::default()
},
);
runner_lock.needs_repaint.repaint_asap();
}
}
Err(err) => {
log::error!("Failed to read file: {:?}", err);

View File

@ -32,10 +32,7 @@ pub use events::*;
pub use storage::*;
use std::collections::BTreeMap;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use std::sync::Arc;
use egui::Vec2;
use wasm_bindgen::prelude::*;

View File

@ -1,12 +1,12 @@
//! The text agent is an `<input>` element used to trigger
//! mobile keyboard and IME input.
//!
use std::{cell::Cell, rc::Rc};
use super::{canvas_element, AppRunner, AppRunnerContainer};
use egui::mutex::MutexGuard;
use std::cell::Cell;
use std::rc::Rc;
use wasm_bindgen::prelude::*;
use super::{canvas_element, AppRunner, AppRunnerRef};
static AGENT_ID: &str = "egui_text_agent";
pub fn text_agent() -> web_sys::HtmlInputElement {
@ -21,7 +21,7 @@ pub fn text_agent() -> web_sys::HtmlInputElement {
}
/// Text event handler,
pub fn install_text_agent(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> {
pub fn install_text_agent(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let body = document.body().expect("document should have a body");
@ -44,60 +44,56 @@ pub fn install_text_agent(runner_container: &mut AppRunnerContainer) -> Result<(
input.set_hidden(true);
// When IME is off
runner_container.add_event_listener(&input, "input", {
runner_ref.add_event_listener(&input, "input", {
let input_clone = input.clone();
let is_composing = is_composing.clone();
move |_event: web_sys::InputEvent, mut runner_lock| {
move |_event: web_sys::InputEvent, runner| {
let text = input_clone.value();
if !text.is_empty() && !is_composing.get() {
input_clone.set_value("");
runner_lock.input.raw.events.push(egui::Event::Text(text));
runner_lock.needs_repaint.repaint_asap();
runner.input.raw.events.push(egui::Event::Text(text));
runner.needs_repaint.repaint_asap();
}
}
})?;
{
// When IME is on, handle composition event
runner_container.add_event_listener(&input, "compositionstart", {
runner_ref.add_event_listener(&input, "compositionstart", {
let input_clone = input.clone();
let is_composing = is_composing.clone();
move |_event: web_sys::CompositionEvent, mut runner_lock: MutexGuard<'_, AppRunner>| {
move |_event: web_sys::CompositionEvent, runner: &mut AppRunner| {
is_composing.set(true);
input_clone.set_value("");
runner_lock
.input
.raw
.events
.push(egui::Event::CompositionStart);
runner_lock.needs_repaint.repaint_asap();
runner.input.raw.events.push(egui::Event::CompositionStart);
runner.needs_repaint.repaint_asap();
}
})?;
runner_container.add_event_listener(
runner_ref.add_event_listener(
&input,
"compositionupdate",
move |event: web_sys::CompositionEvent, mut runner_lock: MutexGuard<'_, AppRunner>| {
move |event: web_sys::CompositionEvent, runner: &mut AppRunner| {
if let Some(event) = event.data().map(egui::Event::CompositionUpdate) {
runner_lock.input.raw.events.push(event);
runner_lock.needs_repaint.repaint_asap();
runner.input.raw.events.push(event);
runner.needs_repaint.repaint_asap();
}
},
)?;
runner_container.add_event_listener(&input, "compositionend", {
runner_ref.add_event_listener(&input, "compositionend", {
let input_clone = input.clone();
move |event: web_sys::CompositionEvent, mut runner_lock: MutexGuard<'_, AppRunner>| {
move |event: web_sys::CompositionEvent, runner: &mut AppRunner| {
is_composing.set(false);
input_clone.set_value("");
if let Some(event) = event.data().map(egui::Event::CompositionEnd) {
runner_lock.input.raw.events.push(event);
runner_lock.needs_repaint.repaint_asap();
runner.input.raw.events.push(event);
runner.needs_repaint.repaint_asap();
}
}
})?;
@ -105,20 +101,16 @@ pub fn install_text_agent(runner_container: &mut AppRunnerContainer) -> Result<(
// When input lost focus, focus on it again.
// It is useful when user click somewhere outside canvas.
runner_container.add_event_listener(
&input,
"focusout",
move |_event: web_sys::MouseEvent, _| {
// Delay 10 ms, and focus again.
let func = js_sys::Function::new_no_args(&format!(
"document.getElementById('{}').focus()",
AGENT_ID
));
window
.set_timeout_with_callback_and_timeout_and_arguments_0(&func, 10)
.unwrap();
},
)?;
runner_ref.add_event_listener(&input, "focusout", move |_event: web_sys::MouseEvent, _| {
// Delay 10 ms, and focus again.
let func = js_sys::Function::new_no_args(&format!(
"document.getElementById('{}').focus()",
AGENT_ID
));
window
.set_timeout_with_callback_and_timeout_and_arguments_0(&func, 10)
.unwrap();
})?;
body.append_child(&input)?;
@ -126,7 +118,7 @@ pub fn install_text_agent(runner_container: &mut AppRunnerContainer) -> Result<(
}
/// Focus or blur text agent to toggle mobile keyboard.
pub fn update_text_agent(runner: MutexGuard<'_, AppRunner>) -> Option<()> {
pub fn update_text_agent(runner: &mut AppRunner) -> Option<()> {
use web_sys::HtmlInputElement;
let window = web_sys::window()?;
let document = window.document()?;
@ -166,9 +158,6 @@ pub fn update_text_agent(runner: MutexGuard<'_, AppRunner>) -> Option<()> {
}
}
} else {
// Drop runner lock
drop(runner);
// Holding the runner lock while calling input.blur() causes a panic.
// This is most probably caused by the browser running the event handler
// for the triggered blur event synchronously, meaning that the mutex
@ -178,15 +167,33 @@ pub fn update_text_agent(runner: MutexGuard<'_, AppRunner>) -> Option<()> {
// and this apparently is the fix for it
//
// ¯\_(ツ)_/¯ - @DusterTheFirst
input.blur().ok()?;
input.set_hidden(true);
canvas_style.set_property("position", "absolute").ok()?;
canvas_style.set_property("top", "0%").ok()?; // move back to normal position
// So since we are inside a runner lock here, we just postpone the blur/hide:
call_after_delay(std::time::Duration::from_millis(0), move || {
input.blur().ok();
input.set_hidden(true);
canvas_style.set_property("position", "absolute").ok();
canvas_style.set_property("top", "0%").ok(); // move back to normal position
});
}
Some(())
}
fn call_after_delay(delay: std::time::Duration, f: impl FnOnce() + 'static) {
use wasm_bindgen::prelude::*;
let window = web_sys::window().unwrap();
let closure = Closure::once(f);
let delay_ms = delay.as_millis() as _;
window
.set_timeout_with_callback_and_timeout_and_arguments_0(
closure.as_ref().unchecked_ref(),
delay_ms,
)
.unwrap();
closure.forget(); // We must forget it, or else the callback is canceled on drop
}
/// If context is running under mobile device?
fn is_mobile() -> Option<bool> {
const MOBILE_DEVICE: [&str; 6] = ["Android", "iPhone", "iPad", "iPod", "webOS", "BlackBerry"];

View File

@ -108,11 +108,10 @@ impl BackendPanel {
ui.ctx().set_debug_on_hover(debug_on_hover);
}
ui.separator();
#[cfg(target_arch = "wasm32")]
#[cfg(feature = "web_screen-reader")]
{
ui.separator();
let mut screen_reader = ui.ctx().options(|o| o.screen_reader);
ui.checkbox(&mut screen_reader, "🔈 Screen reader").on_hover_text("Experimental feature: checking this will turn on the screen reader on supported platforms");
ui.ctx().options_mut(|o| o.screen_reader = screen_reader);
@ -125,6 +124,15 @@ impl BackendPanel {
frame.close();
}
}
if cfg!(debug_assertions) && cfg!(target_arch = "wasm32") {
ui.separator();
// For testing panic handling on web:
#[allow(clippy::manual_assert)]
if ui.button("panic!()").clicked() {
panic!("intentional panic!");
}
}
}
fn integration_ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {

View File

@ -3,12 +3,9 @@
mod apps;
mod backend_panel;
pub(crate) mod frame_history;
mod frame_history;
mod wrap_app;
#[cfg(target_arch = "wasm32")]
use eframe::web::AppRunnerRef;
pub use wrap_app::WrapApp;
/// Time of day as seconds since midnight. Used for clock in demo app.
@ -21,60 +18,7 @@ pub(crate) fn seconds_since_midnight() -> f64 {
// ----------------------------------------------------------------------------
#[cfg(target_arch = "wasm32")]
use eframe::wasm_bindgen::{self, prelude::*};
mod web;
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub struct WebHandle {
handle: AppRunnerRef,
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
impl WebHandle {
#[wasm_bindgen]
pub fn stop_web(&self) -> Result<(), wasm_bindgen::JsValue> {
let mut app = self.handle.lock();
app.destroy()
}
#[wasm_bindgen]
pub fn set_some_content_from_javascript(&mut self, _some_data: &str) {
let _app = self.handle.lock().app_mut::<WrapApp>();
// _app.data = some_data;
}
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub fn init_wasm_hooks() {
// Make sure panics are logged using `console.error`.
console_error_panic_hook::set_once();
// Redirect tracing to console.log and friends:
eframe::web::WebLogger::init(log::LevelFilter::Debug).ok();
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub async fn start_separate(canvas_id: &str) -> Result<WebHandle, wasm_bindgen::JsValue> {
let web_options = eframe::WebOptions::default();
eframe::start_web(
canvas_id,
web_options,
Box::new(|cc| Box::new(WrapApp::new(cc))),
)
.await
.map(|handle| WebHandle { handle })
}
/// This is the entry-point for all the web-assembly.
/// This is called once from the HTML.
/// It loads the app, installs some callbacks, then returns.
/// You can add more callbacks like this if you want to call in to your code.
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub async fn start(canvas_id: &str) -> Result<WebHandle, wasm_bindgen::JsValue> {
init_wasm_hooks();
start_separate(canvas_id).await
}
pub use web::*;

View File

@ -0,0 +1,57 @@
use eframe::{
wasm_bindgen::{self, prelude::*},
web::AppRunnerRef,
};
use crate::WrapApp;
#[wasm_bindgen]
pub struct WebHandle {
runner: AppRunnerRef,
}
#[wasm_bindgen]
impl WebHandle {
/// This is the entry-point for all the web-assembly.
///
/// This is called once from the HTML.
/// It loads the app, installs some callbacks, then returns.
#[wasm_bindgen(constructor)]
pub async fn new(canvas_id: &str) -> Result<WebHandle, wasm_bindgen::JsValue> {
// Redirect tracing to console.log and friends:
eframe::web::WebLogger::init(log::LevelFilter::Debug).ok();
// Make sure panics are logged using `console.error`.
console_error_panic_hook::set_once();
let web_options = eframe::WebOptions::default();
let runner = eframe::start_web(
canvas_id,
web_options,
Box::new(|cc| Box::new(WrapApp::new(cc))),
)
.await?;
Ok(WebHandle { runner })
}
#[wasm_bindgen]
pub fn destroy(&self) {
self.runner.destroy();
}
#[wasm_bindgen]
pub fn has_panicked(&self) -> bool {
self.runner.panic_summary().is_some()
}
#[wasm_bindgen]
pub fn panic_message(&self) -> Option<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())
}
}

View File

@ -91,6 +91,7 @@
transform: rotate(360deg);
}
}
</style>
</head>
@ -124,27 +125,58 @@
// We'll defer our execution until the wasm is ready to go.
// Here we tell bindgen the path to the wasm file so it can start
// initialization and return to us a promise when it's done.
console.debug("loading wasm…");
console.debug("Loading wasm…");
wasm_bindgen("./egui_demo_app_bg.wasm")
.then(on_wasm_loaded)
.catch(on_wasm_error);
.catch(on_error);
function on_wasm_loaded() {
console.debug("wasm loaded. starting app…");
console.debug("Wasm loaded. Starting app…");
// This call installs a bunch of callbacks and then returns:
const handle = wasm_bindgen.start("the_canvas_id");
// call `handle.stop_web()` to stop
// uncomment to quick result
// setTimeout(() => {handle.stop_web(); handle.free())}, 2000)
console.debug("app started.");
document.getElementById("center_text").remove();
let handle = new wasm_bindgen.WebHandle("the_canvas_id");
handle.then(on_app_started).catch(on_error);
}
function on_wasm_error(error) {
function on_app_started(handle) {
// Call `handle.destroy()` to stop. Uncomment to quick result:
// setTimeout(() => { handle.destroy(); handle.free()) }, 2000)
console.debug("App started.");
document.getElementById("center_text").innerHTML = '';
function check_for_panic() {
if (handle.has_panicked()) {
console.error("The egui app has crashed");
// The demo app already logs the panic message and callstack, but you
// can access them like this if you want to show them in the html:
// console.error(`${handle.panic_message()}`);
// console.error(`${handle.panic_callstack()}`);
document.getElementById("the_canvas_id").remove();
document.getElementById("center_text").innerHTML = `
<p>
The egui app has crashed.
</p>
<p style="font-size:14px">
See the console for details.
</p>
<p style="font-size:14px">
Reload the page to try again.
</p>`;
} else {
let delay_ms = 1000;
setTimeout(check_for_panic, delay_ms);
}
}
check_for_panic();
}
function on_error(error) {
console.error("Failed to start: " + error);
document.getElementById("the_canvas_id").remove();
document.getElementById("center_text").innerHTML = `
<p>
An error occurred during loading:

View File

@ -1,3 +1,4 @@
<!-- TODO(emilk): make this example nicer. The layout is HORRIBLE -->
<!DOCTYPE html>
<html>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
@ -17,10 +18,10 @@
/* Light mode background color for what is not covered by the egui canvas,
or where the egui canvas is translucent. */
background: #909090;
display:flex;
display: flex;
}
.canvas_wrap{
.canvas_wrap {
/* height: 200px; */
width: 400px;
}
@ -52,8 +53,8 @@
top: 0%;
left: 50%;
transform: translate(-50%, 0%); */
width:90%;
height:90%;
width: 45%;
height: 90%;
}
.centered {
@ -99,6 +100,7 @@
transform: rotate(360deg);
}
}
</style>
</head>
@ -108,19 +110,17 @@
<div>controls</div>
<button class="stop_one">
stop
stop
</button>
<div class="canvas_wrap one">
<canvas id="the_canvas_id_one"></canvas>
<canvas id="canvas_id_one"></canvas>
</div>
<div class="canvas_wrap two">
<canvas id="the_canvas_id_two"></canvas>
<div class="canvas_wrap two">
<canvas id="canvas_id_two"></canvas>
</div>
<div class="centered" id="center_text">
<p style="font-size:16px">
Loading…
@ -148,38 +148,41 @@
// We'll defer our execution until the wasm is ready to go.
// Here we tell bindgen the path to the wasm file so it can start
// initialization and return to us a promise when it's done.
console.debug("loading wasm…");
console.debug("Loading wasm…");
wasm_bindgen("./egui_demo_app_bg.wasm")
.then(on_wasm_loaded)
.catch(on_wasm_error);
.catch(on_error);
function on_wasm_loaded() {
console.debug("wasm loaded. starting app…");
console.debug("Wasm loaded. Starting apps…");
// This call installs a bunch of callbacks and then returns:
wasm_bindgen.init_wasm_hooks()
const handle_one = new wasm_bindgen.WebHandle("canvas_id_one");
const handle_two = new wasm_bindgen.WebHandle("canvas_id_two");
const handle_one = wasm_bindgen.start_separate("the_canvas_id_one");
const handle_two = wasm_bindgen.start_separate("the_canvas_id_two");
Promise.all([handle_one, handle_two]).then((handles) => {
on_apps_started(handles[0], handles[1])
}).catch(on_error);
}
function on_apps_started(handle_one, handle_two) {
const button = document.getElementsByClassName("stop_one")[0]
button.addEventListener("click", ()=>{
handle_one.stop_web()
button.addEventListener("click", () => {
document.getElementById("canvas_id_one").remove()
handle_one.destroy()
handle_one.free()
});
// call `handle.stop_web()` to stop
// uncomment to quick result
// setTimeout(() => {handle.stop_web()}, 2000)
// Call `handle.destroy()` to stop. Uncomment to quick result:
// setTimeout(() => { handle.destroy() }, 2000)
console.debug("app started.");
console.debug("Apps started.");
document.getElementById("center_text").remove();
}
function on_wasm_error(error) {
function on_error(error) {
console.error("Failed to start: " + error);
document.getElementById("center_text").innerHTML = `
<p>