Skip to content

Commit

Permalink
Wasm graceful exit
Browse files Browse the repository at this point in the history
  • Loading branch information
enomado committed Jul 30, 2022
1 parent 235d777 commit 85568ca
Show file tree
Hide file tree
Showing 8 changed files with 202 additions and 38 deletions.
2 changes: 2 additions & 0 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
overflow: hidden;
margin: 0 !important;
padding: 0 !important;
height: 100%;
width: 100%;
}

/* Position canvas in center-top: */
Expand Down
22 changes: 16 additions & 6 deletions eframe/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,19 @@ pub use epi::*;
// When compiling for web

#[cfg(target_arch = "wasm32")]
mod web;
use egui::mutex::Mutex;

#[cfg(target_arch = "wasm32")]
use std::sync::Arc;

#[cfg(target_arch = "wasm32")]
pub mod web;

#[cfg(target_arch = "wasm32")]
pub use wasm_bindgen;

#[cfg(target_arch = "wasm32")]
use web::AppRunner;
#[cfg(target_arch = "wasm32")]
pub use web_sys;

Expand All @@ -93,12 +101,13 @@ pub use web_sys;
/// use wasm_bindgen::prelude::*;
///
/// /// This is the entry-point for all the web-assembly.
/// /// This is called once from the HTML.
/// /// This is called from the HTML.
/// /// It loads the app, installs some callbacks, then returns.
/// /// It creates singleton app-handle that could be stopped calling `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 fn start(canvas_id: &str) -> Result<(), eframe::wasm_bindgen::JsValue> {
/// pub fn start(canvas_id: &str) -> Result<Arc<Mutex<AppRunner>>, 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))))
/// }
Expand All @@ -108,9 +117,10 @@ pub fn start_web(
canvas_id: &str,
web_options: WebOptions,
app_creator: AppCreator,
) -> Result<(), wasm_bindgen::JsValue> {
web::start(canvas_id, web_options, app_creator)?;
Ok(())
) -> Result<Arc<Mutex<AppRunner>>, wasm_bindgen::JsValue> {
let handle = web::start(canvas_id, web_options, app_creator)?;

Ok(handle)
}

// ----------------------------------------------------------------------------
Expand Down
122 changes: 109 additions & 13 deletions eframe/src/web/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ use super::{glow_wrapping::WrappedGlowPainter, *};

use crate::epi;

use egui::mutex::{Mutex, MutexGuard};
use egui::TexturesDelta;

use egui::{
mutex::{Mutex, MutexGuard},
TexturesDelta,
};
pub use egui::{pos2, Color32};

// ----------------------------------------------------------------------------
Expand Down Expand Up @@ -67,6 +68,24 @@ impl NeedRepaint {
}
}

pub struct IsDestroyed(std::sync::atomic::AtomicBool);

impl Default for IsDestroyed {
fn default() -> Self {
Self(false.into())
}
}

impl IsDestroyed {
pub fn fetch(&self) -> bool {
self.0.load(SeqCst)
}

pub fn set_true(&self) {
self.0.store(true, SeqCst);
}
}

// ----------------------------------------------------------------------------

