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
|
// 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
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
/// 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)?;
|
|
||||||
|
|
||||||
|
// 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 {
|
if follow_system_theme {
|
||||||
super::events::install_color_scheme_change_event(&mut runner_container)?;
|
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
|
// Disable all event handlers on panic
|
||||||
let previous_hook = std::panic::take_hook();
|
let previous_hook = std::panic::take_hook();
|
||||||
|
let panic_handler = runner_ref.panic_handler.clone();
|
||||||
runner_container.runner.lock().events_to_unsubscribe = runner_container.events;
|
|
||||||
|
|
||||||
std::panic::set_hook(Box::new(move |panic_info| {
|
std::panic::set_hook(Box::new(move |panic_info| {
|
||||||
log::info!("egui disabled all event handlers due to panic");
|
log::info!("eframe detected a panic");
|
||||||
runner_container.panicked.store(true, SeqCst);
|
panic_handler.lock().on_panic(panic_info);
|
||||||
|
|
||||||
// Propagate panic info to the previously registered panic hook
|
// Propagate panic info to the previously registered panic hook
|
||||||
previous_hook(panic_info);
|
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::*;
|
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(runner_ref: AppRunnerRef) -> Result<(), JsValue> {
|
||||||
}
|
|
||||||
|
|
||||||
fn request_animation_frame(
|
|
||||||
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,
|
|
||||||
mut runner_lock: egui::mutex::MutexGuard<'_, AppRunner>| {
|
|
||||||
let has_focus = event_name == "focus";
|
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,
|
|
||||||
"hashchange",
|
|
||||||
|_: web_sys::Event, mut runner_lock| {
|
|
||||||
// `epi::Frame::info(&self)` clones `epi::IntegrationInfo`, but we need to modify the original here
|
// `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.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,190 +239,151 @@ 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,
|
|
||||||
mut _runner_lock: egui::mutex::MutexGuard<'_, AppRunner>| {
|
|
||||||
event.prevent_default();
|
event.prevent_default();
|
||||||
// event.stop_propagation();
|
// event.stop_propagation();
|
||||||
// log::debug!("Preventing event {event_name:?}");
|
// 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
|
|
||||||
.raw
|
|
||||||
.events
|
|
||||||
.push(egui::Event::PointerButton {
|
|
||||||
pos,
|
pos,
|
||||||
button,
|
button,
|
||||||
pressed: true,
|
pressed: true,
|
||||||
modifiers,
|
modifiers,
|
||||||
});
|
});
|
||||||
runner_lock.needs_repaint.repaint_asap();
|
runner.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,
|
|
||||||
"mouseup",
|
|
||||||
|event: web_sys::MouseEvent, mut runner_lock| {
|
|
||||||
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
|
|
||||||
.raw
|
|
||||||
.events
|
|
||||||
.push(egui::Event::PointerButton {
|
|
||||||
pos,
|
pos,
|
||||||
button,
|
button,
|
||||||
pressed: false,
|
pressed: false,
|
||||||
modifiers,
|
modifiers,
|
||||||
});
|
});
|
||||||
runner_lock.needs_repaint.repaint_asap();
|
runner.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
|
|
||||||
.input
|
|
||||||
.raw
|
|
||||||
.events
|
|
||||||
.push(egui::Event::PointerButton {
|
|
||||||
pos,
|
pos,
|
||||||
button: egui::PointerButton::Primary,
|
button: egui::PointerButton::Primary,
|
||||||
pressed: true,
|
pressed: true,
|
||||||
modifiers,
|
modifiers,
|
||||||
});
|
});
|
||||||
|
|
||||||
push_touches(&mut runner_lock, egui::TouchPhase::Start, &event);
|
push_touches(runner, egui::TouchPhase::Start, &event);
|
||||||
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,
|
||||||
"touchmove",
|
"touchmove",
|
||||||
|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);
|
runner.input.raw.events.push(egui::Event::PointerMoved(pos));
|
||||||
runner_lock
|
|
||||||
.input
|
|
||||||
.raw
|
|
||||||
.events
|
|
||||||
.push(egui::Event::PointerMoved(pos));
|
|
||||||
|
|
||||||
push_touches(&mut runner_lock, egui::TouchPhase::Move, &event);
|
push_touches(runner, egui::TouchPhase::Move, &event);
|
||||||
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, "touchend", |event: web_sys::TouchEvent, runner| {
|
||||||
&canvas,
|
if let Some(pos) = runner.input.latest_touch_pos {
|
||||||
"touchend",
|
let modifiers = runner.input.raw.modifiers;
|
||||||
|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:
|
// First release mouse to click:
|
||||||
runner_lock
|
runner.input.raw.events.push(egui::Event::PointerButton {
|
||||||
.input
|
|
||||||
.raw
|
|
||||||
.events
|
|
||||||
.push(egui::Event::PointerButton {
|
|
||||||
pos,
|
pos,
|
||||||
button: egui::PointerButton::Primary,
|
button: egui::PointerButton::Primary,
|
||||||
pressed: false,
|
pressed: false,
|
||||||
modifiers,
|
modifiers,
|
||||||
});
|
});
|
||||||
// Then remove hover effect:
|
// Then remove hover effect:
|
||||||
runner_lock.input.raw.events.push(egui::Event::PointerGone);
|
runner.input.raw.events.push(egui::Event::PointerGone);
|
||||||
|
|
||||||
push_touches(&mut runner_lock, egui::TouchPhase::End, &event);
|
push_touches(runner, egui::TouchPhase::End, &event);
|
||||||
runner_lock.needs_repaint.repaint_asap();
|
runner.needs_repaint.repaint_asap();
|
||||||
event.stop_propagation();
|
event.stop_propagation();
|
||||||
event.prevent_default();
|
event.prevent_default();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finally, focus or blur text agent to toggle mobile keyboard:
|
// Finally, focus or blur text agent to toggle mobile keyboard:
|
||||||
text_agent::update_text_agent(runner_lock);
|
text_agent::update_text_agent(runner);
|
||||||
},
|
})?;
|
||||||
)?;
|
|
||||||
|
|
||||||
runner_container.add_event_listener(
|
runner_ref.add_event_listener(
|
||||||
&canvas,
|
&canvas,
|
||||||
"touchcancel",
|
"touchcancel",
|
||||||
|event: web_sys::TouchEvent, mut runner_lock| {
|
|event: web_sys::TouchEvent, runner| {
|
||||||
push_touches(&mut runner_lock, egui::TouchPhase::Cancel, &event);
|
push_touches(runner, egui::TouchPhase::Cancel, &event);
|
||||||
event.stop_propagation();
|
event.stop_propagation();
|
||||||
event.prevent_default();
|
event.prevent_default();
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
runner_container.add_event_listener(
|
runner_ref.add_event_listener(&canvas, "wheel", |event: web_sys::WheelEvent, runner| {
|
||||||
&canvas,
|
|
||||||
"wheel",
|
|
||||||
|event: web_sys::WheelEvent, mut runner_lock| {
|
|
||||||
let unit = match event.delta_mode() {
|
let unit = match event.delta_mode() {
|
||||||
web_sys::WheelEvent::DOM_DELTA_PIXEL => egui::MouseWheelUnit::Point,
|
web_sys::WheelEvent::DOM_DELTA_PIXEL => egui::MouseWheelUnit::Point,
|
||||||
web_sys::WheelEvent::DOM_DELTA_LINE => egui::MouseWheelUnit::Line,
|
web_sys::WheelEvent::DOM_DELTA_LINE => egui::MouseWheelUnit::Line,
|
||||||
|
|
@ -472,16 +392,16 @@ pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Resul
|
||||||
};
|
};
|
||||||
// delta sign is flipped to match native (winit) convention.
|
// delta sign is flipped to match native (winit) convention.
|
||||||
let delta = -egui::vec2(event.delta_x() as f32, event.delta_y() as f32);
|
let delta = -egui::vec2(event.delta_x() as f32, event.delta_y() as f32);
|
||||||
let modifiers = runner_lock.input.raw.modifiers;
|
let modifiers = runner.input.raw.modifiers;
|
||||||
|
|
||||||
runner_lock.input.raw.events.push(egui::Event::MouseWheel {
|
runner.input.raw.events.push(egui::Event::MouseWheel {
|
||||||
unit,
|
unit,
|
||||||
delta,
|
delta,
|
||||||
modifiers,
|
modifiers,
|
||||||
});
|
});
|
||||||
|
|
||||||
let scroll_multiplier = match unit {
|
let scroll_multiplier = match unit {
|
||||||
egui::MouseWheelUnit::Page => canvas_size_in_points(runner_lock.canvas_id()).y,
|
egui::MouseWheelUnit::Page => canvas_size_in_points(runner.canvas_id()).y,
|
||||||
egui::MouseWheelUnit::Line => {
|
egui::MouseWheelUnit::Line => {
|
||||||
#[allow(clippy::let_and_return)]
|
#[allow(clippy::let_and_return)]
|
||||||
let points_per_scroll_line = 8.0; // Note that this is intentionally different from what we use in winit.
|
let points_per_scroll_line = 8.0; // Note that this is intentionally different from what we use in winit.
|
||||||
|
|
@ -497,7 +417,7 @@ pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Resul
|
||||||
// `modifiers_from_event()`, but we cannot directly use that fn for a [`WheelEvent`].
|
// `modifiers_from_event()`, but we cannot directly use that fn for a [`WheelEvent`].
|
||||||
if event.ctrl_key() || event.meta_key() {
|
if event.ctrl_key() || event.meta_key() {
|
||||||
let factor = (delta.y / 200.0).exp();
|
let factor = (delta.y / 200.0).exp();
|
||||||
runner_lock.input.raw.events.push(egui::Event::Zoom(factor));
|
runner.input.raw.events.push(egui::Event::Zoom(factor));
|
||||||
} else {
|
} else {
|
||||||
if event.shift_key() {
|
if event.shift_key() {
|
||||||
// Treat as horizontal scrolling.
|
// Treat as horizontal scrolling.
|
||||||
|
|
@ -505,60 +425,45 @@ pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Resul
|
||||||
delta = egui::vec2(delta.x + delta.y, 0.0);
|
delta = egui::vec2(delta.x + delta.y, 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
runner_lock
|
runner.input.raw.events.push(egui::Event::Scroll(delta));
|
||||||
.input
|
|
||||||
.raw
|
|
||||||
.events
|
|
||||||
.push(egui::Event::Scroll(delta));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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, "dragover", |event: web_sys::DragEvent, runner| {
|
||||||
&canvas,
|
|
||||||
"dragover",
|
|
||||||
|event: web_sys::DragEvent, mut runner_lock| {
|
|
||||||
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();
|
||||||
for i in 0..data_transfer.items().length() {
|
for i in 0..data_transfer.items().length() {
|
||||||
if let Some(item) = data_transfer.items().get(i) {
|
if let Some(item) = data_transfer.items().get(i) {
|
||||||
runner_lock.input.raw.hovered_files.push(egui::HoveredFile {
|
runner.input.raw.hovered_files.push(egui::HoveredFile {
|
||||||
mime: item.type_(),
|
mime: item.type_(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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, "dragleave", |event: web_sys::DragEvent, runner| {
|
||||||
&canvas,
|
runner.input.raw.hovered_files.clear();
|
||||||
"dragleave",
|
runner.needs_repaint.repaint_asap();
|
||||||
|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, "drop", {
|
||||||
let runner_ref = runner_container.runner.clone();
|
let runner_ref = runner_ref.clone();
|
||||||
|
|
||||||
move |event: web_sys::DragEvent, mut runner_lock| {
|
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,8 +483,7 @@ 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,
|
||||||
|
|
@ -590,6 +494,7 @@ pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Resul
|
||||||
);
|
);
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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::*;
|
||||||
|
|
|
||||||
|
|
@ -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,10 +101,7 @@ 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,
|
|
||||||
"focusout",
|
|
||||||
move |_event: web_sys::MouseEvent, _| {
|
|
||||||
// Delay 10 ms, and focus again.
|
// Delay 10 ms, and focus again.
|
||||||
let func = js_sys::Function::new_no_args(&format!(
|
let func = js_sys::Function::new_no_args(&format!(
|
||||||
"document.getElementById('{}').focus()",
|
"document.getElementById('{}').focus()",
|
||||||
|
|
@ -117,8 +110,7 @@ pub fn install_text_agent(runner_container: &mut AppRunnerContainer) -> Result<(
|
||||||
window
|
window
|
||||||
.set_timeout_with_callback_and_timeout_and_arguments_0(&func, 10)
|
.set_timeout_with_callback_and_timeout_and_arguments_0(&func, 10)
|
||||||
.unwrap();
|
.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()?;
|
|
||||||
|
|
||||||
|
// 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);
|
input.set_hidden(true);
|
||||||
canvas_style.set_property("position", "absolute").ok()?;
|
canvas_style.set_property("position", "absolute").ok();
|
||||||
canvas_style.set_property("top", "0%").ok()?; // move back to normal position
|
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"];
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
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:
|
||||||
|
|
|
||||||
|
|
@ -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" />
|
||||||
|
|
@ -52,7 +53,7 @@
|
||||||
top: 0%;
|
top: 0%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, 0%); */
|
transform: translate(-50%, 0%); */
|
||||||
width:90%;
|
width: 45%;
|
||||||
height: 90%;
|
height: 90%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -99,6 +100,7 @@
|
||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
@ -112,15 +114,13 @@
|
||||||
</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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue