diff --git a/docs/index.html b/docs/index.html
index 648ba74c..e228ed34 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -33,6 +33,8 @@
overflow: hidden;
margin: 0 !important;
padding: 0 !important;
+ height: 100%;
+ width: 100%;
}
/* Position canvas in center-top: */
@@ -131,7 +133,11 @@
console.debug("wasm loaded. starting app…");
// This call installs a bunch of callbacks and then returns:
- wasm_bindgen.start("the_canvas_id");
+ const handle = wasm_bindgen.start("the_canvas_id");
+
+ // call `handle.stop_web()` to stop
+ // uncomment to quick result
+ // setTimeout(() => {handle.stop_web(); handle.free())}, 2000)
console.debug("app started.");
document.getElementById("center_text").remove();
diff --git a/docs/multiple_apps.html b/docs/multiple_apps.html
new file mode 100644
index 00000000..e08659c0
--- /dev/null
+++ b/docs/multiple_apps.html
@@ -0,0 +1,200 @@
+
+
+
+
+
+
+
+
+ egui – An immediate mode GUI written in Rust
+
+
+
+
+
+
+ controls
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/eframe/CHANGELOG.md b/eframe/CHANGELOG.md
index b61a4a25..b3d863ba 100644
--- a/eframe/CHANGELOG.md
+++ b/eframe/CHANGELOG.md
@@ -28,6 +28,8 @@ NOTE: [`egui-winit`](../egui-winit/CHANGELOG.md), [`egui_glium`](../egui_glium/C
#### Web:
* Added option to select WebGL version ([#1803](https://github.com/emilk/egui/pull/1803)).
+* Added ability to stop/re-run web app from JavaScript. ⚠️ You need to update your CSS with `html, body: { height: 100%; width: 100%; }` ([#1803](https://github.com/emilk/egui/pull/1650)).
+
## 0.18.0 - 2022-04-30
diff --git a/eframe/src/lib.rs b/eframe/src/lib.rs
index e96de64b..e09f8fd6 100644
--- a/eframe/src/lib.rs
+++ b/eframe/src/lib.rs
@@ -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;
@@ -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 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 fn start(canvas_id: &str) -> Result<(), eframe::wasm_bindgen::JsValue> {
+/// pub fn start(canvas_id: &str) -> Result>, 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))))
/// }
@@ -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>, wasm_bindgen::JsValue> {
+ let handle = web::start(canvas_id, web_options, app_creator)?;
+
+ Ok(handle)
}
// ----------------------------------------------------------------------------
diff --git a/eframe/src/web/backend.rs b/eframe/src/web/backend.rs
index a65b982d..7c1a37a6 100644
--- a/eframe/src/web/backend.rs
+++ b/eframe/src/web/backend.rs
@@ -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};
// ----------------------------------------------------------------------------
@@ -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 {
@@ -147,11 +166,19 @@ pub struct AppRunner {
pub(crate) input: WebInput,
app: Box,
pub(crate) needs_repaint: std::sync::Arc,
+ pub(crate) is_destroyed: std::sync::Arc,
last_save_time: f64,
screen_reader: super::screen_reader::ScreenReader,
pub(crate) text_cursor_pos: Option,
pub(crate) mutable_text_under_cursor: bool,
textures_delta: TexturesDelta,
+ pub events_to_unsubscribe: Vec,
+}
+
+impl Drop for AppRunner {
+ fn drop(&mut self) {
+ tracing::debug!("AppRunner has fully dropped");
+ }
}
impl AppRunner {
@@ -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());
@@ -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
@@ -358,19 +405,60 @@ impl AppRunner {
pub type AppRunnerRef = Arc>;
+pub struct TargetEvent {
+ target: EventTarget,
+ event_name: String,
+ closure: Closure,
+}
+
+pub struct IntervalHandle {
+ pub handle: i32,
+ pub closure: Closure,
+}
+
+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,
+ pub events: Vec,
}
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(
- &self,
+ &mut self,
target: &EventTarget,
event_name: &'static str,
mut closure: impl FnMut(E, MutexGuard<'_, AppRunner>) + 'static,
@@ -391,14 +479,19 @@ impl AppRunnerContainer {
closure(event, runner_ref.lock());
}
- }) as Box
+ }) as Box
});
// 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(())
}
@@ -421,23 +514,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 {
- 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);
diff --git a/eframe/src/web/events.rs b/eframe/src/web/events.rs
index bfbf1e3b..b50d581e 100644
--- a/eframe/src/web/events.rs
+++ b/eframe/src/web/events.rs
@@ -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,
) -> Result<(), JsValue> {
- fn paint_if_needed(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
+ fn paint_if_needed(runner_ref: &AppRunnerRef) -> Result {
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()?;
@@ -18,7 +22,7 @@ pub fn paint_and_schedule(
runner_lock.auto_save();
}
- Ok(())
+ Ok(IsDestroyed(is_destroyed))
}
fn request_animation_frame(
@@ -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();
@@ -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);
- 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| {
+ 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| {
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;
diff --git a/eframe/src/web/glow_wrapping.rs b/eframe/src/web/glow_wrapping.rs
index ead1d599..b0ac5c2c 100644
--- a/eframe/src/web/glow_wrapping.rs
+++ b/eframe/src/web/glow_wrapping.rs
@@ -87,6 +87,10 @@ impl WrappedGlowPainter {
Ok(())
}
+
+ pub fn destroy(&mut self) {
+ self.painter.destroy()
+ }
}
/// Returns glow context and shader prefix.
diff --git a/eframe/src/web/mod.rs b/eframe/src/web/mod.rs
index b79a2dba..f8a4fb49 100644
--- a/eframe/src/web/mod.rs
+++ b/eframe/src/web/mod.rs
@@ -11,6 +11,7 @@ pub mod storage;
mod text_agent;
pub use backend::*;
+use egui::Vec2;
pub use events::*;
pub use storage::*;
@@ -41,6 +42,7 @@ pub fn now_sec() -> f64 {
/ 1000.0
}
+#[allow(dead_code)]
pub fn screen_size_in_native_points() -> Option {
let window = web_sys::window()?;
Some(egui::vec2(
@@ -96,13 +98,21 @@ pub fn canvas_size_in_points(canvas_id: &str) -> egui::Vec2 {
pub fn resize_canvas_to_screen_size(canvas_id: &str, max_size_points: egui::Vec2) -> Option<()> {
let canvas = canvas_element(canvas_id)?;
+ let parent = canvas.parent_element()?;
+
+ let width = parent.scroll_width();
+ let height = parent.scroll_height();
+
+ let canvas_real_size = Vec2 {
+ x: width as f32,
+ y: height as f32,
+ };
- let screen_size_points = screen_size_in_native_points()?;
let pixels_per_point = native_pixels_per_point();
let max_size_pixels = pixels_per_point * max_size_points;
- let canvas_size_pixels = pixels_per_point * screen_size_points;
+ let canvas_size_pixels = pixels_per_point * canvas_real_size;
let canvas_size_pixels = canvas_size_pixels.min(max_size_pixels);
let canvas_size_points = canvas_size_pixels / pixels_per_point;
diff --git a/eframe/src/web/text_agent.rs b/eframe/src/web/text_agent.rs
index b939971b..2249a255 100644
--- a/eframe/src/web/text_agent.rs
+++ b/eframe/src/web/text_agent.rs
@@ -22,7 +22,7 @@ pub fn text_agent() -> web_sys::HtmlInputElement {
}
/// Text event handler,
-pub fn install_text_agent(runner_container: &AppRunnerContainer) -> Result<(), JsValue> {
+pub fn install_text_agent(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> {
use wasm_bindgen::JsCast;
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
diff --git a/egui_demo_app/src/lib.rs b/egui_demo_app/src/lib.rs
index 8053baab..786aa40d 100644
--- a/egui_demo_app/src/lib.rs
+++ b/egui_demo_app/src/lib.rs
@@ -5,6 +5,15 @@ mod backend_panel;
pub(crate) mod frame_history;
mod wrap_app;
+#[cfg(target_arch = "wasm32")]
+use std::sync::Arc;
+
+#[cfg(target_arch = "wasm32")]
+use eframe::web::AppRunner;
+
+#[cfg(target_arch = "wasm32")]
+use egui::mutex::Mutex;
+
pub use wrap_app::WrapApp;
/// Time of day as seconds since midnight. Used for clock in demo app.
@@ -19,23 +28,60 @@ pub(crate) fn seconds_since_midnight() -> f64 {
#[cfg(target_arch = "wasm32")]
use eframe::wasm_bindgen::{self, prelude::*};
+#[cfg(target_arch = "wasm32")]
+#[wasm_bindgen]
+pub struct WebHandle {
+ handle: Arc>,
+}
+
+#[cfg(target_arch = "wasm32")]
+#[wasm_bindgen]
+impl WebHandle {
+ #[wasm_bindgen]
+ #[cfg(target_arch = "wasm32")]
+ pub fn stop_web(&self) -> Result<(), wasm_bindgen::JsValue> {
+ let mut app = self.handle.lock();
+ let res = app.destroy();
+
+ // let numw = Arc::weak_count(&app);
+ // let nums = Arc::strong_count(&app);
+ // tracing::debug!("runner ref {:?}, {:?}", numw, nums);
+
+ res
+ }
+}
+
+#[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:
+ tracing_wasm::set_as_global_default();
+}
+
+#[cfg(target_arch = "wasm32")]
+#[wasm_bindgen]
+pub fn start_separate(canvas_id: &str) -> Result {
+ let web_options = eframe::WebOptions::default();
+ let handle = eframe::start_web(
+ canvas_id,
+ web_options,
+ Box::new(|cc| Box::new(WrapApp::new(cc))),
+ )
+ .map(|handle| WebHandle { handle });
+
+ 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 fn start(canvas_id: &str) -> Result<(), wasm_bindgen::JsValue> {
- // Make sure panics are logged using `console.error`.
- console_error_panic_hook::set_once();
-
- // Redirect tracing to console.log and friends:
- tracing_wasm::set_as_global_default();
-
- let web_options = eframe::WebOptions::default();
- eframe::start_web(
- canvas_id,
- web_options,
- Box::new(|cc| Box::new(WrapApp::new(cc))),
- )
+pub fn start(canvas_id: &str) -> Result {
+ init_wasm_hooks();
+ start_separate(canvas_id)
}