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:
parent
7f2de426d2
commit
ac50fa0d94
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
|
|
|
|||
|
|
@ -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"];
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue