1
0
mirror of https://github.com/emilk/egui.git synced 2026-09-01 06:10:06 -04:00

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
10 changed files with 703 additions and 650 deletions

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"];