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 // When compiling for web
#[cfg(target_arch = "wasm32")]
pub mod web;
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
pub use wasm_bindgen; pub use wasm_bindgen;
#[cfg(target_arch = "wasm32")]
use web::AppRunnerRef;
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
pub use web_sys; 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")] #[cfg(target_arch = "wasm32")]
pub async fn start_web( pub mod 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?;
Ok(handle) #[cfg(target_arch = "wasm32")]
} pub use web::start_web;
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// When compiling natively // When compiling natively

View File

@ -1,7 +1,6 @@
use egui::{ use std::{cell::RefCell, rc::Rc};
mutex::{Mutex, MutexGuard},
TexturesDelta, use egui::{mutex::Mutex, TexturesDelta};
};
use crate::{epi, App}; 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 struct AppRunner {
pub(crate) frame: epi::Frame, pub(crate) frame: epi::Frame,
egui_ctx: egui::Context, egui_ctx: egui::Context,
@ -183,7 +240,6 @@ pub struct AppRunner {
pub(crate) text_cursor_pos: Option<egui::Pos2>, pub(crate) text_cursor_pos: Option<egui::Pos2>,
pub(crate) mutable_text_under_cursor: bool, pub(crate) mutable_text_under_cursor: bool,
textures_delta: TexturesDelta, textures_delta: TexturesDelta,
pub events_to_unsubscribe: Vec<EventToUnsubscribe>,
} }
impl Drop for AppRunner { impl Drop for AppRunner {
@ -277,7 +333,6 @@ impl AppRunner {
text_cursor_pos: None, text_cursor_pos: None,
mutable_text_under_cursor: false, mutable_text_under_cursor: false,
textures_delta: Default::default(), textures_delta: Default::default(),
events_to_unsubscribe: Default::default(),
}; };
runner.input.raw.max_texture_side = Some(runner.painter.max_texture_side()); runner.input.raw.max_texture_side = Some(runner.painter.max_texture_side());
@ -333,21 +388,13 @@ impl AppRunner {
Ok(()) Ok(())
} }
pub fn destroy(&mut self) -> Result<(), JsValue> { fn destroy(&mut self) {
let is_destroyed_already = self.is_destroyed.fetch(); if self.is_destroyed.fetch() {
if is_destroyed_already {
log::warn!("App was destroyed already"); log::warn!("App was destroyed already");
Ok(())
} else { } else {
log::debug!("Destroying"); log::debug!("Destroying");
for x in self.events_to_unsubscribe.drain(..) {
x.unsubscribe()?;
}
self.painter.destroy(); self.painter.destroy();
self.is_destroyed.set_true(); 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 { pub struct TargetEvent {
target: EventTarget, target: EventTarget,
@ -454,7 +624,7 @@ pub struct IntervalHandle {
pub enum EventToUnsubscribe { pub enum EventToUnsubscribe {
TargetEvent(TargetEvent), TargetEvent(TargetEvent),
#[allow(dead_code)]
IntervalHandle(IntervalHandle), 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 /// Install event listeners to register different input events
/// and start running the given app. /// 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, canvas_id: &str,
web_options: crate::WebOptions, web_options: crate::WebOptions,
app_creator: epi::AppCreator, app_creator: epi::AppCreator,
@ -544,43 +695,36 @@ pub async fn start(
let mut runner = AppRunner::new(canvas_id, web_options, app_creator).await?; let mut runner = AppRunner::new(canvas_id, web_options, app_creator).await?;
runner.warm_up()?; runner.warm_up()?;
start_runner(runner, follow_system_theme) let runner_ref = AppRunnerRef::new(runner);
}
/// Install event listeners to register different input events // Install events:
/// and starts running the given [`AppRunner`]. {
fn start_runner(app_runner: AppRunner, follow_system_theme: bool) -> Result<AppRunnerRef, JsValue> { super::events::install_canvas_events(&runner_ref)?;
let mut runner_container = AppRunnerContainer { super::events::install_document_events(&runner_ref)?;
runner: Arc::new(Mutex::new(app_runner)), super::events::install_window_events(&runner_ref)?;
panicked: Arc::new(AtomicBool::new(false)), text_agent::install_text_agent(&runner_ref)?;
events: Vec::with_capacity(20), if follow_system_theme {
}; super::events::install_color_scheme_change_event(&runner_ref)?;
}
super::events::install_canvas_events(&mut runner_container)?; super::events::paint_and_schedule(&runner_ref)?;
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)?;
} }
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 std::panic::set_hook(Box::new(move |panic_info| {
let previous_hook = std::panic::take_hook(); 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| { Ok(runner_ref)
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)
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------

View File

@ -1,99 +1,80 @@
use std::sync::atomic::{AtomicBool, Ordering};
use egui::Key;
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( pub fn paint_and_schedule(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
runner_ref: &AppRunnerRef, fn paint_if_needed(runner: &mut AppRunner) -> Result<(), JsValue> {
panicked: Arc<AtomicBool>, if runner.needs_repaint.when_to_repaint() <= now_sec() {
) -> Result<(), JsValue> { runner.needs_repaint.clear();
struct IsDestroyed(pub bool); let (repaint_after, clipped_primitives) = runner.logic()?;
runner.paint(&clipped_primitives)?;
fn paint_if_needed(runner_ref: &AppRunnerRef) -> Result<IsDestroyed, JsValue> { runner
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
.needs_repaint .needs_repaint
.repaint_after(repaint_after.as_secs_f64()); .repaint_after(repaint_after.as_secs_f64());
runner_lock.auto_save_if_needed(); runner.auto_save_if_needed();
} }
Ok(())
Ok(IsDestroyed(is_destroyed))
} }
fn request_animation_frame( fn request_animation_frame(runner_ref: AppRunnerRef) -> Result<(), JsValue> {
runner_ref: AppRunnerRef,
panicked: Arc<AtomicBool>,
) -> Result<(), JsValue> {
let window = web_sys::window().unwrap(); 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())?; window.request_animation_frame(closure.as_ref().unchecked_ref())?;
closure.forget(); // We must forget it, or else the callback is canceled on drop closure.forget(); // We must forget it, or else the callback is canceled on drop
Ok(()) Ok(())
} }
// Only paint and schedule if there has been no panic // Only paint and schedule if there has been no panic
if !panicked.load(Ordering::SeqCst) { if let Some(mut runner_lock) = runner_ref.try_lock() {
let is_destroyed = paint_if_needed(runner_ref)?; paint_if_needed(&mut runner_lock)?;
if !is_destroyed.0 { drop(runner_lock);
request_animation_frame(runner_ref.clone(), panicked)?; request_animation_frame(runner_ref.clone())?;
}
} }
Ok(()) 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(); let document = web_sys::window().unwrap().document().unwrap();
{ {
// Avoid sticky modifier keys on alt-tab: // Avoid sticky modifier keys on alt-tab:
for event_name in ["blur", "focus"] { for event_name in ["blur", "focus"] {
let closure = let closure = move |_event: web_sys::MouseEvent, runner: &mut AppRunner| {
move |_event: web_sys::MouseEvent, let has_focus = event_name == "focus";
mut runner_lock: egui::mutex::MutexGuard<'_, AppRunner>| {
let has_focus = event_name == "focus";
if !has_focus { if !has_focus {
// We lost focus - good idea to save // We lost focus - good idea to save
runner_lock.save(); runner.save();
} }
runner_lock.input.on_web_page_focus_change(has_focus); runner.input.on_web_page_focus_change(has_focus);
runner_lock.egui_ctx().request_repaint(); runner.egui_ctx().request_repaint();
// log::debug!("{event_name:?}"); // 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, &document,
"keydown", "keydown",
|event: web_sys::KeyboardEvent, mut runner_lock| { |event: web_sys::KeyboardEvent, runner| {
if event.is_composing() || event.key_code() == 229 { 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/ // 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; return;
} }
let modifiers = modifiers_from_event(&event); let modifiers = modifiers_from_event(&event);
runner_lock.input.raw.modifiers = modifiers; runner.input.raw.modifiers = modifiers;
let key = event.key(); let key = event.key();
let egui_key = translate_key(&key); let egui_key = translate_key(&key);
if let Some(key) = egui_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, key,
pressed: true, pressed: true,
repeat: false, // egui will fill this in for us! 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. // When text agent is shown, it sends text event instead.
&& text_agent::text_agent().hidden() && 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)] #[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. // Always prevent moving cursor to url bar.
// egui wants to use tab to move to the next text field. // egui wants to use tab to move to the next text field.
true true
} else if egui_key == Some(Key::P) { } else if egui_key == Some(egui::Key::P) {
#[allow(clippy::needless_bool)] #[allow(clippy::needless_bool)]
if modifiers.ctrl || modifiers.command || modifiers.mac_cmd { 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. 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, &document,
"keyup", "keyup",
|event: web_sys::KeyboardEvent, mut runner_lock| { |event: web_sys::KeyboardEvent, runner| {
let modifiers = modifiers_from_event(&event); 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()) { 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, key,
pressed: false, pressed: false,
repeat: false, repeat: false,
modifiers, modifiers,
}); });
} }
runner_lock.needs_repaint.repaint_asap(); runner.needs_repaint.repaint_asap();
}, },
)?; )?;
#[cfg(web_sys_unstable_apis)] #[cfg(web_sys_unstable_apis)]
runner_container.add_event_listener( runner_ref.add_event_listener(
&document, &document,
"paste", "paste",
|event: web_sys::ClipboardEvent, mut runner_lock| { |event: web_sys::ClipboardEvent, runner| {
if let Some(data) = event.clipboard_data() { if let Some(data) = event.clipboard_data() {
if let Ok(text) = data.get_data("text") { if let Ok(text) = data.get_data("text") {
let text = text.replace("\r\n", "\n"); let text = text.replace("\r\n", "\n");
if !text.is_empty() { if !text.is_empty() {
runner_lock.input.raw.events.push(egui::Event::Paste(text)); runner.input.raw.events.push(egui::Event::Paste(text));
runner_lock.needs_repaint.repaint_asap(); runner.needs_repaint.repaint_asap();
} }
event.stop_propagation(); event.stop_propagation();
event.prevent_default(); event.prevent_default();
@ -190,76 +171,54 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res
)?; )?;
#[cfg(web_sys_unstable_apis)] #[cfg(web_sys_unstable_apis)]
runner_container.add_event_listener( runner_ref.add_event_listener(&document, "cut", |_: web_sys::ClipboardEvent, runner| {
&document, runner.input.raw.events.push(egui::Event::Cut);
"cut", runner.needs_repaint.repaint_asap();
|_: web_sys::ClipboardEvent, mut runner_lock| { })?;
runner_lock.input.raw.events.push(egui::Event::Cut);
runner_lock.needs_repaint.repaint_asap();
},
)?;
#[cfg(web_sys_unstable_apis)] #[cfg(web_sys_unstable_apis)]
runner_container.add_event_listener( runner_ref.add_event_listener(&document, "copy", |_: web_sys::ClipboardEvent, runner| {
&document, runner.input.raw.events.push(egui::Event::Copy);
"copy", runner.needs_repaint.repaint_asap();
|_: web_sys::ClipboardEvent, mut runner_lock| { })?;
runner_lock.input.raw.events.push(egui::Event::Copy);
runner_lock.needs_repaint.repaint_asap();
},
)?;
Ok(()) 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(); let window = web_sys::window().unwrap();
// Save-on-close // Save-on-close
runner_container.add_event_listener( runner_ref.add_event_listener(&window, "onbeforeunload", |_: web_sys::Event, runner| {
&window, runner.save();
"onbeforeunload", })?;
|_: web_sys::Event, mut runner_lock| {
runner_lock.save();
},
)?;
for event_name in &["load", "pagehide", "pageshow", "resize"] { for event_name in &["load", "pagehide", "pageshow", "resize"] {
runner_container.add_event_listener( runner_ref.add_event_listener(&window, event_name, |_: web_sys::Event, runner| {
&window, runner.needs_repaint.repaint_asap();
event_name, })?;
|_: web_sys::Event, runner_lock| {
runner_lock.needs_repaint.repaint_asap();
},
)?;
} }
runner_container.add_event_listener( runner_ref.add_event_listener(&window, "hashchange", |_: web_sys::Event, runner| {
&window, // `epi::Frame::info(&self)` clones `epi::IntegrationInfo`, but we need to modify the original here
"hashchange", runner.frame.info.web_info.location.hash = location_hash();
|_: 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();
},
)?;
Ok(()) Ok(())
} }
pub fn install_color_scheme_change_event( pub fn install_color_scheme_change_event(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
runner_container: &mut AppRunnerContainer,
) -> 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)? {
runner_container.add_event_listener::<web_sys::MediaQueryListEvent>( runner_ref.add_event_listener::<web_sys::MediaQueryListEvent>(
&media_query_list, &media_query_list,
"change", "change",
|event, mut runner_lock| { |event, runner| {
let theme = theme_from_dark_mode(event.matches()); let theme = theme_from_dark_mode(event.matches());
runner_lock.frame.info.system_theme = Some(theme); runner.frame.info.system_theme = Some(theme);
runner_lock.egui_ctx().set_visuals(theme.egui_visuals()); runner.egui_ctx().set_visuals(theme.egui_visuals());
runner_lock.needs_repaint.repaint_asap(); runner.needs_repaint.repaint_asap();
}, },
)?; )?;
} }
@ -267,8 +226,8 @@ pub fn install_color_scheme_change_event(
Ok(()) Ok(())
} }
pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> { pub fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
let canvas = canvas_element(runner_container.runner.lock().canvas_id()).unwrap(); let canvas = canvas_element(runner_ref.try_lock().unwrap().canvas_id()).unwrap();
{ {
let prevent_default_events = [ 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 { for event_name in prevent_default_events {
let closure = let closure = move |event: web_sys::MouseEvent, _runner: &mut AppRunner| {
move |event: web_sys::MouseEvent, event.prevent_default();
mut _runner_lock: egui::mutex::MutexGuard<'_, AppRunner>| { // event.stop_propagation();
event.prevent_default(); // log::debug!("Preventing event {event_name:?}");
// 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, &canvas,
"mousedown", "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) { if let Some(button) = button_from_mouse_event(&event) {
let pos = pos_from_mouse_event(runner_lock.canvas_id(), &event); let pos = pos_from_mouse_event(runner.canvas_id(), &event);
let modifiers = runner_lock.input.raw.modifiers; let modifiers = runner.input.raw.modifiers;
runner_lock runner.input.raw.events.push(egui::Event::PointerButton {
.input pos,
.raw button,
.events pressed: true,
.push(egui::Event::PointerButton { modifiers,
pos, });
button, runner.needs_repaint.repaint_asap();
pressed: true,
modifiers,
});
runner_lock.needs_repaint.repaint_asap();
} }
event.stop_propagation(); event.stop_propagation();
// Note: prevent_default breaks VSCode tab focusing, hence why we don't call it here. // 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, &canvas,
"mousemove", "mousemove",
|event: web_sys::MouseEvent, mut runner_lock| { |event: web_sys::MouseEvent, runner| {
let pos = pos_from_mouse_event(runner_lock.canvas_id(), &event); let pos = pos_from_mouse_event(runner.canvas_id(), &event);
runner_lock runner.input.raw.events.push(egui::Event::PointerMoved(pos));
.input runner.needs_repaint.repaint_asap();
.raw
.events
.push(egui::Event::PointerMoved(pos));
runner_lock.needs_repaint.repaint_asap();
event.stop_propagation(); event.stop_propagation();
event.prevent_default(); event.prevent_default();
}, },
)?; )?;
runner_container.add_event_listener( runner_ref.add_event_listener(&canvas, "mouseup", |event: web_sys::MouseEvent, runner| {
&canvas, if let Some(button) = button_from_mouse_event(&event) {
"mouseup", let pos = pos_from_mouse_event(runner.canvas_id(), &event);
|event: web_sys::MouseEvent, mut runner_lock| { let modifiers = runner.input.raw.modifiers;
if let Some(button) = button_from_mouse_event(&event) { runner.input.raw.events.push(egui::Event::PointerButton {
let pos = pos_from_mouse_event(runner_lock.canvas_id(), &event); pos,
let modifiers = runner_lock.input.raw.modifiers; button,
runner_lock pressed: false,
.input modifiers,
.raw });
.events runner.needs_repaint.repaint_asap();
.push(egui::Event::PointerButton {
pos,
button,
pressed: false,
modifiers,
});
runner_lock.needs_repaint.repaint_asap();
text_agent::update_text_agent(runner_lock); text_agent::update_text_agent(runner);
} }
event.stop_propagation(); event.stop_propagation();
event.prevent_default(); event.prevent_default();
}, })?;
)?;
runner_container.add_event_listener( runner_ref.add_event_listener(
&canvas, &canvas,
"mouseleave", "mouseleave",
|event: web_sys::MouseEvent, mut runner_lock| { |event: web_sys::MouseEvent, runner| {
runner_lock.input.raw.events.push(egui::Event::PointerGone); runner.input.raw.events.push(egui::Event::PointerGone);
runner_lock.needs_repaint.repaint_asap(); runner.needs_repaint.repaint_asap();
event.stop_propagation(); event.stop_propagation();
event.prevent_default(); event.prevent_default();
}, },
)?; )?;
runner_container.add_event_listener( runner_ref.add_event_listener(
&canvas, &canvas,
"touchstart", "touchstart",
|event: web_sys::TouchEvent, mut runner_lock| { |event: web_sys::TouchEvent, runner| {
let mut latest_touch_pos_id = runner_lock.input.latest_touch_pos_id; let mut latest_touch_pos_id = runner.input.latest_touch_pos_id;
let pos = let pos = pos_from_touch_event(runner.canvas_id(), &event, &mut latest_touch_pos_id);
pos_from_touch_event(runner_lock.canvas_id(), &event, &mut latest_touch_pos_id); runner.input.latest_touch_pos_id = latest_touch_pos_id;
runner_lock.input.latest_touch_pos_id = latest_touch_pos_id; runner.input.latest_touch_pos = Some(pos);
runner_lock.input.latest_touch_pos = Some(pos); let modifiers = runner.input.raw.modifiers;
let modifiers = runner_lock.input.raw.modifiers; runner.input.raw.events.push(egui::Event::PointerButton {
runner_lock pos,
.input button: egui::PointerButton::Primary,
.raw pressed: true,
.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,
modifiers, modifiers,
}); });
let scroll_multiplier = match unit { push_touches(runner, egui::TouchPhase::Start, &event);
egui::MouseWheelUnit::Page => canvas_size_in_points(runner_lock.canvas_id()).y, runner.needs_repaint.repaint_asap();
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();
event.stop_propagation(); event.stop_propagation();
event.prevent_default(); event.prevent_default();
}, },
)?; )?;
runner_container.add_event_listener( runner_ref.add_event_listener(
&canvas, &canvas,
"dragover", "touchmove",
|event: web_sys::DragEvent, mut runner_lock| { |event: web_sys::TouchEvent, runner| {
if let Some(data_transfer) = event.data_transfer() { let mut latest_touch_pos_id = runner.input.latest_touch_pos_id;
runner_lock.input.raw.hovered_files.clear(); let pos = pos_from_touch_event(runner.canvas_id(), &event, &mut latest_touch_pos_id);
for i in 0..data_transfer.items().length() { runner.input.latest_touch_pos_id = latest_touch_pos_id;
if let Some(item) = data_transfer.items().get(i) { runner.input.latest_touch_pos = Some(pos);
runner_lock.input.raw.hovered_files.push(egui::HoveredFile { runner.input.raw.events.push(egui::Event::PointerMoved(pos));
mime: item.type_(),
..Default::default()
});
}
}
runner_lock.needs_repaint.repaint_asap();
event.stop_propagation();
event.prevent_default();
}
},
)?;
runner_container.add_event_listener( push_touches(runner, egui::TouchPhase::Move, &event);
&canvas, runner.needs_repaint.repaint_asap();
"dragleave",
|event: web_sys::DragEvent, mut runner_lock| {
runner_lock.input.raw.hovered_files.clear();
runner_lock.needs_repaint.repaint_asap();
event.stop_propagation(); event.stop_propagation();
event.prevent_default(); event.prevent_default();
}, },
)?; )?;
runner_container.add_event_listener(&canvas, "drop", { runner_ref.add_event_listener(&canvas, "touchend", |event: web_sys::TouchEvent, runner| {
let runner_ref = runner_container.runner.clone(); 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() { if let Some(data_transfer) = event.data_transfer() {
runner_lock.input.raw.hovered_files.clear(); runner.input.raw.hovered_files.clear();
runner_lock.needs_repaint.repaint_asap(); runner.needs_repaint.repaint_asap();
// Unlock the runner so it can be locked after a future await point
drop(runner_lock);
if let Some(files) = data_transfer.files() { if let Some(files) = data_transfer.files() {
for i in 0..files.length() { 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(); let bytes = js_sys::Uint8Array::new(&array_buffer).to_vec();
log::debug!("Loaded {:?} ({} bytes).", name, bytes.len()); log::debug!("Loaded {:?} ({} bytes).", name, bytes.len());
// Re-lock the mutex on the other side of the await point if let Some(mut runner_lock) = runner_ref.try_lock() {
let mut runner_lock = runner_ref.lock(); runner_lock.input.raw.dropped_files.push(
runner_lock.input.raw.dropped_files.push( egui::DroppedFile {
egui::DroppedFile { name,
name, last_modified: Some(last_modified),
last_modified: Some(last_modified), bytes: Some(bytes.into()),
bytes: Some(bytes.into()), ..Default::default()
..Default::default() },
}, );
); runner_lock.needs_repaint.repaint_asap();
runner_lock.needs_repaint.repaint_asap(); }
} }
Err(err) => { Err(err) => {
log::error!("Failed to read file: {:?}", err); log::error!("Failed to read file: {:?}", err);

View File

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

View File

@ -1,12 +1,12 @@
//! The text agent is an `<input>` element used to trigger //! The text agent is an `<input>` element used to trigger
//! mobile keyboard and IME input. //! 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 wasm_bindgen::prelude::*;
use super::{canvas_element, AppRunner, AppRunnerRef};
static AGENT_ID: &str = "egui_text_agent"; static AGENT_ID: &str = "egui_text_agent";
pub fn text_agent() -> web_sys::HtmlInputElement { pub fn text_agent() -> web_sys::HtmlInputElement {
@ -21,7 +21,7 @@ pub fn text_agent() -> web_sys::HtmlInputElement {
} }
/// Text event handler, /// 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 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");
@ -44,60 +44,56 @@ pub fn install_text_agent(runner_container: &mut AppRunnerContainer) -> Result<(
input.set_hidden(true); input.set_hidden(true);
// When IME is off // When IME is off
runner_container.add_event_listener(&input, "input", { runner_ref.add_event_listener(&input, "input", {
let input_clone = input.clone(); let input_clone = input.clone();
let is_composing = is_composing.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(); let text = input_clone.value();
if !text.is_empty() && !is_composing.get() { if !text.is_empty() && !is_composing.get() {
input_clone.set_value(""); input_clone.set_value("");
runner_lock.input.raw.events.push(egui::Event::Text(text)); runner.input.raw.events.push(egui::Event::Text(text));
runner_lock.needs_repaint.repaint_asap(); runner.needs_repaint.repaint_asap();
} }
} }
})?; })?;
{ {
// When IME is on, handle composition event // 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 input_clone = input.clone();
let is_composing = is_composing.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); is_composing.set(true);
input_clone.set_value(""); input_clone.set_value("");
runner_lock runner.input.raw.events.push(egui::Event::CompositionStart);
.input runner.needs_repaint.repaint_asap();
.raw
.events
.push(egui::Event::CompositionStart);
runner_lock.needs_repaint.repaint_asap();
} }
})?; })?;
runner_container.add_event_listener( runner_ref.add_event_listener(
&input, &input,
"compositionupdate", "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) { if let Some(event) = event.data().map(egui::Event::CompositionUpdate) {
runner_lock.input.raw.events.push(event); runner.input.raw.events.push(event);
runner_lock.needs_repaint.repaint_asap(); runner.needs_repaint.repaint_asap();
} }
}, },
)?; )?;
runner_container.add_event_listener(&input, "compositionend", { runner_ref.add_event_listener(&input, "compositionend", {
let input_clone = input.clone(); 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); is_composing.set(false);
input_clone.set_value(""); input_clone.set_value("");
if let Some(event) = event.data().map(egui::Event::CompositionEnd) { if let Some(event) = event.data().map(egui::Event::CompositionEnd) {
runner_lock.input.raw.events.push(event); runner.input.raw.events.push(event);
runner_lock.needs_repaint.repaint_asap(); 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. // When input lost focus, focus on it again.
// It is useful when user click somewhere outside canvas. // It is useful when user click somewhere outside canvas.
runner_container.add_event_listener( runner_ref.add_event_listener(&input, "focusout", move |_event: web_sys::MouseEvent, _| {
&input, // Delay 10 ms, and focus again.
"focusout", let func = js_sys::Function::new_no_args(&format!(
move |_event: web_sys::MouseEvent, _| { "document.getElementById('{}').focus()",
// Delay 10 ms, and focus again. AGENT_ID
let func = js_sys::Function::new_no_args(&format!( ));
"document.getElementById('{}').focus()", window
AGENT_ID .set_timeout_with_callback_and_timeout_and_arguments_0(&func, 10)
)); .unwrap();
window })?;
.set_timeout_with_callback_and_timeout_and_arguments_0(&func, 10)
.unwrap();
},
)?;
body.append_child(&input)?; 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. /// 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; use web_sys::HtmlInputElement;
let window = web_sys::window()?; let window = web_sys::window()?;
let document = window.document()?; let document = window.document()?;
@ -166,9 +158,6 @@ pub fn update_text_agent(runner: MutexGuard<'_, AppRunner>) -> Option<()> {
} }
} }
} else { } else {
// Drop runner lock
drop(runner);
// Holding the runner lock while calling input.blur() causes a panic. // Holding the runner lock while calling input.blur() causes a panic.
// This is most probably caused by the browser running the event handler // This is most probably caused by the browser running the event handler
// for the triggered blur event synchronously, meaning that the mutex // 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 // and this apparently is the fix for it
// //
// ¯\_(ツ)_/¯ - @DusterTheFirst // ¯\_(ツ)_/¯ - @DusterTheFirst
input.blur().ok()?;
input.set_hidden(true); // So since we are inside a runner lock here, we just postpone the blur/hide:
canvas_style.set_property("position", "absolute").ok()?;
canvas_style.set_property("top", "0%").ok()?; // move back to normal position 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(()) 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? /// If context is running under mobile device?
fn is_mobile() -> Option<bool> { fn is_mobile() -> Option<bool> {
const MOBILE_DEVICE: [&str; 6] = ["Android", "iPhone", "iPad", "iPod", "webOS", "BlackBerry"]; 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.ctx().set_debug_on_hover(debug_on_hover);
} }
ui.separator();
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
#[cfg(feature = "web_screen-reader")] #[cfg(feature = "web_screen-reader")]
{ {
ui.separator();
let mut screen_reader = ui.ctx().options(|o| o.screen_reader); 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.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); ui.ctx().options_mut(|o| o.screen_reader = screen_reader);
@ -125,6 +124,15 @@ impl BackendPanel {
frame.close(); 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) { fn integration_ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {

View File

@ -3,12 +3,9 @@
mod apps; mod apps;
mod backend_panel; mod backend_panel;
pub(crate) mod frame_history; mod frame_history;
mod wrap_app; mod wrap_app;
#[cfg(target_arch = "wasm32")]
use eframe::web::AppRunnerRef;
pub use wrap_app::WrapApp; pub use wrap_app::WrapApp;
/// Time of day as seconds since midnight. Used for clock in demo app. /// 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")] #[cfg(target_arch = "wasm32")]
use eframe::wasm_bindgen::{self, prelude::*}; mod web;
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
#[wasm_bindgen] pub use web::*;
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
}

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); transform: rotate(360deg);
} }
} }
</style> </style>
</head> </head>
@ -124,27 +125,58 @@
// We'll defer our execution until the wasm is ready to go. // 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 // 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. // 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") wasm_bindgen("./egui_demo_app_bg.wasm")
.then(on_wasm_loaded) .then(on_wasm_loaded)
.catch(on_wasm_error); .catch(on_error);
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: // This call installs a bunch of callbacks and then returns:
const handle = wasm_bindgen.start("the_canvas_id"); let handle = new wasm_bindgen.WebHandle("the_canvas_id");
handle.then(on_app_started).catch(on_error);
// 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();
} }
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); console.error("Failed to start: " + error);
document.getElementById("the_canvas_id").remove();
document.getElementById("center_text").innerHTML = ` document.getElementById("center_text").innerHTML = `
<p> <p>
An error occurred during loading: An error occurred during loading:

View File

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