Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Graceful exit from web #1650

Merged
merged 5 commits into from
Aug 2, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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%;
emilk marked this conversation as resolved.
Show resolved Hide resolved
}

/* 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`
enomado marked this conversation as resolved.
Show resolved Hide resolved
/// /// 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