diff --git a/docs/index.html b/docs/index.html index 648ba74cdfe..dae8cc6443c 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: */ diff --git a/eframe/src/lib.rs b/eframe/src/lib.rs index e96de64b3ee..8b9148af159 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 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>, 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 301369cee71..5e1c5ebf93d 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,18 +405,59 @@ 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, @@ -390,14 +478,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(()) } @@ -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 { - 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 bfbf1e3b669..b50d581eb5d 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 ead1d599120..b0ac5c2ccdc 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 b79a2dba6b7..f8a4fb493af 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 b939971bb6e..2249a2556e2 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 8053baab11c..61a6525a6da 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,13 +28,35 @@ 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 res = self.handle.lock().destroy(); + + // let numw = Arc::weak_count(&uu); + // let nums = Arc::strong_count(&uu); + // tracing::debug!("runner ref {:?}, {:?}", numw, nums); + + res + } +} + /// 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> { +pub fn start(canvas_id: &str) -> Result { // Make sure panics are logged using `console.error`. console_error_panic_hook::set_once(); @@ -33,9 +64,12 @@ pub fn start(canvas_id: &str) -> Result<(), wasm_bindgen::JsValue> { tracing_wasm::set_as_global_default(); let web_options = eframe::WebOptions::default(); - eframe::start_web( + let handle = eframe::start_web( canvas_id, web_options, Box::new(|cc| Box::new(WrapApp::new(cc))), ) + .map(|handle| WebHandle { handle }); + + handle }