fn web_location() -> epi::Location {
Expand Down Expand Up @@ -147,11 +166,19 @@ pub struct AppRunner {
pub(crate) input: WebInput,
app: Box<dyn epi::App>,
pub(crate) needs_repaint: std::sync::Arc<NeedRepaint>,
pub(crate) is_destroyed: std::sync::Arc<IsDestroyed>,
last_save_time: f64,
screen_reader: super::screen_reader::ScreenReader,
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 {
fn drop(&mut self) {
tracing::debug!("AppRunner has fully dropped");
}
}

impl AppRunner {
Expand Down Expand Up @@ -220,11 +247,13 @@ impl AppRunner {
input: Default::default(),
app,
needs_repaint,
is_destroyed: Default::default(),
last_save_time: now_sec(),
screen_reader: Default::default(),
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());
Expand Down Expand Up @@ -266,6 +295,24 @@ impl AppRunner {
Ok(())
}

pub fn destroy(&mut self) -> Result<(), JsValue> {
let is_destroyed_already = self.is_destroyed.fetch();

if is_destroyed_already {
tracing::warn!("App was destroyed already");
Ok(())
} else {
tracing::debug!("Destroying");
for x in self.events_to_unsubscribe.drain(..) {
x.unsubscribe()?;
}

self.painter.destroy();
self.is_destroyed.set_true();
Ok(())
}
}

/// Returns how long to wait until the next repaint.
///
/// Call [`Self::paint`] later to paint
Expand Down Expand Up @@ -358,18 +405,59 @@ impl AppRunner {

pub type AppRunnerRef = Arc<Mutex<AppRunner>>;

pub struct TargetEvent {
target: EventTarget,
event_name: String,
closure: Closure<dyn FnMut(web_sys::Event)>,
}

pub struct IntervalHandle {
pub handle: i32,
pub closure: Closure<dyn FnMut()>,
}

pub enum EventToUnsubscribe {
TargetEvent(TargetEvent),
#[allow(dead_code)]
IntervalHandle(IntervalHandle),
}

impl EventToUnsubscribe {
pub fn unsubscribe(self) -> Result<(), JsValue> {
use wasm_bindgen::JsCast;

match self {
EventToUnsubscribe::TargetEvent(handle) => {
handle.target.remove_event_listener_with_callback(
handle.event_name.as_str(),
handle.closure.as_ref().unchecked_ref(),
)?;
Ok(())
}
EventToUnsubscribe::IntervalHandle(handle) => {
let window = web_sys::window().unwrap();
window.clear_interval_with_handle(handle.handle);
Ok(())
}
}
}
}
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
///

#[must_use]
pub fn add_event_listener<E: wasm_bindgen::JsCast>(
&self,
&mut self,
target: &EventTarget,
event_name: &'static str,
mut closure: impl FnMut(E, MutexGuard<'_, AppRunner>) + 'static,
Expand All @@ -390,14 +478,19 @@ impl AppRunnerContainer {

closure(event, runner_ref.lock());
}
}) as Box<dyn FnMut(_)>
}) 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())?;

// Bypass closure drop so that event handler can call the closure
closure.forget();
let handle = TargetEvent {
target: target.clone(),
event_name: event_name.to_string(),
closure,
};

self.events.push(EventToUnsubscribe::TargetEvent(handle));

Ok(())
}
Expand All @@ -420,23 +513,26 @@ pub fn start(
/// Install event listeners to register different input events
/// and starts running the given [`AppRunner`].
fn start_runner(app_runner: AppRunner) -> Result<AppRunnerRef, JsValue> {
let runner_container = AppRunnerContainer {
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(&runner_container)?;
super::events::install_document_events(&runner_container)?;
text_agent::install_text_agent(&runner_container)?;
super::events::install_canvas_events(&mut runner_container)?;
super::events::install_document_events(&mut runner_container)?;
text_agent::install_text_agent(&mut runner_container)?;

super::events::paint_and_schedule(&runner_container.runner, runner_container.panicked.clone())?;

// Disable all event handlers on panic
let previous_hook = std::panic::take_hook();
let panicked = runner_container.panicked;

runner_container.runner.lock().events_to_unsubscribe = runner_container.events;

std::panic::set_hook(Box::new(move |panic_info| {
tracing::info!("egui disabled all event handlers due to panic");
panicked.store(true, SeqCst);
runner_container.panicked.store(true, SeqCst);

// Propagate panic info to the previously registered panic hook
previous_hook(panic_info);
Expand Down
36 changes: 22 additions & 14 deletions eframe/src/web/events.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
use super::*;
use std::sync::atomic::{AtomicBool, Ordering};

struct IsDestroyed(pub bool);

pub fn paint_and_schedule(
runner_ref: &AppRunnerRef,
panicked: Arc<AtomicBool>,
) -> Result<(), JsValue> {
fn paint_if_needed(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
fn paint_if_needed(runner_ref: &AppRunnerRef) -> Result<IsDestroyed, JsValue> {
let mut runner_lock = runner_ref.lock();
if runner_lock.needs_repaint.when_to_repaint() <= now_sec() {
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();
runner_lock.clear_color_buffer();
let (repaint_after, clipped_primitives) = runner_lock.logic()?;
Expand All @@ -18,7 +22,7 @@ pub fn paint_and_schedule(
runner_lock.auto_save();
}

Ok(())
Ok(IsDestroyed(is_destroyed))
}

fn request_animation_frame(
Expand All @@ -35,14 +39,16 @@ pub fn paint_and_schedule(

// Only paint and schedule if there has been no panic
if !panicked.load(Ordering::SeqCst) {
paint_if_needed(runner_ref)?;
request_animation_frame(runner_ref.clone(), panicked)?;
let is_destroyed = paint_if_needed(runner_ref)?;
if !is_destroyed.0 {
request_animation_frame(runner_ref.clone(), panicked)?;
}
}

Ok(())
}

pub fn install_document_events(runner_container: &AppRunnerContainer) -> Result<(), JsValue> {
pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();

Expand Down Expand Up @@ -188,25 +194,27 @@ pub fn install_document_events(runner_container: &AppRunnerContainer) -> Result<
Ok(())
}

pub fn install_canvas_events(runner_container: &AppRunnerContainer) -> Result<(), JsValue> {
use wasm_bindgen::JsCast;
pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> {
let canvas = canvas_element(runner_container.runner.lock().canvas_id()).unwrap();

{
// By default, right-clicks open a context menu.
// We don't want to do that (right clicks is handled by egui):
let event_name = "contextmenu";
let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
event.prevent_default();
}) as Box<dyn FnMut(_)>);
canvas.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?;
closure.forget();

let closure =
move |event: web_sys::MouseEvent,
mut _runner_lock: egui::mutex::MutexGuard<AppRunner>| {
event.prevent_default();
};

runner_container.add_event_listener(&canvas, event_name, closure)?;
}

runner_container.add_event_listener(
&canvas,
"mousedown",
|event: web_sys::MouseEvent, mut runner_lock| {
|event: web_sys::MouseEvent, mut runner_lock: egui::mutex::MutexGuard<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;
Expand Down
4 changes: 4 additions & 0 deletions eframe/src/web/glow_wrapping.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ impl WrappedGlowPainter {

Ok(())
}

pub fn destroy(&mut self) {
self.painter.destroy()
}
}

/// Returns glow context and shader prefix.
Expand Down
Loading

0 comments on commit 85568ca

Please sign in to comment.