From c834847c48264ed8455ec93b963dfa95ec311b16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Wed, 30 Aug 2023 13:31:08 +0200 Subject: [PATCH] Handle wasm panics, display a message and allow a restart (#7507) --- Cargo.lock | 27 +- Cargo.toml | 2 +- app/gui/Cargo.toml | 1 - app/gui/src/integration_test.rs | 11 +- app/gui/src/transport/web.rs | 69 +-- app/gui/view/documentation/src/lib.rs | 14 +- .../src/component/breadcrumbs/breadcrumb.rs | 526 ------------------ .../src/component/node/action_bar.rs | 2 +- app/gui/view/graph-editor/src/lib.rs | 6 +- app/gui/view/src/project.rs | 2 +- app/gui/view/welcome-screen/Cargo.toml | 1 - app/gui/view/welcome-screen/src/lib.rs | 18 +- app/ide-desktop/eslint.config.js | 2 +- app/ide-desktop/lib/client/src/security.ts | 2 +- app/ide-desktop/lib/content/src/index.ts | 6 + app/ide-desktop/lib/content/src/panic.tsx | 103 ++++ .../src/authentication/src/components/app.tsx | 1 + .../lib/dashboard/src/tailwind.css | 4 + .../lib/dashboard/tailwind.config.ts | 6 +- build-config.yaml | 2 +- build/build/src/config.rs | 4 +- build/build/src/project/wasm.rs | 5 +- build/ci_utils/src/programs/wasm_pack.rs | 12 + .../ensogl/component/drop-manager/src/lib.rs | 68 +-- .../ensogl/component/scrollbar/src/lib.rs | 4 +- .../component/text/src/font/msdf/build.rs | 2 +- lib/rust/ensogl/core/Cargo.toml | 1 - lib/rust/ensogl/core/src/animation/loops.rs | 89 +-- lib/rust/ensogl/core/src/application.rs | 2 - .../core/src/control/io/keyboard/dom.rs | 20 +- lib/rust/ensogl/core/src/control/io/mouse.rs | 37 +- lib/rust/ensogl/core/src/debug/monitor.rs | 25 +- lib/rust/ensogl/core/src/display/scene.rs | 6 +- lib/rust/ensogl/core/src/display/world.rs | 12 +- .../ensogl/core/src/system/gpu/context.rs | 13 +- .../gpu/data/texture/storage/remote_image.rs | 13 +- .../ensogl/core/src/system/web/dom/shape.rs | 10 +- lib/rust/ensogl/examples/text-area/src/lib.rs | 13 +- lib/rust/ensogl/pack/js/package-lock.json | 111 +++- lib/rust/ensogl/pack/js/package.json | 3 +- lib/rust/ensogl/pack/js/src/runner/index.ts | 52 +- lib/rust/frp/src/io/timer.rs | 47 ++ lib/rust/frp/src/io/timer/delayed_interval.rs | 6 +- lib/rust/frp/src/io/timer/interval.rs | 71 +-- lib/rust/frp/src/io/timer/timeout.rs | 74 +-- lib/rust/frp/src/microtasks.rs | 69 +-- lib/rust/prelude/src/lib.rs | 2 +- lib/rust/web/Cargo.toml | 59 +- lib/rust/web/js/callbacks_with_cleanup.ts | 87 +++ lib/rust/web/js/intersection_observer.js | 79 --- lib/rust/web/js/resize_observer.js | 73 --- lib/rust/web/src/binding/mock.rs | 126 ++++- lib/rust/web/src/binding/wasm.rs | 95 ++++ lib/rust/web/src/closure.rs | 8 - lib/rust/web/src/closure/storage.rs | 91 --- lib/rust/web/src/event.rs | 32 +- lib/rust/web/src/event/listener.rs | 49 +- lib/rust/web/src/lib.rs | 298 ++++------ lib/rust/web/src/resize_observer.rs | 51 +- lib/rust/web/src/stream.rs | 65 --- lib/rust/web/tsconfig.json | 3 + 61 files changed, 1052 insertions(+), 1640 deletions(-) delete mode 100644 app/gui/view/graph-editor/src/component/breadcrumbs/breadcrumb.rs create mode 100644 app/ide-desktop/lib/content/src/panic.tsx create mode 100644 lib/rust/web/js/callbacks_with_cleanup.ts delete mode 100644 lib/rust/web/js/intersection_observer.js delete mode 100644 lib/rust/web/js/resize_observer.js delete mode 100644 lib/rust/web/src/closure.rs delete mode 100644 lib/rust/web/src/closure/storage.rs delete mode 100644 lib/rust/web/src/stream.rs create mode 100644 lib/rust/web/tsconfig.json diff --git a/Cargo.lock b/Cargo.lock index d11987e8348..b637f7dd9dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2202,7 +2202,6 @@ dependencies = [ "analytics", "ast", "bimap", - "console_error_panic_hook", "const_format", "convert_case 0.6.0", "double-representation", @@ -2706,7 +2705,6 @@ dependencies = [ "bitflags 2.2.1", "bytemuck", "code-builder", - "console_error_panic_hook", "enso-callback", "enso-data-structures", "enso-debug-api", @@ -7423,9 +7421,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen-macro", @@ -7433,16 +7431,16 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 1.0.107", + "syn 2.0.15", "wasm-bindgen-shared", ] @@ -7460,9 +7458,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7470,22 +7468,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", + "syn 2.0.15", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" [[package]] name = "wasm-bindgen-test" @@ -7609,7 +7607,6 @@ dependencies = [ "enso-frp", "ensogl", "wasm-bindgen", - "web-sys", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 2bcc2ed35f1..5d98ffedee2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -93,7 +93,7 @@ serde-wasm-bindgen = { version = "0.4.5" } tokio = { version = "1.23.0", features = ["full", "tracing"] } tokio-stream = { version = "0.1.12", features = ["fs"] } tokio-util = { version = "0.7.4", features = ["full"] } -wasm-bindgen = { version = "0.2.84", features = [] } +wasm-bindgen = { version = "0.2.87", features = [] } wasm-bindgen-test = { version = "0.3.34" } anyhow = { version = "1.0.66" } failure = { version = "0.1.8" } diff --git a/app/gui/Cargo.toml b/app/gui/Cargo.toml index c5a1e8307c0..08f5f1841a0 100644 --- a/app/gui/Cargo.toml +++ b/app/gui/Cargo.toml @@ -42,7 +42,6 @@ engine-protocol = { path = "controller/engine-protocol" } json-rpc = { path = "../../lib/rust/json-rpc" } span-tree = { path = "language/span-tree" } bimap = { version = "0.4.0" } -console_error_panic_hook = { workspace = true } const_format = { workspace = true } convert_case = { workspace = true } failure = { workspace = true } diff --git a/app/gui/src/integration_test.rs b/app/gui/src/integration_test.rs index 377f2cee66f..691f09c3fe3 100644 --- a/app/gui/src/integration_test.rs +++ b/app/gui/src/integration_test.rs @@ -9,7 +9,7 @@ use crate::executor::setup_global_executor; use crate::executor::web::EventLoopExecutor; use crate::Ide; -use enso_web::Closure; +use enso_web::CleanupHandle; use enso_web::HtmlDivElement; use ensogl::application::test_utils::ApplicationExt; use std::pin::Pin; @@ -205,8 +205,8 @@ impl Fixture { #[derive(Default, Debug)] #[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))] pub struct WaitAFrame { - frame_passed: Rc>, - closure: Option>, + frame_passed: Rc>, + frame_listener: Option, } impl Future for WaitAFrame { @@ -230,12 +230,11 @@ impl Future for WaitAFrame { } else { let waker = cx.waker().clone(); let frame_passed = self.frame_passed.clone_ref(); - let closure = Closure::once(move |_| { + let listener = enso_web::request_animation_frame(move |_| { frame_passed.set(true); waker.wake() }); - enso_web::window.request_animation_frame_with_closure_or_panic(&closure); - self.closure = Some(closure); + self.frame_listener = Some(listener); std::task::Poll::Pending } } diff --git a/app/gui/src/transport/web.rs b/app/gui/src/transport/web.rs index 18f88e31a08..a04e3f20f10 100644 --- a/app/gui/src/transport/web.rs +++ b/app/gui/src/transport/web.rs @@ -1,15 +1,14 @@ -//! web_sys::WebSocket-based `Transport` implementation. +//! web::WebSocket-based `Transport` implementation. use crate::prelude::*; use enso_web::traits::*; +use enso_web as web; use enso_web::event::listener::Slot; use failure::Error; use futures::channel::mpsc; use json_rpc::Transport; use json_rpc::TransportEvent; -use wasm_bindgen::JsCast; -use web_sys::BinaryType; @@ -54,7 +53,7 @@ pub enum SendingError { impl SendingError { /// Constructs from the error yielded by one of the JS's WebSocket sending functions. - pub fn from_send_error(error: wasm_bindgen::JsValue) -> SendingError { + pub fn from_send_error(error: web::JsValue) -> SendingError { SendingError::FailedToSend(error.print_to_string()) } } @@ -82,7 +81,7 @@ pub enum State { impl State { /// Returns current state of the given WebSocket. - pub fn query_ws(ws: &web_sys::WebSocket) -> State { + pub fn query_ws(ws: &web::WebSocket) -> State { State::from_code(ws.ready_state()) } @@ -90,10 +89,10 @@ impl State { /// cf https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState pub fn from_code(code: u16) -> State { match code { - web_sys::WebSocket::CONNECTING => State::Connecting, - web_sys::WebSocket::OPEN => State::Open, - web_sys::WebSocket::CLOSING => State::Closing, - web_sys::WebSocket::CLOSED => State::Closed, + web::WebSocket::CONNECTING => State::Connecting, + web::WebSocket::OPEN => State::Open, + web::WebSocket::CLOSING => State::Closing, + web::WebSocket::CLOSED => State::Closed, num => State::Unknown(num), // impossible } } @@ -114,8 +113,8 @@ pub mod event { #[derive(Clone, Copy, Debug)] pub enum Open {} impl Type for Open { - type Interface = web_sys::Event; - type Target = web_sys::WebSocket; + type Interface = web::Event; + type Target = web::WebSocket; const NAME: &'static str = "open"; } @@ -123,8 +122,8 @@ pub mod event { #[derive(Clone, Copy, Debug)] pub enum Close {} impl Type for Close { - type Interface = web_sys::CloseEvent; - type Target = web_sys::WebSocket; + type Interface = web::CloseEvent; + type Target = web::WebSocket; const NAME: &'static str = "close"; } @@ -132,8 +131,8 @@ pub mod event { #[derive(Clone, Copy, Debug)] pub enum Message {} impl Type for Message { - type Interface = web_sys::MessageEvent; - type Target = web_sys::WebSocket; + type Interface = web::MessageEvent; + type Target = web::WebSocket; const NAME: &'static str = "message"; } @@ -141,8 +140,8 @@ pub mod event { #[derive(Clone, Copy, Debug)] pub enum Error {} impl Type for Error { - type Interface = web_sys::Event; - type Target = web_sys::WebSocket; + type Interface = web::Event; + type Target = web::WebSocket; const NAME: &'static str = "error"; } } @@ -165,7 +164,7 @@ struct Model { pub on_error: Slot, // === Internal === - pub socket: web_sys::WebSocket, + pub socket: web::WebSocket, /// Special callback on "close" event. As it must be invoked after `on_close`, care should be /// taken to keep it registered as an event listener *after* `on_close` registration. /// By default `Model` takes care of it by itself. @@ -176,8 +175,8 @@ struct Model { impl Model { /// Wraps given WebSocket object. - pub fn new(socket: web_sys::WebSocket) -> Model { - socket.set_binary_type(BinaryType::Arraybuffer); + pub fn new(socket: web::WebSocket) -> Model { + socket.set_binary_type(web::BinaryType::Arraybuffer); Model { on_close: Slot::new(&socket), on_message: Slot::new(&socket), @@ -190,7 +189,7 @@ impl Model { } /// Close the socket. - pub fn close(&mut self, reason: &str) -> Result<(), wasm_bindgen::JsValue> { + pub fn close(&mut self, reason: &str) -> Result<(), web::JsValue> { // If socket was manually requested to close, it should not try to reconnect then. self.auto_reconnect = false; let normal_closure = 1000; @@ -226,15 +225,15 @@ impl Model { /// Establish a new WS connection, using the same URL as the previous one. /// All callbacks will be transferred to the new connection. - pub fn reconnect(&mut self) -> Result<(), wasm_bindgen::JsValue> { + pub fn reconnect(&mut self) -> Result<(), web::JsValue> { if !self.auto_reconnect { - return Err(js_sys::Error::new("Reconnecting has been disabled").into()); + return Err(web::Error::new("Reconnecting has been disabled").into()); } let url = self.socket.url(); info!("Reconnecting WS to {url}."); - let new_ws = web_sys::WebSocket::new(&url)?; + let new_ws = web::WebSocket::new(&url)?; self.on_close.set_target(&new_ws); self.on_error.set_target(&new_ws); @@ -271,7 +270,7 @@ pub struct WebSocket { impl WebSocket { /// Wrap given raw JS WebSocket object. - pub fn new(ws: web_sys::WebSocket) -> WebSocket { + pub fn new(ws: web::WebSocket) -> WebSocket { let model = Rc::new(RefCell::new(Model::new(ws))); WebSocket { model } } @@ -279,14 +278,14 @@ impl WebSocket { /// Establish connection with endpoint defined by the given URL and wrap it. /// Asynchronous, because it waits until connection is established. pub async fn new_opened(url: &str) -> Result { - let ws = web_sys::WebSocket::new(url).map_err(ConnectingError::construction_error)?; + let ws = web::WebSocket::new(url).map_err(ConnectingError::construction_error)?; let mut wst = WebSocket::new(ws); wst.wait_until_open().await?; Ok(wst) } /// Generate a callback to be invoked when socket needs reconnecting. - fn reconnect_trigger(&self) -> impl FnMut(web_sys::CloseEvent) { + fn reconnect_trigger(&self) -> impl FnMut(web::CloseEvent) { let model = Rc::downgrade(&self.model); move |_| { if let Some(model) = model.upgrade() { @@ -336,7 +335,7 @@ impl WebSocket { } /// Sets callback for the `close` event. - pub fn set_on_close(&mut self, f: impl FnMut(web_sys::CloseEvent) + 'static) { + pub fn set_on_close(&mut self, f: impl FnMut(web::CloseEvent) + 'static) { self.with_borrow_mut_model(move |model| { model.on_close.set_callback(f); // Force internal callback to be after the user-defined one. @@ -345,17 +344,17 @@ impl WebSocket { } /// Sets callback for the `error` event. - pub fn set_on_error(&mut self, f: impl FnMut(web_sys::Event) + 'static) { + pub fn set_on_error(&mut self, f: impl FnMut(web::Event) + 'static) { self.with_borrow_mut_model(move |model| model.on_error.set_callback(f)) } /// Sets callback for the `message` event. - pub fn set_on_message(&mut self, f: impl FnMut(web_sys::MessageEvent) + 'static) { + pub fn set_on_message(&mut self, f: impl FnMut(web::MessageEvent) + 'static) { self.with_borrow_mut_model(move |model| model.on_message.set_callback(f)) } /// Sets callback for the `open` event. - pub fn set_on_open(&mut self, f: impl FnMut(web_sys::Event) + 'static) { + pub fn set_on_open(&mut self, f: impl FnMut(web::Event) + 'static) { self.with_borrow_mut_model(move |model| model.on_open.set_callback(f)) } @@ -367,7 +366,7 @@ impl WebSocket { /// /// WARNING: `f` works under borrow_mut and must not give away control. fn send_with_open_socket(&mut self, f: F) -> Result - where F: FnOnce(&mut web_sys::WebSocket) -> Result { + where F: FnOnce(&mut web::WebSocket) -> Result { // Sending through the closed WebSocket can return Ok() with error only // appearing in the log. We explicitly check for this to get failure as // early as possible. @@ -405,6 +404,7 @@ impl Transport for WebSocket { self.send_with_open_socket(|ws| ws.send_with_u8_array(mut_slice)) } + #[cfg(target_arch = "wasm32")] fn set_event_transmitter(&mut self, transmitter: mpsc::UnboundedSender) { info!("Setting event transmitter."); let transmitter_copy = transmitter.clone(); @@ -435,6 +435,11 @@ impl Transport for WebSocket { channel::emit(&transmitter, TransportEvent::Opened); }); } + + #[cfg(not(target_arch = "wasm32"))] + fn set_event_transmitter(&mut self, transmitter: mpsc::UnboundedSender) { + let _ = transmitter; + } } #[cfg(test)] diff --git a/app/gui/view/documentation/src/lib.rs b/app/gui/view/documentation/src/lib.rs index fb8aacdd9f0..a41e7af56b1 100644 --- a/app/gui/view/documentation/src/lib.rs +++ b/app/gui/view/documentation/src/lib.rs @@ -55,7 +55,7 @@ pub use ensogl_breadcrumbs as breadcrumbs; const INITIAL_SECTION_NAME: &str = "Popular"; /// Delay before updating the displayed documentation. -const DISPLAY_DELAY_MS: i32 = 0; +const DISPLAY_DELAY_MS: u32 = 0; // === Style === @@ -94,7 +94,7 @@ pub struct Model { overlay: Rectangle, background: Rectangle, display_object: display::object::Instance, - event_handlers: Rc>>, + event_handlers: Rc>>, } impl Model { @@ -200,13 +200,13 @@ impl Model { display_doc: &frp::Source, ) { let new_handlers = linked_pages.into_iter().filter_map(|page| { - let content = page.page.clone_ref(); let anchor = html::anchor_name(&page.name); if let Some(element) = web::document.get_element_by_id(&anchor) { - let closure: web::JsEventHandler = web::Closure::new(f_!([display_doc, content] { - display_doc.emit(content.clone_ref()); - })); - Some(web::add_event_listener(&element, "click", closure)) + let content = page.page.clone_ref(); + let display_doc = display_doc.clone_ref(); + Some(web::add_event_listener(&element, "click", move |_| { + display_doc.emit(content.clone_ref()) + })) } else { None } diff --git a/app/gui/view/graph-editor/src/component/breadcrumbs/breadcrumb.rs b/app/gui/view/graph-editor/src/component/breadcrumbs/breadcrumb.rs deleted file mode 100644 index aac11c41736..00000000000 --- a/app/gui/view/graph-editor/src/component/breadcrumbs/breadcrumb.rs +++ /dev/null @@ -1,526 +0,0 @@ -//! This module provides a clickable view for a single breadcrumb. - -use crate::prelude::*; -use ensogl::display::shape::*; - -use crate::component::breadcrumbs; -use crate::component::breadcrumbs::project_name::LINE_HEIGHT; -use crate::MethodPointer; - -use super::GLYPH_WIDTH; -use super::HORIZONTAL_MARGIN; -use super::TEXT_SIZE; - -use enso_frp as frp; -use ensogl::application::Application; -use ensogl::data::color; -use ensogl::display; -use ensogl::display::object::ObjectOps; -use ensogl::DEPRECATED_Animation; -use ensogl_component::text; -use ensogl_hardcoded_theme as theme; -use nalgebra::Vector2; -use std::f32::consts::PI; - - - -// ================= -// === Constants === -// ================= - -/// Breadcrumb vertical margin. -pub const VERTICAL_MARGIN: f32 = 0.0; -/// Breadcrumb left margin. -pub const LEFT_MARGIN: f32 = 0.0; -/// Breadcrumb right margin. -pub const RIGHT_MARGIN: f32 = 0.0; -const ICON_LEFT_MARGIN: f32 = 0.0; -const ICON_RIGHT_MARGIN: f32 = HORIZONTAL_MARGIN; -const ICON_RADIUS: f32 = 6.0; -const ICON_SIZE: f32 = ICON_RADIUS * 2.0; -const ICON_RING_WIDTH: f32 = 1.5; -const ICON_ARROW_SIZE: f32 = 4.0; -const SEPARATOR_SIZE: f32 = 6.0; -/// Breadcrumb padding. -pub const PADDING: f32 = 1.0; -const SEPARATOR_MARGIN: f32 = 10.0; - - - -// ================== -// === Background === -// ================== - -/// A transparent "background" of single breadcrumb, set for capturing mouse events. -pub mod background { - use super::*; - - ensogl::shape! { - alignment = center; - (style: Style) { - let bg_color = color::Rgba::new(0.0,0.0,0.0,0.000_001); - Plane().fill(bg_color).into() - } - } -} - - - -// ============ -// === Icon === -// ============ - -mod icon { - use super::*; - - ensogl::shape! { - pointer_events = false; - alignment = center; - (style: Style, red: f32, green: f32, blue: f32, alpha: f32) { - let outer_circle = Circle((ICON_RADIUS).px()); - let inner_circle = Circle((ICON_RADIUS - ICON_RING_WIDTH).px()); - let ring = outer_circle - inner_circle; - let size = ICON_ARROW_SIZE; - let arrow = Triangle(size.px(),size.px()).rotate((PI/2.0).radians()); - let arrow = arrow.translate_x(0.5.px()); - let shape = ring + arrow; - let color = format!("vec4({red},{green},{blue},{alpha})"); - let color : Var = color.into(); - shape.fill(color).into() - } - } -} - - - -// ================= -// === Separator === -// ================= - -mod separator { - use super::*; - - ensogl::shape! { - pointer_events = false; - alignment = center; - (style: Style, red: f32, green: f32, blue: f32, alpha: f32) { - let size = SEPARATOR_SIZE; - let angle = PI/2.0; - let triangle = Triangle(size.px(),size.px()).rotate(angle.radians()); - let color = format!("vec4({red},{green},{blue},{alpha})"); - let color : Var = color.into(); - triangle.fill(color).into() - } - } -} - - - -// ======================== -// === RelativePosition === -// ======================== - -/// The position of this breadcrumb relative to the selected breadcrumb. We use this to determine -/// the color. -#[derive(Debug, Clone, Copy)] -enum RelativePosition { - Left, - Right, -} - - - -// ================== -// === Animations === -// ================== - -/// ProjectName's animations handlers. -#[derive(Debug, Clone, CloneRef)] -pub struct Animations { - color: DEPRECATED_Animation>, - separator_color: DEPRECATED_Animation>, - fade_in: DEPRECATED_Animation, -} - -impl Animations { - /// Constructor. - pub fn new(network: &frp::Network) -> Self { - let color = DEPRECATED_Animation::new(network); - let fade_in = DEPRECATED_Animation::new(network); - let separator_color = DEPRECATED_Animation::new(network); - Self { color, separator_color, fade_in } - } -} - - - -// ================= -// === FrpInputs === -// ================= - -/// Breadcrumb frp network inputs. -#[derive(Debug, Clone, CloneRef)] -pub struct FrpInputs { - /// Select the breadcrumb, triggering the selection animation. - pub select: frp::Source, - /// Select the breadcrumb, triggering the deselection animation, using the (self,new) - /// breadcrumb indices to determine if the breadcrumb is on the left or on the right of the - /// newly selected breadcrumb. - pub deselect: frp::Source<(usize, usize)>, - /// Triggers the fade in animation, which only makes sense during the breadcrumb creation. - pub fade_in: frp::Source, -} - -impl FrpInputs { - /// Constructor. - pub fn new(network: &frp::Network) -> Self { - frp::extend! {network - select <- source(); - deselect <- source(); - fade_in <- source(); - } - Self { select, deselect, fade_in } - } -} - - - -// ================== -// === FrpOutputs === -// ================== - -/// Breadcrumb frp network outputs. -#[derive(Debug, Clone, CloneRef)] -pub struct FrpOutputs { - /// Signalizes that the breadcrumb was clicked. - pub clicked: frp::Source, - /// Signalizes that the breadcrumb's size changed. - pub size: frp::Source>, - /// Signalizes the breadcrumb's selection state. - pub selected: frp::Source, - /// Used to check if the breadcrumb is selected. - pub is_selected: frp::Sampler, -} - -impl FrpOutputs { - /// Constructor. - pub fn new(network: &frp::Network) -> Self { - frp::extend! { network - clicked <- source(); - size <- source(); - selected <- source(); - is_selected <- selected.sampler(); - } - Self { clicked, size, selected, is_selected } - } -} - - - -// =========== -// === Frp === -// =========== - -/// A breadcrumb frp structure with its endpoints and network representation. -#[derive(Debug, Clone, CloneRef)] -#[allow(missing_docs)] -pub struct Frp { - pub inputs: FrpInputs, - pub outputs: FrpOutputs, - pub network: frp::Network, -} - -impl Deref for Frp { - type Target = FrpInputs; - fn deref(&self) -> &Self::Target { - &self.inputs - } -} - -impl Default for Frp { - fn default() -> Self { - Self::new() - } -} - -impl Frp { - /// Constructor. - pub fn new() -> Self { - let network = frp::Network::new("breadcrumbs"); - let inputs = FrpInputs::new(&network); - let outputs = FrpOutputs::new(&network); - Self { inputs, outputs, network } - } -} - - - -// ====================== -// === BreadcrumbInfo === -// ====================== - -/// Breadcrumb information such as name and expression ID. -#[derive(Debug)] -#[allow(missing_docs)] -pub struct BreadcrumbInfo { - pub method_pointer: MethodPointer, - pub expression_id: ast::Id, -} - - - -// ======================= -// === BreadcrumbModel === -// ======================= - -/// Breadcrumbs model. -#[derive(Debug, Clone, CloneRef, display::Object)] -pub struct BreadcrumbModel { - display_object: display::object::Instance, - view: background::View, - separator: separator::View, - icon: icon::View, - label: text::Text, - animations: Animations, - style: StyleWatch, - /// Breadcrumb information such as name and expression ID. - pub info: Rc, - relative_position: Rc>>, - outputs: FrpOutputs, -} - -impl BreadcrumbModel { - /// Constructor. - #[profile(Detail)] - pub fn new( - app: &Application, - frp: &Frp, - method_pointer: &MethodPointer, - expression_id: &ast::Id, - ) -> Self { - let scene = &app.display.default_scene; - let display_object = display::object::Instance::new(); - let view = background::View::new(); - let icon = icon::View::new(); - let separator = separator::View::new(); - let label = app.new_view::(); - let expression_id = *expression_id; - let method_pointer = method_pointer.clone(); - let info = Rc::new(BreadcrumbInfo { method_pointer, expression_id }); - let animations = Animations::new(&frp.network); - let relative_position = default(); - let outputs = frp.outputs.clone_ref(); - - ensogl::shapes_order_dependencies! { - scene => { - background -> icon; - background -> separator; - } - } - - scene.layers.panel.add(&view); - scene.layers.panel.add(&icon); - scene.layers.panel.add(&separator); - - scene.layers.main.remove(&label); - label.add_to_scene_layer(&scene.layers.panel_text); - - // FIXME : StyleWatch is unsuitable here, as it was designed as an internal tool for shape - // system (#795) - let style = StyleWatch::new(&scene.style_sheet); - Self { - display_object, - view, - separator, - icon, - label, - animations, - style, - info, - relative_position, - outputs, - } - .init() - } - - fn init(self) -> Self { - self.add_child(&self.view); - self.view.add_child(&self.separator); - self.separator.add_child(&self.icon); - self.icon.add_child(&self.label); - - let styles = &self.style; - let full_color = styles.get_color(theme::graph_editor::breadcrumbs::full); - let transparent_color = styles.get_color(theme::graph_editor::breadcrumbs::transparent); - - let color = if self.is_selected() { full_color } else { transparent_color }; - - self.label.set_property_default(color); - self.label.set_property_default(text::formatting::Size::from(TEXT_SIZE)); - self.label.set_single_line_mode(true); - self.label.set_x(ICON_RADIUS + ICON_RIGHT_MARGIN); - self.label.set_y(TEXT_SIZE / 2.0); - self.label.set_content(&self.info.method_pointer.name); - - let width = self.width(); - let height = self.height(); - let offset = SEPARATOR_MARGIN + SEPARATOR_SIZE / 2.0; - - self.view.set_size(Vector2::new(width, height)); - self.fade_in(0.0); - let separator_size = (SEPARATOR_SIZE + PADDING * 2.0).max(0.0); - let icon_size = (ICON_SIZE + PADDING * 2.0).max(0.0); - self.separator.set_size(Vector2::new(separator_size, separator_size)); - self.separator.set_x((offset - width / 2.0).round()); - self.icon.set_size(Vector2::new(icon_size, icon_size)); - let x_position = offset + PADDING + ICON_SIZE / 2.0 + LEFT_MARGIN + ICON_LEFT_MARGIN; - self.icon.set_x(x_position.round()); - - self - } - - fn label_width(&self) -> f32 { - self.info.method_pointer.name.len() as f32 * GLYPH_WIDTH - } - - /// Get the width of the view. - pub fn width(&self) -> f32 { - let separator_width = SEPARATOR_MARGIN * 2.0 + SEPARATOR_SIZE; - let icon_width = ICON_LEFT_MARGIN + ICON_SIZE + ICON_RIGHT_MARGIN; - let label_width = self.label_width(); - let margin_and_padding = LEFT_MARGIN + RIGHT_MARGIN + PADDING * 2.0; - let width = separator_width + icon_width + label_width + margin_and_padding; - width.ceil() - } - - /// Get the height of the view. - pub fn height(&self) -> f32 { - LINE_HEIGHT + breadcrumbs::VERTICAL_MARGIN * 2.0 - } - - fn fade_in(&self, value: f32) { - let width = self.width(); - let height = self.height(); - let x_position = width * value / 2.0; - let y_position = -height / 2.0 - VERTICAL_MARGIN - PADDING; - self.view.set_position(Vector3(x_position.round(), y_position.round(), 0.0)); - } - - fn set_color(&self, value: Vector4) { - let color = color::Rgba::from(value); - self.label.set_property(.., color); - self.icon.red.set(color.red); - self.icon.green.set(color.green); - self.icon.blue.set(color.blue); - self.icon.alpha.set(color.alpha); - } - - fn set_separator_color(&self, value: Vector4) { - let color = color::Rgba::from(value); - self.separator.red.set(color.red); - self.separator.green.set(color.green); - self.separator.blue.set(color.blue); - self.separator.alpha.set(color.alpha); - } - - fn select(&self) { - let styles = &self.style; - let selected_color = styles.get_color(theme::graph_editor::breadcrumbs::selected); - let left_deselected = styles.get_color(theme::graph_editor::breadcrumbs::deselected::left); - - self.animations.color.set_target_value(selected_color.into()); - self.animations.separator_color.set_target_value(left_deselected.into()); - } - - fn deselect(&self, old: usize, new: usize) { - let left = RelativePosition::Left; - let right = RelativePosition::Right; - self.relative_position - .set((new > old).as_option().map(|_| Some(left)).unwrap_or(Some(right))); - let color = self.deselected_color().into(); - self.animations.color.set_target_value(color); - self.animations.separator_color.set_target_value(color); - } - - fn deselected_color(&self) -> color::Rgba { - let styles = &self.style; - let selected_color = styles.get_color(theme::graph_editor::breadcrumbs::selected); - let left_deselected = styles.get_color(theme::graph_editor::breadcrumbs::deselected::left); - let right_deselected = - styles.get_color(theme::graph_editor::breadcrumbs::deselected::right); - - match self.relative_position.get() { - Some(RelativePosition::Right) => right_deselected, - Some(RelativePosition::Left) => left_deselected, - None => selected_color, - } - } - - fn is_selected(&self) -> bool { - self.outputs.is_selected.value() - } -} - - - -// ================== -// === Breadcrumb === -// ================== - -/// The breadcrumb's view which displays its name and exposes mouse press interactions. -#[derive(Debug, Clone, CloneRef, Deref, display::Object)] -#[allow(missing_docs)] -pub struct Breadcrumb { - #[deref] - #[display_object] - model: Rc, - pub frp: Frp, -} - -impl Breadcrumb { - /// Constructor. - pub fn new(app: &Application, method_pointer: &MethodPointer, expression_id: &ast::Id) -> Self { - let frp = Frp::new(); - let model = Rc::new(BreadcrumbModel::new(app, &frp, method_pointer, expression_id)); - let network = &frp.network; - let scene = &app.display.default_scene; - - // FIXME : StyleWatch is unsuitable here, as it was designed as an internal tool for shape - // system (#795) - let styles = StyleWatch::new(&scene.style_sheet); - let hover_color = styles.get_color(theme::graph_editor::breadcrumbs::hover); - - frp::extend! { network - eval_ frp.fade_in(model.animations.fade_in.set_target_value(1.0)); - eval_ frp.select({ - model.outputs.selected.emit(true); - model.select(); - }); - eval frp.deselect(((old,new)) { - model.outputs.selected.emit(false); - model.deselect(*old,*new); - }); - not_selected <- frp.outputs.selected.map(|selected| !selected); - mouse_over_if_not_selected <- model.view.events_deprecated.mouse_over.gate(¬_selected); - mouse_out_if_not_selected <- model.view.events_deprecated.mouse_out.gate(¬_selected); - eval_ mouse_over_if_not_selected( - model.animations.color.set_target_value(hover_color.into()) - ); - eval_ mouse_out_if_not_selected( - model.animations.color.set_target_value(model.deselected_color().into()) - ); - eval_ model.view.events_deprecated.mouse_down_primary(frp.outputs.clicked.emit(())); - } - - - // === Animations === - - frp::extend! {network - eval model.animations.fade_in.value((value) model.fade_in(*value)); - eval model.animations.color.value((value) model.set_color(*value)); - eval model.animations.separator_color.value((value) model.set_separator_color(*value)); - } - - Self { model, frp } - } -} diff --git a/app/gui/view/graph-editor/src/component/node/action_bar.rs b/app/gui/view/graph-editor/src/component/node/action_bar.rs index edb3b63e195..a62ced19cd2 100644 --- a/app/gui/view/graph-editor/src/component/node/action_bar.rs +++ b/app/gui/view/graph-editor/src/component/node/action_bar.rs @@ -38,7 +38,7 @@ const HOVER_EXTENSION: Vector2 = Vector2(15.0, 11.0); /// The size of additional hover area that is drawn below the node background. Necessary to prevent /// easily losing the hover state when moving the mouse towards the action bar. const HOVER_BRIDGE_SIZE: Vector2 = Vector2(10.0, 26.0); -const HOVER_HIDE_DELAY_MS: i32 = 20; +const HOVER_HIDE_DELAY_MS: u32 = 20; const VISIBILITY_TOOLTIP_LABEL: &str = "Show preview"; const DISABLE_OUTPUT_CONTEXT_TOOLTIP_LABEL: &str = "Don't write to files and databases"; const ENABLE_OUTPUT_CONTEXT_TOOLTIP_LABEL: &str = "Allow writing to files and databases"; diff --git a/app/gui/view/graph-editor/src/lib.rs b/app/gui/view/graph-editor/src/lib.rs index c1117cff8b6..ca014553b4b 100644 --- a/app/gui/view/graph-editor/src/lib.rs +++ b/app/gui/view/graph-editor/src/lib.rs @@ -112,8 +112,8 @@ const SNAP_DISTANCE_THRESHOLD: f32 = 10.0; const VIZ_PREVIEW_MODE_TOGGLE_TIME_MS: f32 = 300.0; /// Number of frames we expect to pass during the `VIZ_PREVIEW_MODE_TOGGLE_TIME_MS` interval. /// Assumes 60fps. We use this value to check against dropped frames during the interval. -const VIZ_PREVIEW_MODE_TOGGLE_FRAMES: i32 = - (VIZ_PREVIEW_MODE_TOGGLE_TIME_MS / 1000.0 * 60.0) as i32; +const VIZ_PREVIEW_MODE_TOGGLE_FRAMES: u64 = + (VIZ_PREVIEW_MODE_TOGGLE_TIME_MS / 1000.0 * 60.0) as u64; const MAX_ZOOM: f32 = 1.0; /// The amount of pixels that the dragged target edge overlaps with the cursor. const CURSOR_EDGE_OVERLAP: f32 = 2.0; @@ -3166,7 +3166,7 @@ fn init_remaining_graph_editor_frp( viz_release <- viz_release_ev.gate(&viz_was_pressed); viz_press_time <- viz_press.map(|_| { let time = web::window.performance_or_panic().now() as f32; - let frame_counter = Rc::new(web::FrameCounter::start_counting()); + let frame_counter = Rc::new(ensogl::animation::FrameCounter::start_counting()); (time, Some(frame_counter)) }); viz_release_time <- viz_release.map(|_| web::window.performance_or_panic().now() as f32); diff --git a/app/gui/view/src/project.rs b/app/gui/view/src/project.rs index 8461db6d206..45cdb041139 100644 --- a/app/gui/view/src/project.rs +++ b/app/gui/view/src/project.rs @@ -43,7 +43,7 @@ use ide_view_project_view_top_bar::ProjectViewTopBar; /// A time which must pass since last change of expression of node which is the searcher input /// to send `searcher_input_changed` event. The delay ensures we don't needlessly update Component /// Browser when user is quickly typing in the expression input. -const INPUT_CHANGE_DELAY_MS: i32 = 200; +const INPUT_CHANGE_DELAY_MS: u32 = 200; /// Mitigate limitations of constant strings concatenation. diff --git a/app/gui/view/welcome-screen/Cargo.toml b/app/gui/view/welcome-screen/Cargo.toml index 08b2b021ef9..68ec0663aef 100644 --- a/app/gui/view/welcome-screen/Cargo.toml +++ b/app/gui/view/welcome-screen/Cargo.toml @@ -11,4 +11,3 @@ crate-type = ["cdylib", "rlib"] ensogl = { path = "../../../../lib/rust/ensogl" } enso-frp = { path = "../../../../lib/rust/frp" } wasm-bindgen = { workspace = true } -web-sys = { version = "0.3.4", features = [] } diff --git a/app/gui/view/welcome-screen/src/lib.rs b/app/gui/view/welcome-screen/src/lib.rs index 0159eecebdc..87d1d893de1 100644 --- a/app/gui/view/welcome-screen/src/lib.rs +++ b/app/gui/view/welcome-screen/src/lib.rs @@ -29,10 +29,8 @@ use ensogl::display::DomSymbol; use ensogl::system::web; use ensogl::system::web::traits::*; use std::rc::Rc; -use web::Closure; use web::Element; use web::HtmlDivElement; -use web::MouseEvent; @@ -63,15 +61,6 @@ mod css_id { // === ClickableElement === // ======================== - -// === ClickClosure === - -/// Type alias for "on-click" event handlers on buttons. -type ClickClosure = Closure; - - -// === ClickableElement === - /// Clickable HTML element. It has a single `click` event source that fires an event on each `click` /// JS event. #[derive(Debug, Clone, CloneRef)] @@ -79,6 +68,7 @@ struct ClickableElement { pub element: Element, pub click: frp::Source, pub network: frp::Network, + listener: web::CleanupHandle, } impl Deref for ClickableElement { @@ -93,10 +83,8 @@ impl ClickableElement { frp::new_network! { network click <- source_(); } - let closure: ClickClosure = Closure::new(f_!(click.emit(()))); - let handle = web::add_event_listener(&element, "click", closure); - network.store(&handle); - Self { element, network, click } + let listener = web::add_event_listener(&element, "click", f_!(click.emit(()))); + Self { element, network, click, listener } } } diff --git a/app/ide-desktop/eslint.config.js b/app/ide-desktop/eslint.config.js index 2e77490b661..d8149cd64ed 100644 --- a/app/ide-desktop/eslint.config.js +++ b/app/ide-desktop/eslint.config.js @@ -123,7 +123,7 @@ const RESTRICTED_SYNTAXES = [ }, { // Matches functions and arrow functions, but not methods. - selector: `:matches(FunctionDeclaration[id.name=${NOT_PASCAL_CASE}]:has(${JSX}), VariableDeclarator[id.name=${NOT_PASCAL_CASE}]:has(:matches(ArrowFunctionExpression.init ${JSX})))`, + selector: `:matches(FunctionDeclaration[id.name=${NOT_PASCAL_CASE}]:has(ReturnStatement:has(${JSX})), VariableDeclarator[id.name=${NOT_PASCAL_CASE}]:has(:matches(ArrowFunctionExpression.init ${JSX})))`, message: 'Use `PascalCase` for React components', }, { diff --git a/app/ide-desktop/lib/client/src/security.ts b/app/ide-desktop/lib/client/src/security.ts index 2cdfab0f6e6..bb5006dd9f4 100644 --- a/app/ide-desktop/lib/client/src/security.ts +++ b/app/ide-desktop/lib/client/src/security.ts @@ -20,7 +20,7 @@ const TRUSTED_HOSTS = [ ] /** The list of hosts that the app can open external links to. */ -const TRUSTED_EXTERNAL_HOSTS = ['discord.gg'] +const TRUSTED_EXTERNAL_HOSTS = ['discord.gg', 'github.com'] /** The list of URLs a new WebView can be pointed to. */ const WEBVIEW_URL_WHITELIST: string[] = [] diff --git a/app/ide-desktop/lib/content/src/index.ts b/app/ide-desktop/lib/content/src/index.ts index 259541ffd89..fbf4dacc3f5 100644 --- a/app/ide-desktop/lib/content/src/index.ts +++ b/app/ide-desktop/lib/content/src/index.ts @@ -11,6 +11,7 @@ import * as dashboard from 'enso-authentication' import * as detect from 'enso-common/src/detect' import * as app from '../../../../../target/ensogl-pack/linked-dist' +import * as panic from './panic' import * as remoteLog from './remoteLog' import GLOBAL_CONFIG from '../../../../gui/config.yaml' assert { type: 'yaml' } @@ -217,6 +218,11 @@ class Main implements AppRunner { logger.log(logMessage) } } + newApp.printPanicMessage = (message: string) => + new Promise(resolve => { + panic.displayPanicMessageToast(message, resolve) + }) + this.app = newApp if (!this.app.initialized) { diff --git a/app/ide-desktop/lib/content/src/panic.tsx b/app/ide-desktop/lib/content/src/panic.tsx new file mode 100644 index 00000000000..da00e05f536 --- /dev/null +++ b/app/ide-desktop/lib/content/src/panic.tsx @@ -0,0 +1,103 @@ +/** @file This file defines a component that is responsible for displaying a user-facing message + * about an application crash (panic). The component is used as a body of a Toastify toast. */ + +import * as React from 'react' +import * as toastify from 'react-toastify' + +import * as detect from 'enso-common/src/detect' + +/** Props for `InternalPanicMessage` component. */ +interface InternalPanicMessageProps { + /** The panic message with stack trace. Usually big multiline text. */ + message: string + /** A callback to trigger application restart. */ + restart: () => void +} + +/** A component displaying panic message inside a toast. */ +function InternalPanicMessage(props: InternalPanicMessageProps) { + return ( +
+

Enso has crashed.

+

+ Enso has encountered a critical error and needs to be restarted. This is a bug, and + we would appreciate it if you could report it to us. +

+

Please include following panic message in your report:

+
+                {props.message}
+            
+
+ + + Report + +
+
+ ) +} + +/** Generate an URL to GitHub issue report template, prefilling as much information as possible. */ +function bugReportUrl(message: string) { + const version = BUILD_INFO.version.endsWith('-dev') ? BUILD_INFO.commit : BUILD_INFO.version + const browserVersion = detect.isRunningInElectron() ? 'standalone' : navigator.userAgent + const logs = bugReportLogs(message) + + const reportUrl = new URL('https://GitHub.com/enso-org/enso/issues/new') + const params = reportUrl.searchParams + params.append('labels', '--bug,triage') + params.append('template', 'bug-report.yml') + params.append('title', 'Panic report') + params.append('enso-version', version) + params.append('browser-version', browserVersion) + params.append('logs', logs) + return reportUrl.toString() +} + +/** Generate a pre-filled log message for GitHub issue report. The output of this function is later + * encoded and used as an URL query parameter. + */ +function bugReportLogs(message: string) { + // Due to URL length limitation, we need to truncate the panic massage that is passed to the + // GitHub issue template. The stacktrace is cut off line per line, up to the limit. The limit + // is chosen somewhat conservatively, but within the limits that GitHub allows. Verified + // empirically. + const maxMessageLength = 5000 + + const lastLineEnd = message.lastIndexOf('\n', maxMessageLength) + const truncatedMessage = message.slice(0, lastLineEnd < 0 ? maxMessageLength : lastLineEnd) + return '```\n' + truncatedMessage + '\n```\n' +} + +/** Display a toast with panic message. */ +export function displayPanicMessageToast(message: string, restartApp: () => void) { + const restart = () => { + restartApp() + toastify.toast.dismiss(toastId) + } + const element = + const toastId = toastify.toast.error(element, { + closeButton: false, + autoClose: false, + style: { + // Allow the toast to fill the screen almost completely, leaving a small margin. + margin: '0 calc((100% - min(100vw - 2rem, 1200px)) / 2)', + maxHeight: 'calc(100vh - 4rem)', + }, + bodyStyle: { + alignItems: 'flex-start', + overflow: 'hidden', + }, + }) +} diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/components/app.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/components/app.tsx index 22d3a348201..5838b7503c8 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/components/app.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/components/app.tsx @@ -137,6 +137,7 @@ export default function App(props: AppProps) { return ( <> div:last-child { + overflow: hidden; +} diff --git a/app/ide-desktop/lib/dashboard/tailwind.config.ts b/app/ide-desktop/lib/dashboard/tailwind.config.ts index 6f2abd160cd..eac56c28a2b 100644 --- a/app/ide-desktop/lib/dashboard/tailwind.config.ts +++ b/app/ide-desktop/lib/dashboard/tailwind.config.ts @@ -7,6 +7,7 @@ import * as url from 'node:url' // ================= const THIS_PATH = path.resolve(path.dirname(url.fileURLToPath(import.meta.url))) +const CONTENT_PATH = path.resolve(THIS_PATH, '../content') // ===================== // === Configuration === @@ -14,7 +15,7 @@ const THIS_PATH = path.resolve(path.dirname(url.fileURLToPath(import.meta.url))) // The names come from a third-party API and cannot be changed. /* eslint-disable no-restricted-syntax, @typescript-eslint/naming-convention */ -export const content = [THIS_PATH + '/src/**/*.tsx'] +export const content = [THIS_PATH + '/src/**/*.tsx', CONTENT_PATH + '/src/**/*.tsx'] export const theme = { extend: { colors: { @@ -100,6 +101,9 @@ export const theme = { '80': '20rem', '96': '24rem', }, + maxHeight: { + 'half-screen': '50vh', + }, opacity: { '1/3': '.33333333', }, diff --git a/build-config.yaml b/build-config.yaml index e3f42116906..67067d59102 100644 --- a/build-config.yaml +++ b/build-config.yaml @@ -6,7 +6,7 @@ required-versions: # NB. The Rust version is pinned in rust-toolchain.toml. # NB. The Node version is pinned in .node-version. cargo-watch: ^8.1.1 - wasm-pack: ^0.10.2 + wasm-pack: ^0.12.1 # TODO [mwu]: Script can install `flatc` later on (if `conda` is present), so this is not required. However it should # be required, if `conda` is missing. # flatc: =1.12.0 diff --git a/build/build/src/config.rs b/build/build/src/config.rs index 9a457ac044a..61da59517eb 100644 --- a/build/build/src/config.rs +++ b/build/build/src/config.rs @@ -144,7 +144,7 @@ mod tests { wasm-size-limit: "4.37MB" required-versions: node: =16.15.0 - wasm-pack: ^0.10.2 + wasm-pack: ^0.12.1 flatc: =1.12.0 "#; let config = serde_yaml::from_str::(config)?; @@ -166,7 +166,7 @@ wasm-size-limit: 15.25 MiB required-versions: cargo-watch: ^8.1.1 node: =16.15.0 - wasm-pack: ^0.10.2 + wasm-pack: ^0.12.1 # TODO [mwu]: Script can install `flatc` later on (if `conda` is present), so this is not required. However it should # be required, if `conda` is missing. # flatc: =1.12.0 diff --git a/build/build/src/project/wasm.rs b/build/build/src/project/wasm.rs index 5fb0c067706..66de5ba3476 100644 --- a/build/build/src/project/wasm.rs +++ b/build/build/src/project/wasm.rs @@ -38,7 +38,7 @@ pub mod test; -pub const BINARYEN_VERSION_TO_INSTALL: u32 = 108; +pub const BINARYEN_VERSION_TO_INSTALL: u32 = 114; pub const DEFAULT_INTEGRATION_TESTS_WASM_TIMEOUT: Duration = Duration::from_secs(300); @@ -263,6 +263,7 @@ impl IsTarget for Wasm { .env_remove(ide_ci::programs::rustup::env::RUSTUP_TOOLCHAIN.name()) .build() .arg(wasm_pack::Profile::from(*profile)) + .reference_types() .target(wasm_pack::Target::Web) .output_directory(args.out_dir) .output_name(args.out_name) @@ -581,6 +582,8 @@ impl Wasm { wasm_opt_command .args(wasm_opt_options) .arg(&temp_dist.pkg_wasm) + .arg("--enable-reference-types") + .arg("--no-validation") .apply(&wasm_opt::Output(&temp_dist.pkg_opt_wasm)) .run_ok() .await?; diff --git a/build/ci_utils/src/programs/wasm_pack.rs b/build/ci_utils/src/programs/wasm_pack.rs index 98eee722c5a..7dec0cb4057 100644 --- a/build/ci_utils/src/programs/wasm_pack.rs +++ b/build/ci_utils/src/programs/wasm_pack.rs @@ -100,6 +100,18 @@ impl WasmPackCommand { pub fn output_name(&mut self, output_name: impl AsRef) -> &mut Self { self.arg("--out-name").arg(output_name.as_ref()) } + + /// Enable wasm-bindgen weak references feature. + /// https://rustwasm.github.io/wasm-bindgen/reference/weak-references.html + pub fn weak_refs(&mut self) -> &mut Self { + self.arg("--weak-refs") + } + + /// Enable wasm-bindgen reference types feature. + /// https://rustwasm.github.io/wasm-bindgen/reference/reference-types.html + pub fn reference_types(&mut self) -> &mut Self { + self.arg("--reference-types") + } } // new_command_type! {WasmPack, WasmPackBuildCommand} diff --git a/lib/rust/ensogl/component/drop-manager/src/lib.rs b/lib/rust/ensogl/component/drop-manager/src/lib.rs index 375705d69a2..638ad720e73 100644 --- a/lib/rust/ensogl/component/drop-manager/src/lib.rs +++ b/lib/rust/ensogl/component/drop-manager/src/lib.rs @@ -28,19 +28,10 @@ use crate::prelude::*; use enso_frp as frp; use enso_web as web; -use enso_web::stream::BlobExt; -use enso_web::stream::ReadableStreamDefaultReader; -use enso_web::Closure; +use enso_web::JsCast; use ensogl_core::display::scene::Scene; use ensogl_core::system::web::dom::WithKnownShape; -#[cfg(target_arch = "wasm32")] -use enso_web::JsCast; -#[cfg(target_arch = "wasm32")] -use js_sys::Uint8Array; -#[cfg(target_arch = "wasm32")] -use wasm_bindgen_futures::JsFuture; - // ============ @@ -61,17 +52,16 @@ pub struct File { pub mime_type: ImString, pub size: u64, #[derivative(Debug = "ignore")] - reader: Rc>, + reader: Rc>, } impl File { - /// Constructor from the [`web_sys::File`]. - pub fn from_js_file(file: &web_sys::File) -> Result { + /// Constructor from the [`web::File`]. + pub fn from_js_file(file: &web::File) -> Result { let name = ImString::new(file.name()); let size = file.size() as u64; let mime_type = ImString::new(file.type_()); - let blob = AsRef::::as_ref(file); - let reader = blob.stream_reader()?; + let reader: web::ReadableStreamDefaultReader = file.stream().get_reader().dyn_into()?; let reader = Rc::new(Some(reader)); Ok(File { name, mime_type, size, reader }) } @@ -86,13 +76,13 @@ impl File { /// https://github.com/w3c/FileAPI/issues/144#issuecomment-570982732. pub async fn read_chunk(&self) -> Result>, web::JsValue> { if let Some(reader) = &*self.reader { - let js_result = JsFuture::from(reader.read()).await?; + let js_result = wasm_bindgen_futures::JsFuture::from(reader.read()).await?; let is_done = js_sys::Reflect::get(&js_result, &"done".into())?.as_bool().unwrap(); if is_done { Ok(None) } else { let chunk = js_sys::Reflect::get(&js_result, &"value".into())?; - let data = chunk.dyn_into::()?.to_vec(); + let data = chunk.dyn_into::()?.to_vec(); Ok(Some(data)) } } else { @@ -113,9 +103,6 @@ impl File { // === DropFileManager === // ======================= -type DropClosure = Closure; -type DragOverClosure = Closure bool>; - #[derive(Clone, Debug, Default)] /// The data emitted by the `files_received` frp endpoint. pub struct DropEventData { @@ -130,15 +117,13 @@ pub struct DropEventData { /// It adds listeners for drag and drop events to the target passed during construction. It provides /// the single frp endpoints emitting a signal when a file is dropped. // NOTE[allow_dead] We allow dead fields here, because they keep living closures and network. -#[derive(Clone, CloneRef, Debug)] +#[derive(Debug)] pub struct Manager { #[allow(dead_code)] - network: frp::Network, - files_received: frp::Source, + network: frp::Network, + files_received: frp::Source, #[allow(dead_code)] - drop_handle: web::EventListenerHandle, - #[allow(dead_code)] - drag_over_handle: web::EventListenerHandle, + handlers: [web::CleanupHandle; 2], } impl Manager { @@ -151,21 +136,18 @@ impl Manager { } let target: &web::EventTarget = dom.deref(); - let drop: DropClosure = Closure::new(f!([files_received,scene](event:web_sys::DragEvent) { - debug!("Dropped files."); - event.prevent_default(); - Self::handle_drop_event(event, &files_received, &scene); - })); - // To mark element as a valid drop target, the `dragover` event handler should return - // `false`. See - // https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/File_drag_and_drop#define_the_drop_zone - let drag_over: DragOverClosure = Closure::new(|event: web_sys::DragEvent| { - event.prevent_default(); - false - }); - let drop_handle = web::add_event_listener(target, "drop", drop); - let drag_over_handle = web::add_event_listener(target, "dragover", drag_over); - Self { network, files_received, drop_handle, drag_over_handle } + let received = files_received.clone_ref(); + let scene = scene.clone_ref(); + let handlers = [ + web::add_event_listener(target, "drop", move |event| { + let event: web::DragEvent = event.dyn_into().unwrap(); + debug!("Dropped files."); + event.prevent_default(); + Self::handle_drop_event(event, &received, &scene); + }), + web::add_event_listener(target, "dragover", |e| e.prevent_default()), + ]; + Self { network, files_received, handlers } } /// The frp endpoint emitting signal when a file is dropped. @@ -174,7 +156,7 @@ impl Manager { } /// Retrieve the position of the drop event in the scene coordinates. - fn event_position(scene: &Scene, event: &web_sys::DragEvent) -> Vector2 { + fn event_position(scene: &Scene, event: &web::DragEvent) -> Vector2 { let dom: WithKnownShape = scene.dom.root.clone_ref().into(); let shape = dom.shape.value(); let base = Vector2::new(0.0, shape.height); @@ -186,7 +168,7 @@ impl Manager { } fn handle_drop_event( - event: web_sys::DragEvent, + event: web::DragEvent, files_received: &frp::Source, scene: &Scene, ) { diff --git a/lib/rust/ensogl/component/scrollbar/src/lib.rs b/lib/rust/ensogl/component/scrollbar/src/lib.rs index 0758646681f..ee844b3067f 100644 --- a/lib/rust/ensogl/component/scrollbar/src/lib.rs +++ b/lib/rust/ensogl/component/scrollbar/src/lib.rs @@ -73,9 +73,9 @@ const MIN_THUMB_SIZE: f32 = 12.0; /// After an animation, the thumb will be visible for this time, before it hides again. const HIDE_DELAY: f32 = 1000.0; /// Time delay before holding down a mouse button triggers first scroll, in milliseconds. -const CLICK_AND_HOLD_DELAY_MS: i32 = 500; +const CLICK_AND_HOLD_DELAY_MS: u32 = 500; /// Time interval between scrolls while holding down a mouse button, in milliseconds. -const CLICK_AND_HOLD_INTERVAL_MS: i32 = 200; +const CLICK_AND_HOLD_INTERVAL_MS: u32 = 200; /// Minimum scroll movement in pixels per frame required to show the scrollbar. const ERROR_MARGIN_FOR_ACTIVITY_DETECTION: f32 = 0.1; diff --git a/lib/rust/ensogl/component/text/src/font/msdf/build.rs b/lib/rust/ensogl/component/text/src/font/msdf/build.rs index 06b3f4f6e16..5434be04bb3 100644 --- a/lib/rust/ensogl/component/text/src/font/msdf/build.rs +++ b/lib/rust/ensogl/component/text/src/font/msdf/build.rs @@ -14,7 +14,7 @@ pub const PACKAGE: GithubRelease<&str> = GithubRelease { }; const PATCH_LINE: &str = - "; export { ccall, getValue, _msdfgen_getKerning, _msdfgen_setVariationAxis,\ + "; module.exports = { ccall, getValue, _msdfgen_getKerning, _msdfgen_setVariationAxis,\ _msdfgen_generateAutoframedMSDF, _msdfgen_generateAutoframedMSDFByIndex, \ _msdfgen_result_getMSDFData, _msdfgen_result_getAdvance, _msdfgen_result_getTranslation,\ _msdfgen_result_getScale, _msdfgen_freeResult, _msdfgen_freeFont,\ diff --git a/lib/rust/ensogl/core/Cargo.toml b/lib/rust/ensogl/core/Cargo.toml index 8394d8aa18b..2e3a9619ab7 100644 --- a/lib/rust/ensogl/core/Cargo.toml +++ b/lib/rust/ensogl/core/Cargo.toml @@ -33,7 +33,6 @@ ensogl-text-embedded-fonts = { path = "../component/text/src/font/embedded" } bit_field = { version = "0.10.0" } bitflags = { workspace = true } bytemuck = { workspace = true } -console_error_panic_hook = { workspace = true } enum_dispatch = { version = "0.3.6" } failure = { workspace = true } futures = { workspace = true } diff --git a/lib/rust/ensogl/core/src/animation/loops.rs b/lib/rust/ensogl/core/src/animation/loops.rs index 64fd84c9614..b98902815dc 100644 --- a/lib/rust/ensogl/core/src/animation/loops.rs +++ b/lib/rust/ensogl/core/src/animation/loops.rs @@ -1,7 +1,6 @@ //! This module contains implementation of loops mainly used for per-frame callbacks firing. use crate::prelude::*; -use crate::system::web::traits::*; use enso_callback::traits::*; use crate::system::web; @@ -9,7 +8,7 @@ use crate::types::unit2::Duration; use enso_callback as callback; use frp::microtasks::TickPhases; -use web::Closure; +use web::CleanupHandle; @@ -76,16 +75,7 @@ where OnFrame: RawOnFrameCallback { /// Create and start a new animation loop. fn new(on_frame: OnFrame) -> Self { - let data = Rc::new(RefCell::new(JsLoopData::new(on_frame))); - let weak_data = Rc::downgrade(&data); - let js_on_frame = - move |time: f64| weak_data.upgrade().for_each(|t| t.borrow_mut().run(time)); - data.borrow_mut().js_on_frame = Some(Closure::new(js_on_frame)); - let js_on_frame_handle_id = web::window.request_animation_frame_with_closure_or_panic( - data.borrow_mut().js_on_frame.as_ref().unwrap(), - ); - data.borrow_mut().js_on_frame_handle_id = js_on_frame_handle_id; - Self { data } + Self { data: Rc::new_cyclic(|weak| RefCell::new(JsLoopData::new(weak.clone(), on_frame))) } } } @@ -94,37 +84,35 @@ where OnFrame: RawOnFrameCallback #[derivative(Debug(bound = ""))] struct JsLoopData { #[derivative(Debug = "ignore")] - on_frame: OnFrame, - js_on_frame: Option>, - js_on_frame_handle_id: i32, + on_frame: OnFrame, + weak_self: Weak>, + on_frame_handle: CleanupHandle, } -impl JsLoopData { +impl JsLoopData { /// Constructor. - fn new(on_frame: OnFrame) -> Self { - let js_on_frame = default(); - let js_on_frame_handle_id = default(); - Self { on_frame, js_on_frame, js_on_frame_handle_id } + fn new(weak_self: Weak>, on_frame: OnFrame) -> Self { + let on_frame_handle = Self::register_next_frame(weak_self.clone()); + Self { on_frame, weak_self, on_frame_handle } } - // FIXME: We are converting `f64` to `f32` here which is a mistake. We should revert to `f64` - // for a better time precision. - fn run(&mut self, current_time_ms: f64) - where OnFrame: FnMut(Duration) { - let on_frame = &mut self.on_frame; - self.js_on_frame_handle_id = self.js_on_frame.as_ref().map_or(default(), |js_on_frame| { - on_frame((current_time_ms as f32).ms()); - web::window.request_animation_frame_with_closure_or_panic(js_on_frame) + fn run(&mut self, current_time_ms: f64) { + // FIXME: We are converting `f64` to `f32` here which is a mistake. We should use `f64` for + // a better time precision. + let time_in_ms_f32 = (current_time_ms as f32).ms(); + (self.on_frame)(time_in_ms_f32); + self.on_frame_handle = Self::register_next_frame(self.weak_self.clone()); + } + + fn register_next_frame(weak_self: Weak>) -> CleanupHandle { + web::request_animation_frame(move |time| { + if let Some(strong) = weak_self.upgrade() { + strong.borrow_mut().run(time); + } }) } } -impl Drop for JsLoopData { - fn drop(&mut self) { - web::window.cancel_animation_frame_or_warn(self.js_on_frame_handle_id); - } -} - // ============ @@ -512,3 +500,36 @@ pub mod test_utils { }); } } + + + +// ==================== +// === FrameCounter === +// ==================== + +/// A counter that counts the number of frames that have passed since its initialization. +/// +/// Uses `request_animation_frame` under the hood to count frames. +#[derive(Debug)] +pub struct FrameCounter { + frames: Rc>, + js_loop: Loop, +} + +impl FrameCounter { + /// Creates a new frame counter. + pub fn start_counting() -> Self { + let frames: Rc> = default(); + Self { + frames: frames.clone(), + js_loop: Loop::new_before_animations(move |_| { + frames.update(|f| f + 1); + }), + } + } + + /// Returns the number of frames that have passed since the counter was created. + pub fn frames_since_start(&self) -> u64 { + self.frames.as_ref().get() + } +} diff --git a/lib/rust/ensogl/core/src/application.rs b/lib/rust/ensogl/core/src/application.rs index 24229e31caf..78d61049fd6 100644 --- a/lib/rust/ensogl/core/src/application.rs +++ b/lib/rust/ensogl/core/src/application.rs @@ -107,8 +107,6 @@ impl Application { frp.private.output.tooltip <+ frp.private.input.set_tooltip; } - // We hide the system cursor to replace it with the EnsoGL-provided one. - self.frp.hide_system_cursor(); self } diff --git a/lib/rust/ensogl/core/src/control/io/keyboard/dom.rs b/lib/rust/ensogl/core/src/control/io/keyboard/dom.rs index 29904ab7004..7ae65ce0646 100644 --- a/lib/rust/ensogl/core/src/control/io/keyboard/dom.rs +++ b/lib/rust/ensogl/core/src/control/io/keyboard/dom.rs @@ -39,25 +39,19 @@ macro_rules! define_bindings { fn connect( &self, target: &$crate::system::web::EventTarget, - ) -> Rc<[web::EventListenerHandle]> { + ) -> Rc<[web::CleanupHandle]> { use crate::control::callback::traits::*; use web::JsCast; - type EventJsClosure = web::Closure; $( let dispatcher = self.$name.clone_ref(); - let closure = move |event: web::JsValue| { + let $name = web::add_event_listener(target, stringify!($js_name), move |event| { let _profiler = profiler::start_task!( profiler::APP_LIFETIME, concat!("keyboard_", stringify!($name)) ); let event = event.unchecked_into::(); dispatcher.run_all(&$event::new(event)) - }; - let closure: EventJsClosure = web::Closure::new(closure); - let js_name = stringify!($js_name); - let opt = event_listener_options(); - let $name = - web::add_event_listener_with_options(&target, js_name, closure, opt); + }); )* Rc::new([$($name),*]) } @@ -77,13 +71,7 @@ macro_rules! define_bindings { pub struct KeyboardManager { #[deref] dispatchers: EventDispatchers, - handles: Rc<[web::EventListenerHandle]>, -} - -/// Return options for addEventListener function. See also -/// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener -fn event_listener_options() -> web::EventListenerHandleOptions { - default() + handles: Rc<[web::CleanupHandle]>, } define_bindings! { diff --git a/lib/rust/ensogl/core/src/control/io/mouse.rs b/lib/rust/ensogl/core/src/control/io/mouse.rs index 03a20e7cb1e..b5860de4a69 100644 --- a/lib/rust/ensogl/core/src/control/io/mouse.rs +++ b/lib/rust/ensogl/core/src/control/io/mouse.rs @@ -43,27 +43,26 @@ macro_rules! define_bindings { dom: &$crate::system::web::dom::WithKnownShape, $target: &$crate::system::web::EventTarget, $global_target: &$crate::system::web::EventTarget, - ) -> Rc<[web::EventListenerHandle]> { + ) -> Rc<[web::CleanupHandle]> { use crate::control::callback::traits::*; use web::JsCast; - type EventJsClosure = web::Closure; $( let shape = dom.shape.clone_ref(); let dispatcher = self.$name.clone_ref(); - let closure = move |event: web::JsValue| { - let _profiler = profiler::start_task!( - profiler::APP_LIFETIME, - concat!("mouse_", stringify!($name)) - ); - let shape = shape.value(); - let event = event.unchecked_into::(); - dispatcher.run_all(&$event::new(event, shape)) - }; - let closure: EventJsClosure = web::Closure::new(closure); - let js_name = stringify!($js_name); - let opt = event_listener_options(); - let $name = web::add_event_listener_with_options - (&$event_target, js_name, closure, opt); + let $name = web::add_event_listener_with_options( + &$event_target, + stringify!($js_name), + event_listener_options(), + move |event| { + let _profiler = profiler::start_task!( + profiler::APP_LIFETIME, + concat!("mouse_", stringify!($name)) + ); + let shape = shape.value(); + let event = event.unchecked_into::(); + dispatcher.run_all(&$event::new(event, shape)) + } + ); )* Rc::new([$($name),*]) } @@ -83,18 +82,18 @@ macro_rules! define_bindings { pub struct MouseManager { #[deref] dispatchers: EventDispatchers, - handles: Rc<[web::EventListenerHandle]>, + handles: Rc<[web::CleanupHandle]>, } /// Return options for addEventListener function. See also /// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener -fn event_listener_options() -> web::EventListenerHandleOptions { +fn event_listener_options() -> web::ListenerOptions { // 1. We listen for events in the bubbling phase. If we ever would like to listen in the capture // phase, it would need to be set to "bubbling" for the "mouseleave" and "mouseenter" events, // as they provide incorrect events for the "capture" phase. // // 2. We want to prevent default action on wheel events, thus listener cannot be passive. - web::EventListenerHandleOptions::new().not_passive() + web::ListenerOptions::new().not_passive() } define_bindings! { target, global_target, diff --git a/lib/rust/ensogl/core/src/debug/monitor.rs b/lib/rust/ensogl/core/src/debug/monitor.rs index 92127103593..194ed07f7be 100644 --- a/lib/rust/ensogl/core/src/debug/monitor.rs +++ b/lib/rust/ensogl/core/src/debug/monitor.rs @@ -181,10 +181,8 @@ impl Config { #[derive(Clone, Debug, Deref)] pub struct Dom { #[deref] - rc: Rc, - on_pause_press: Rc>, - on_play_press: Rc>, - on_main_press: Rc>, + rc: Rc, + listeners: [web::CleanupHandle; 3], } /// Internal representation of `Dom`. @@ -203,20 +201,13 @@ impl Dom { pub fn new(frp: &api::Public, config: &Config, screen_shape: Shape) -> Self { let data = DomData::new(config, screen_shape); let button = &data.control_button; - let on_pause_press = web::Closure::::new(f!(frp.pause_data_processing())); - let on_play_press = web::Closure::::new(f!(frp.resume_data_processing())); - let on_main_press = web::Closure::::new(f!(frp.on_main_press())); - let on_pause_press_fn = on_pause_press.as_ref().unchecked_ref(); - let on_play_press_fn = on_play_press.as_ref().unchecked_ref(); - let on_main_press_fn = on_main_press.as_ref().unchecked_ref(); - button.pause.add_event_listener_with_callback("mousedown", on_pause_press_fn).unwrap(); - button.play.add_event_listener_with_callback("mousedown", on_play_press_fn).unwrap(); - data.plot_area.add_event_listener_with_callback("mousedown", on_main_press_fn).unwrap(); + let listeners = [ + web::add_event_listener(&button.pause, "mousedown", f_!(frp.pause_data_processing())), + web::add_event_listener(&button.play, "mousedown", f_!(frp.resume_data_processing())), + web::add_event_listener(&data.plot_area, "mousedown", f_!(frp.on_main_press())), + ]; let rc = Rc::new(data); - let on_pause_press = Rc::new(on_pause_press); - let on_play_press = Rc::new(on_play_press); - let on_main_press = Rc::new(on_main_press); - Self { rc, on_pause_press, on_play_press, on_main_press } + Self { rc, listeners } } } diff --git a/lib/rust/ensogl/core/src/display/scene.rs b/lib/rust/ensogl/core/src/display/scene.rs index 0ea12709cab..41c4679e6f8 100644 --- a/lib/rust/ensogl/core/src/display/scene.rs +++ b/lib/rust/ensogl/core/src/display/scene.rs @@ -34,7 +34,7 @@ use crate::system::gpu::shader; use crate::system::gpu::Context; use crate::system::gpu::ContextLostHandler; use crate::system::web; -use crate::system::web::EventListenerHandle; +use crate::system::web::CleanupHandle; use enso_frp as frp; use enso_shapely::shared; @@ -1009,7 +1009,7 @@ pub struct SceneData { initial_shader_compilation: Rc>, display_mode: Rc>, extensions: Extensions, - disable_context_menu: Rc, + disable_context_menu: CleanupHandle, } impl SceneData { @@ -1032,7 +1032,7 @@ impl SceneData { let style_sheet = world::with_context(|t| t.style_sheet.clone_ref()); let frp = Frp::new(&dom.root.shape); let mouse = Mouse::new(&frp, &display_object, &dom.root, &variables, &display_mode); - let disable_context_menu = Rc::new(web::ignore_context_menu(&dom.root)); + let disable_context_menu = web::ignore_context_menu(&dom.root); let global_keyboard = Keyboard::new(&web::window, &display_object); let network = &frp.network; let extensions = Extensions::default(); diff --git a/lib/rust/ensogl/core/src/display/world.rs b/lib/rust/ensogl/core/src/display/world.rs index 315bdaa5435..8204c9a4ef3 100644 --- a/lib/rust/ensogl/core/src/display/world.rs +++ b/lib/rust/ensogl/core/src/display/world.rs @@ -29,9 +29,7 @@ use crate::system::gpu::shader; use crate::system::web; use enso_types::unit2::Duration; -use web::prelude::Closure; use web::JsCast; -use web::JsValue; // ============== @@ -429,7 +427,7 @@ pub struct WorldData { stats: Stats, stats_monitor: debug::monitor::Monitor, pub on: Callbacks, - debug_hotkeys_handle: Rc>>, + debug_hotkeys_handle: Rc>>, update_themes_handle: callback::Handle, garbage_collector: garbage::Collector, emit_measurements_handle: Rc>>, @@ -496,7 +494,7 @@ impl WorldData { let display_mode = self.display_mode.clone_ref(); let display_mode_uniform = with_context(|ctx| ctx.display_mode.clone_ref()); let emit_measurements_handle = self.emit_measurements_handle.clone_ref(); - let closure: Closure = Closure::new(move |val: JsValue| { + let closure = move |val: web::Event| { let event = val.unchecked_into::(); let digit_prefix = "Digit"; if event.alt_key() && event.ctrl_key() { @@ -531,8 +529,10 @@ impl WorldData { display_mode_uniform.set(code_value as i32); } } - }); - let handle = web::add_event_listener_with_bool(&web::window, "keydown", closure, true); + }; + let options = web::ListenerOptions::new().capture(); + let handle = + web::add_event_listener_with_options(&web::window, "keydown", options, closure); *self.debug_hotkeys_handle.borrow_mut() = Some(handle); } diff --git a/lib/rust/ensogl/core/src/system/gpu/context.rs b/lib/rust/ensogl/core/src/system/gpu/context.rs index eaf4e298893..596e4d7bea7 100644 --- a/lib/rust/ensogl/core/src/system/gpu/context.rs +++ b/lib/rust/ensogl/core/src/system/gpu/context.rs @@ -7,7 +7,6 @@ use crate::system::gpu::data::GlEnum; use crate::system::gpu::shader; use crate::system::web; -use web::Closure; use web_sys::WebGl2RenderingContext; @@ -94,8 +93,8 @@ pub type DeviceContextHandler = web::HtmlCanvasElement; /// the context, the context will not be restored automaticaly. #[derive(Debug)] pub struct ContextLostHandler { - on_lost: web::EventListenerHandle, - on_restored: web::EventListenerHandle, + on_lost: web::CleanupHandle, + on_restored: web::CleanupHandle, } @@ -150,14 +149,14 @@ pub fn init_webgl_2_context( let context = Context::from_native(native); type Handler = web::JsEventHandler; display.set_context(Some(&context)); - let lost: Handler = Closure::new(f_!([display] + let lost = f_!([display] { warn!("Lost the WebGL context."); display.set_context(None) - )); - let restored: Handler = Closure::new(f_!([display] + }); + let restored = f_!([display] { warn!("Trying to restore the WebGL context."); display.set_context(Some(&context)) - )); + }); let on_lost = web::add_event_listener(hdc, "webglcontextlost", lost); let on_restored = web::add_event_listener(hdc, "webglcontextrestored", restored); Ok(ContextLostHandler { on_lost, on_restored }) diff --git a/lib/rust/ensogl/core/src/system/gpu/data/texture/storage/remote_image.rs b/lib/rust/ensogl/core/src/system/gpu/data/texture/storage/remote_image.rs index 7ae72015d7f..b0671df61c7 100644 --- a/lib/rust/ensogl/core/src/system/gpu/data/texture/storage/remote_image.rs +++ b/lib/rust/ensogl/core/src/system/gpu/data/texture/storage/remote_image.rs @@ -8,8 +8,6 @@ use crate::system::gpu::Context; #[cfg(target_arch = "wasm32")] use crate::system::web; -#[cfg(target_arch = "wasm32")] -use web::Closure; #[cfg(target_arch = "wasm32")] use web_sys::HtmlImageElement; @@ -87,7 +85,7 @@ impl TextureReload for Texture>::None; + let no_callback = >::None; let callback_ref = Rc::new(RefCell::new(no_callback)); let image_ref = Rc::new(RefCell::new(image)); let callback_ref2 = callback_ref.clone(); @@ -96,8 +94,8 @@ impl TextureReload for Texture TextureReload for Texture WithKnownShape { shape_source <- source(); shape <- shape_source.sampler(); }; - let callback = Closure::new(f!([shape_source, overridden_pixel_ratio] (w,h) - shape_source.emit(Shape::new(w, h, overridden_pixel_ratio.get())))); - let observer = Rc::new(ResizeObserver::new(dom.as_ref(), callback)); + let observer = Rc::new(ResizeObserver::new( + dom.as_ref(), + f!([shape_source, overridden_pixel_ratio] (w, h) { + shape_source.emit(Shape::new(w, h, overridden_pixel_ratio.get())) + }), + )); shape_source.emit(Shape::new_from_element_with_reflow(&element)); Self { dom, network, shape, shape_source, observer, overridden_pixel_ratio } } diff --git a/lib/rust/ensogl/examples/text-area/src/lib.rs b/lib/rust/ensogl/examples/text-area/src/lib.rs index fa8a6ff32eb..51974c8e23b 100644 --- a/lib/rust/ensogl/examples/text-area/src/lib.rs +++ b/lib/rust/ensogl/examples/text-area/src/lib.rs @@ -30,12 +30,10 @@ use ensogl_core::application::Application; use ensogl_core::data::color; use ensogl_core::display::navigation::navigator::Navigator; use ensogl_core::display::Scene; -use ensogl_core::frp::io::timer::DocumentOps; -use ensogl_core::frp::io::timer::HtmlElementOps; use ensogl_core::system::web; -use ensogl_core::system::web::Closure; +use ensogl_core::system::web::traits::DocumentOps; +use ensogl_core::system::web::traits::HtmlElementOps; use ensogl_core::system::web::JsCast; -use ensogl_core::system::web::JsValue; use ensogl_text::buffer; use ensogl_text::formatting; use ensogl_text::Text; @@ -228,7 +226,7 @@ fn init_debug_hotkeys(scene: &Scene, area: &Rc>>, div: &web let area = area.clone_ref(); let div = div.clone(); let mut fonts_cycle = ["enso", "mplus1p"].iter().cycle(); - let closure: Closure = Closure::new(move |val: JsValue| { + let closure = move |val: web::Event| { let event = val.unchecked_into::(); if event.ctrl_key() { let key = event.code(); @@ -318,7 +316,8 @@ fn init_debug_hotkeys(scene: &Scene, area: &Rc>>, div: &web } } } - }); - let handle = web::add_event_listener_with_bool(&web::window, "keydown", closure, true); + }; + let options = web::ListenerOptions::new().capture(); + let handle = web::add_event_listener_with_options(&web::window, "keydown", options, closure); mem::forget(handle); } diff --git a/lib/rust/ensogl/pack/js/package-lock.json b/lib/rust/ensogl/pack/js/package-lock.json index a4c357c5566..319e13970f6 100644 --- a/lib/rust/ensogl/pack/js/package-lock.json +++ b/lib/rust/ensogl/pack/js/package-lock.json @@ -16,7 +16,6 @@ "esbuild": "^0.16.12", "eslint": "^8.30.0", "eslint-plugin-jsdoc": "^39.6.4", - "ts-node": "^10.9.1", "tsup": "^6.5.0", "typescript": "^4.9.4" }, @@ -31,6 +30,8 @@ "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "@jridgewell/trace-mapping": "0.3.9" }, @@ -465,6 +466,8 @@ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", "dev": true, + "optional": true, + "peer": true, "engines": { "node": ">=6.0.0" } @@ -473,13 +476,17 @@ "version": "1.4.14", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -524,25 +531,33 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/@tsconfig/node16": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/@types/json-schema": { "version": "7.0.11", @@ -555,6 +570,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==", "dev": true, + "optional": true, "peer": true }, "node_modules/@types/semver": { @@ -777,6 +793,8 @@ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", "dev": true, + "optional": true, + "peer": true, "engines": { "node": ">=0.4.0" } @@ -844,7 +862,9 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/argparse": { "version": "2.0.1", @@ -1032,7 +1052,9 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/cross-spawn": { "version": "7.0.3", @@ -1076,6 +1098,8 @@ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true, + "optional": true, + "peer": true, "engines": { "node": ">=0.3.1" } @@ -2206,7 +2230,9 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/merge-stream": { "version": "2.0.0", @@ -2864,6 +2890,8 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -3096,7 +3124,9 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/webidl-conversions": { "version": "4.0.2", @@ -3165,6 +3195,8 @@ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "dev": true, + "optional": true, + "peer": true, "engines": { "node": ">=6" } @@ -3188,6 +3220,8 @@ "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", "dev": true, + "optional": true, + "peer": true, "requires": { "@jridgewell/trace-mapping": "0.3.9" } @@ -3401,19 +3435,25 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "@jridgewell/sourcemap-codec": { "version": "1.4.14", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "@jridgewell/trace-mapping": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", "dev": true, + "optional": true, + "peer": true, "requires": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -3449,25 +3489,33 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "@tsconfig/node16": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "@types/json-schema": { "version": "7.0.11", @@ -3480,6 +3528,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==", "dev": true, + "optional": true, "peer": true }, "@types/semver": { @@ -3604,7 +3653,9 @@ "version": "8.2.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "ajv": { "version": "6.12.6", @@ -3653,7 +3704,9 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "argparse": { "version": "2.0.1", @@ -3793,7 +3846,9 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "cross-spawn": { "version": "7.0.3", @@ -3825,7 +3880,9 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "dir-glob": { "version": "3.0.1", @@ -4570,7 +4627,9 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "merge-stream": { "version": "2.0.0", @@ -5027,6 +5086,8 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", "dev": true, + "optional": true, + "peer": true, "requires": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -5166,7 +5227,9 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "webidl-conversions": { "version": "4.0.2", @@ -5222,7 +5285,9 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "yocto-queue": { "version": "0.1.0", diff --git a/lib/rust/ensogl/pack/js/package.json b/lib/rust/ensogl/pack/js/package.json index 8b8e702b60d..4ad26ad26e6 100644 --- a/lib/rust/ensogl/pack/js/package.json +++ b/lib/rust/ensogl/pack/js/package.json @@ -18,7 +18,7 @@ "typecheck": "npx tsc --noEmit", "build": "npx --yes tsup src/runner/index.ts --format=cjs --dts --sourcemap", "build-asset-extractor": "npx --yes tsup --format=cjs --target=esnext src/asset-extractor/asset-extractor.ts --dts --sourcemap", - "build-runtime-libs": "npx --yes esbuild --bundle --platform=node --format=cjs src/runtime-libs/runtime-libs.ts", + "build-runtime-libs": "npx --yes esbuild --bundle --format=esm src/runtime-libs/runtime-libs.ts", "lint": "npx --yes eslint src" }, "bugs": { @@ -30,7 +30,6 @@ "esbuild": "^0.16.12", "eslint": "^8.30.0", "eslint-plugin-jsdoc": "^39.6.4", - "ts-node": "^10.9.1", "tsup": "^6.5.0", "typescript": "^4.9.4" }, diff --git a/lib/rust/ensogl/pack/js/src/runner/index.ts b/lib/rust/ensogl/pack/js/src/runner/index.ts index d37553a1588..a37c67dc858 100644 --- a/lib/rust/ensogl/pack/js/src/runner/index.ts +++ b/lib/rust/ensogl/pack/js/src/runner/index.ts @@ -186,6 +186,7 @@ export class App { packageInfo: debug.PackageInfo config: config.Options wasm: any = null + disposeModule: (() => void) | null = null loader: wasm.Loader | null = null assets: Assets | null = null wasmFunctions: string[] = [] @@ -238,8 +239,8 @@ export class App { } /** Log the message on the remote server. */ - // This method is assumed to be overriden by the App's owner. Eventually it should be removed from the runner - // altogether, as it is not its responsibility. + // This method is assumed to be overridden by the App's owner. Eventually it should be removed + // from the runner altogether, as it is not its responsibility. // eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars async remoteLog(message: string, data: any) { console.warn('Remote logging is not set up.') @@ -301,9 +302,12 @@ export class App { `const process = "Overridden to prevent Emscripten from redefining module.exports." const module = {} ${pkgJs} + module.exports.disposeModule = () => wasm = undefined; return module.exports` )() const out: unknown = await snippetsFn.init(wasm) + this.disposeModule = snippetsFn.disposeModule + if (this.config.groups.debug.options.enableSpector.value) { /* eslint @typescript-eslint/no-unsafe-member-access: "off" */ /* eslint @typescript-eslint/no-unsafe-call: "off" */ @@ -319,8 +323,8 @@ export class App { }) } - /** Download and load the WASM to memory. */ - async loadWasm() { + /** Request wasm and asset files. */ + async requestWasm() { const loader = new wasm.Loader(this.config) const assetsUrl = this.config.groups.loader.options.assetsUrl.value @@ -371,11 +375,18 @@ export class App { const data = new Map(Array.from(info.data, ([k, i]) => [k, assetsBlobs[i]!])) return new Asset(info.type, info.key, data) }) - - const pkgJs = await responses.pkgJs.text() this.loader = loader - this.wasm = await this.compileAndRunWasm(pkgJs, responses.pkgWasm) this.assets = new Assets(assets) + return { + pkgJs: await responses.pkgJs.text(), + pkgWasm: responses.pkgWasm, + } + } + + /** Download and load the WASM to memory. */ + async loadWasm() { + const response = await this.requestWasm() + this.wasm = await this.compileAndRunWasm(response.pkgJs, response.pkgWasm) } /** Loads the WASM binary and its dependencies. After the files are fetched, the WASM module is @@ -558,6 +569,33 @@ export class App { rustSetAssetFn(builder, key, data) } } + + /** + * Handle a panic from Rust. Displays a user-friendly error message and allows easy bug + * reporting. Once panic happened, the wasm module must be closed and reloaded from scratch, as + * no state can be trusted. There are some challenges with this, as we are not able to directly + * stop the execution. + */ + async handlePanic(message: string) { + // Remove all references to the old wasm module. The garbage collector should be able to + // collect it. We also have to cleanup the scene DOM, as the drop handlers cannot be called. + this.disposeModule?.() + this.wasm = null + document.querySelector('#root > .scene')?.remove() + + await this.printPanicMessage(message) + // Once the panic message is acknowledged by the user, restart the application. + await this.run() + } + + /** + * Present the panic message to the user. This method is assumed to be overridden by the App's + * owner. Resolving the promise returned by this method will cause the application to restart. + */ + // eslint-disable-next-line @typescript-eslint/require-await + async printPanicMessage(message: string): Promise { + console.error(message) + } } // ========================== diff --git a/lib/rust/frp/src/io/timer.rs b/lib/rust/frp/src/io/timer.rs index 43bf5c9fa64..2367ac322f9 100644 --- a/lib/rust/frp/src/io/timer.rs +++ b/lib/rust/frp/src/io/timer.rs @@ -1,11 +1,58 @@ //! Browser timer handlers wrapped in FRP API. +use crate::prelude::*; + mod delayed_interval; mod interval; mod timeout; + +// ============== +// === Export === +// ============== + pub use delayed_interval::*; pub use interval::*; pub use timeout::*; + + + +// ================== +// === RawTimeout === +// ================== + +/// A common low-level abstraction for a reconfigurable JS timer with a non-changing `Fn` handler. +#[derive(Derivative)] +#[derivative(Debug)] +pub(self) struct RawTimer { + timer_handle: RefCell>, + #[derivative(Debug = "ignore")] + handler: Rc, +} + +impl RawTimer { + /// Create a new timer with a given handler function. The function will be called whenever the + /// timer fires (either by timeout expiration or interval). + fn new(handler: impl Fn() + 'static) -> Self { + Self { handler: Rc::new(handler), timer_handle: default() } + } + + /// Start the timer with a given timeout. The timer will be stopped if it was already running. + fn set_timeout(&self, time: u32) { + let handler = self.handler.clone(); + self.timer_handle.replace(Some(enso_web::set_timeout(move || handler(), time))); + } + + /// Start the timer with a given interval. The timer will be stopped if it was already running. + fn set_interval(&self, time: u32) { + let handler = self.handler.clone(); + self.timer_handle.replace(Some(enso_web::set_interval(move || handler(), time))); + } + + /// Stop the timer if it was running. + fn stop(&self) { + self.timer_handle.take(); + } +} diff --git a/lib/rust/frp/src/io/timer/delayed_interval.rs b/lib/rust/frp/src/io/timer/delayed_interval.rs index a6e5d31bcba..0fe8238e215 100644 --- a/lib/rust/frp/src/io/timer/delayed_interval.rs +++ b/lib/rust/frp/src/io/timer/delayed_interval.rs @@ -75,14 +75,14 @@ impl DelayedInterval { #[derive(Debug, Clone, Copy, Default)] pub struct DelayedIntervalConfig { /// Initial delay between timer activation and first `on_trigger` event being emitted. - pub delay_ms: i32, + pub delay_ms: u32, /// Time between subsequent `on_trigger` events. - pub interval_ms: i32, + pub interval_ms: u32, } impl DelayedIntervalConfig { /// Constructor. - pub fn new(delay_ms: i32, interval_ms: i32) -> Self { + pub fn new(delay_ms: u32, interval_ms: u32) -> Self { Self { delay_ms, interval_ms } } } diff --git a/lib/rust/frp/src/io/timer/interval.rs b/lib/rust/frp/src/io/timer/interval.rs index 0d8b49cd970..8fab3986e63 100644 --- a/lib/rust/frp/src/io/timer/interval.rs +++ b/lib/rust/frp/src/io/timer/interval.rs @@ -4,15 +4,7 @@ use crate::prelude::*; use crate as frp; -use enso_web::window; -use enso_web::Closure; - - -// ============== -// === Export === -// ============== - -pub use enso_web::traits::*; +use super::RawTimer; @@ -20,9 +12,6 @@ pub use enso_web::traits::*; // === Interval === // ================ -/// Closure type alias for use in `setInterval` call. -type TimerClosure = Closure; - /// Periodic timer. /// /// The timer can be started or stopped at any time using `restart` and `stop` inputs. After it is @@ -40,71 +29,31 @@ type TimerClosure = Closure; pub struct Interval { /// Starts the timer with provided period value, specified in integer milliseconds. If the /// timer was already started, it is restarted with new period value. - pub restart: frp::Any, + pub restart: frp::Any, /// Stops the timer. No `on_interval` events will be emitted until it is started again. pub stop: frp::Any, /// Triggered periodically after the timer is started. pub on_interval: frp::Stream<()>, - raw_interval: Rc, + raw_interval: Rc, } impl Interval { /// Constructor. Timer is initially not started. pub fn new(network: &frp::Network) -> Self { frp::extend! { network - on_interval <- any_mut(); } - let closure: TimerClosure = Closure::new(f!(on_interval.emit(()))); - let raw_interval = Rc::new(RawInterval::new(closure)); frp::extend! { network - restart <- any_mut::(); + restart <- any_mut::(); stop <- any_mut::<()>(); - eval restart((duration) raw_interval.restart(*duration)); - eval_ stop(raw_interval.stop()); + on_interval <- any_mut(); + + let timer = Rc::new(RawTimer::new(f!(on_interval.emit(())))); + eval restart((duration) timer.set_interval(*duration)); + eval_ stop(timer.stop()); } let on_interval = on_interval.into(); - Self { on_interval, restart, stop, raw_interval } - } -} - - -// =================== -// === RawInterval === -// =================== - -#[derive(Debug)] -struct RawInterval { - closure: TimerClosure, - timer_handle: RefCell>, -} - -impl RawInterval { - fn new(closure: TimerClosure) -> Self { - Self { closure, timer_handle: default() } - } - - fn restart(&self, time: i32) { - let js_func = self.closure.as_js_function(); - let result = window.set_interval_with_callback_and_timeout_and_arguments_0(js_func, time); - let handle = result.expect("setInterval should never fail when callback is a function."); - self.set_timer_handle(Some(handle)); - } - - fn stop(&self) { - self.set_timer_handle(None); - } - - fn set_timer_handle(&self, handle: Option) { - if let Some(old_handle) = self.timer_handle.replace(handle) { - window.clear_interval_with_handle(old_handle); - } - } -} - -impl Drop for RawInterval { - fn drop(&mut self) { - self.stop(); + Self { on_interval, restart, stop, raw_interval: timer } } } diff --git a/lib/rust/frp/src/io/timer/timeout.rs b/lib/rust/frp/src/io/timer/timeout.rs index b0b3a222afb..cf48083fbca 100644 --- a/lib/rust/frp/src/io/timer/timeout.rs +++ b/lib/rust/frp/src/io/timer/timeout.rs @@ -4,15 +4,7 @@ use crate::prelude::*; use crate as frp; -use enso_web::window; -use enso_web::Closure; - - -// ============== -// === Export === -// ============== - -pub use enso_web::traits::*; +use super::RawTimer; @@ -20,9 +12,6 @@ pub use enso_web::traits::*; // === Timeout === // ================ -/// Closure type alias for use in `setTimeout` call. -type TimerClosure = Closure; - /// One-shot timer. /// /// The timer can be started or cancelled at any time using `restart` and `cancel` inputs. After it @@ -43,7 +32,7 @@ type TimerClosure = Closure; pub struct Timeout { /// Starts the timer immediately with provided timeout value, specified in integer /// milliseconds. If the timer was already started, it is cancelled and restarted. - pub restart: frp::Any, + pub restart: frp::Any, /// Stops the timer if it is running. No `on_expired` events will be emitted until it is /// started again. pub cancel: frp::Any, @@ -51,70 +40,25 @@ pub struct Timeout { pub on_expired: frp::Stream<()>, /// Indicates whether the timer is currently running. pub is_running: frp::Stream, - raw_timeout: Rc, + timer: Rc, } impl Timeout { /// Constructor. Timer is initially not started. pub fn new(network: &frp::Network) -> Self { frp::extend! { network - on_expired <- any_mut(); - } - - let closure: TimerClosure = Closure::new(f!(on_expired.emit(()))); - let raw_timeout = Rc::new(RawTimeout::new(closure)); - - frp::extend! { network - restart <- any_mut::(); + restart <- any_mut::(); cancel <- any_mut::<()>(); + on_expired <- any_mut(); stopped_running <- any(&cancel, &on_expired); is_running <- bool(&stopped_running, &restart); - eval restart((timeout) raw_timeout.restart(*timeout)); - eval_ cancel(raw_timeout.cancel()); + let timer = Rc::new(RawTimer::new(f!(on_expired.emit(())))); + eval restart((timeout) timer.set_timeout(*timeout)); + eval_ cancel(timer.stop()); } let on_expired = on_expired.into(); - Self { on_expired, restart, cancel, raw_timeout, is_running } - } -} - - -// ================== -// === RawTimeout === -// ================== - -#[derive(Debug)] -struct RawTimeout { - closure: TimerClosure, - timer_handle: RefCell>, -} - -impl RawTimeout { - fn new(closure: TimerClosure) -> Self { - Self { closure, timer_handle: default() } - } - - fn restart(&self, time: i32) { - let js_func = self.closure.as_js_function(); - let result = window.set_timeout_with_callback_and_timeout_and_arguments_0(js_func, time); - let handle = result.expect("setTimeout should never fail when callback is a function."); - self.set_timer_handle(Some(handle)); - } - - fn cancel(&self) { - self.set_timer_handle(None); - } - - fn set_timer_handle(&self, handle: Option) { - if let Some(old_handle) = self.timer_handle.replace(handle) { - window.clear_timeout_with_handle(old_handle); - } - } -} - -impl Drop for RawTimeout { - fn drop(&mut self) { - self.cancel(); + Self { on_expired, restart, cancel, timer, is_running } } } diff --git a/lib/rust/frp/src/microtasks.rs b/lib/rust/frp/src/microtasks.rs index d2f0657b241..fcfc66d2f98 100644 --- a/lib/rust/frp/src/microtasks.rs +++ b/lib/rust/frp/src/microtasks.rs @@ -81,11 +81,6 @@ use enso_callback as callback; use enso_generics::Cons; use enso_generics::Nil; use enso_generics::PushLastField; -use enso_web::traits::WindowOps; -use enso_web::Closure; -use enso_web::JsEventHandler; -use enso_web::JsValue; -use enso_web::Promise; @@ -156,31 +151,13 @@ struct Scheduler { impl Scheduler { fn new() -> Self { - let data = Rc::new_cyclic(|weak: &Weak| { - let resolved_promise = Promise::resolve(&JsValue::NULL); - let callbacks = default(); - let late_callbacks = default(); - let schedule_depth = default(); - let is_scheduled = default(); - let run_all_closure = Closure::new(f!([weak] (_: JsValue) { - if let Some(data) = weak.upgrade() { - data.run_all(); - } - })); - let schedule_closure = Closure::new(f!([weak] (_: f64) { - if let Some(data) = weak.upgrade() { - data.schedule_task(); - } - })); - SchedulerData { - is_scheduled, - callbacks, - late_callbacks, - schedule_depth, - resolved_promise, - run_all_closure, - schedule_closure, - } + let data = Rc::new_cyclic(|weak| SchedulerData { + weak_self: weak.clone(), + is_scheduled: default(), + callbacks: default(), + late_callbacks: default(), + schedule_depth: default(), + scheduled_task: default(), }); Self { data } } @@ -214,28 +191,36 @@ fn profile(f: impl FnOnce() + 'static) -> impl FnOnce() + 'static { } struct SchedulerData { - is_scheduled: Cell, - callbacks: callback::registry::NoArgsOnce, - late_callbacks: callback::registry::NoArgsOnce, - schedule_depth: Cell, - resolved_promise: Promise, - run_all_closure: JsEventHandler, - schedule_closure: JsEventHandler, + weak_self: Weak, + scheduled_task: Cell>, + schedule_depth: Cell, + is_scheduled: Cell, + callbacks: callback::registry::NoArgsOnce, + late_callbacks: callback::registry::NoArgsOnce, } impl SchedulerData { #[profile(Task)] fn schedule_task(&self) { if !self.is_scheduled.replace(true) { - // Result left unused on purpose. We only care about `closure` being run in the next - // microtask, which is a guaranteed side effect of providing it to [`Promise::then`] - // method on already resolved promise. - let _ = self.resolved_promise.then(&self.run_all_closure); + let weak = self.weak_self.clone(); + let task = enso_web::queue_microtask(move || { + if let Some(data) = weak.upgrade() { + data.run_all(); + } + }); + self.scheduled_task.replace(Some(task)); } } fn schedule_task_past_limit(&self) { if !self.is_scheduled.replace(true) { - enso_web::window.request_animation_frame_with_closure_or_panic(&self.schedule_closure); + let weak = self.weak_self.clone(); + let task = enso_web::request_animation_frame(move |_| { + if let Some(data) = weak.upgrade() { + data.schedule_task(); + } + }); + self.scheduled_task.replace(Some(task)); } } diff --git a/lib/rust/prelude/src/lib.rs b/lib/rust/prelude/src/lib.rs index 5c5da2be5a1..1982e3a5054 100644 --- a/lib/rust/prelude/src/lib.rs +++ b/lib/rust/prelude/src/lib.rs @@ -218,7 +218,7 @@ pub fn init_global() { #[cfg(target_arch = "wasm32")] fn init_global_internal() { - enso_web::forward_panic_hook_to_console(); + enso_web::register_panic_hook(); enso_web::set_stack_trace_limit(); } diff --git a/lib/rust/web/Cargo.toml b/lib/rust/web/Cargo.toml index 58489ea3ffb..234249cbe93 100644 --- a/lib/rust/web/Cargo.toml +++ b/lib/rust/web/Cargo.toml @@ -24,39 +24,48 @@ async-std = { version = "1.5.0" } [dependencies.web-sys] version = "0.3.4" features = [ + 'AddEventListenerOptions', + 'BinaryType', 'Blob', - 'Document', - 'Node', - 'Element', - 'HtmlElement', - 'HtmlDivElement', - 'HtmlHeadElement', - 'HtmlCollection', - 'CssStyleDeclaration', - 'HtmlCanvasElement', - 'WebGlBuffer', - 'WebGlRenderingContext', - 'WebGl2RenderingContext', 'CanvasRenderingContext2d', - 'WebGlProgram', - 'WebGlShader', - 'WebGlQuery', - 'Window', - 'Navigator', + 'CloseEvent', 'console', - 'Performance', - 'Event', - 'MouseEvent', - 'EventTarget', - 'Text', + 'CssStyleDeclaration', + 'DataTransfer', + 'Document', 'DomRect', 'DomRectReadOnly', - 'Location', - 'ReadableStream', - 'AddEventListenerOptions', + 'DragEvent', + 'Element', + 'Event', 'EventListenerOptions', + 'EventTarget', + 'File', + 'FileList', + 'HtmlCanvasElement', + 'HtmlCollection', + 'HtmlDivElement', + 'HtmlElement', + 'HtmlHeadElement', 'KeyboardEvent', + 'Location', + 'MouseEvent', + 'Navigator', + 'Node', + 'MessageEvent', + 'Performance', + 'ReadableStream', + 'ReadableStreamDefaultReader', + 'Text', + 'WebGl2RenderingContext', + 'WebGlBuffer', + 'WebGlProgram', + 'WebGlQuery', + 'WebGlRenderingContext', + 'WebGlShader', + 'WebSocket', 'WheelEvent', + 'Window', ] [dev-dependencies] diff --git a/lib/rust/web/js/callbacks_with_cleanup.ts b/lib/rust/web/js/callbacks_with_cleanup.ts new file mode 100644 index 00000000000..473f02dcec3 --- /dev/null +++ b/lib/rust/web/js/callbacks_with_cleanup.ts @@ -0,0 +1,87 @@ +const registeredCleanups: Set<() => void> = new Set() + +interface CleanupHandle { + cleanup: () => void +} + +export function registerCleanup(doCleanup: () => void): CleanupHandle { + function registeredCleanup() { + registeredCleanups.delete(registeredCleanup) + doCleanup() + } + registeredCleanups.add(registeredCleanup) + return { cleanup: doCleanup } +} + +export function registerEventListener( + target: EventTarget, + event: string, + callback: EventListener, + options: AddEventListenerOptions +): CleanupHandle { + target.addEventListener(event, callback, options) + // Do not prevent garbage collection of the event target. If it is collected, the event listener + // will be removed automatically anyway. + let weakTarget = new WeakRef(target) + let weakCallback = new WeakRef(callback) + return registerCleanup(() => { + let target = weakTarget.deref() + let callback = weakCallback.deref() + if (target != null && callback != null) { + target.removeEventListener(event, callback, options) + } + }) +} + +export function registerTimeout(callback: Function, delay: number): CleanupHandle { + const timeoutId = setTimeout(callback, delay) + return registerCleanup(() => clearTimeout(timeoutId)) +} + +export function registerInterval(callback: Function, delay: number): CleanupHandle { + const intervalId = setInterval(callback, delay) + return registerCleanup(() => clearInterval(intervalId)) +} + +export function registerAnimationFrame(callback: FrameRequestCallback): CleanupHandle { + const frameId = requestAnimationFrame(callback) + return registerCleanup(() => cancelAnimationFrame(frameId)) +} + +export function registerQueueMicrotask(callback: Function): CleanupHandle { + let canceled = false + queueMicrotask(() => { + if (!canceled) { + callback() + } + }) + return registerCleanup(() => { + canceled = true + }) +} + +export interface ResizeCallback { + (width: number, height: number): void +} + +export function registerResizeObserver(target: Element, callback: ResizeCallback) { + let observer = new ResizeObserver(entries => { + let rect = entries[entries.length - 1].contentRect + callback(rect.width, rect.height) + }) + observer.observe(target, { box: 'content-box' }) + return registerCleanup(() => { + observer.disconnect() + }) +} + +/** + * Call all registered cleanup functions. This is useful to ensure that all event listeners are + * removed and all timers are cleared. + */ +export function cleanupAllHandlers(): void { + for (let cleanup of registeredCleanups) { + cleanup() + } + registeredCleanups.clear() +} diff --git a/lib/rust/web/js/intersection_observer.js b/lib/rust/web/js/intersection_observer.js deleted file mode 100644 index 3726884badf..00000000000 --- a/lib/rust/web/js/intersection_observer.js +++ /dev/null @@ -1,79 +0,0 @@ -// The IntersectionObserver interface of the Intersection Observer API provides -// a way to asynchronously observe changes in the intersection of a target -// element with an ancestor element or with a top-level document's viewport. -// The ancestor element or viewport is referred to as the root. -// -// See also -// https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver - -// ============== -// === IxPool === -// ============== - -class IxPool { - constructor() { - this.next = 0 - this.free = [] - } - - reserve() { - let ix - if (this.free.length == 0) { - ix = this.next - this.next += 1 - } else { - ix = this.free.shift() - } - return ix - } - - drop(ix) { - this.free.unshift(ix) - } -} - -// ============ -// === Pool === -// ============ - -class Pool { - constructor(cons) { - this.cons = cons - this.ixs = new IxPool() - } - - reserve(...args) { - let ix = this.ixs.reserve() - this[ix] = this.cons(...args) - return ix - } - - drop(ix) { - this.ixs.drop(ix) - this[ix] = null - } -} - -// ============================ -// === IntersectionObserver === -// ============================ - -let intersectionObserverPool = new Pool((...args) => new IntersectionObserver(...args)) - -export function intersection_observe(target, f) { - let id = intersectionObserverPool.reserve(intersection_observer_update(f)) - intersectionObserverPool[id].observe(target) - return id -} - -export function intersection_unobserve(id) { - intersectionObserverPool[id].disconnect() - intersectionObserverPool.drop(id) -} - -function intersection_observer_update(f) { - return entries => { - let rect = entries[0].boundingClientRect - f(rect.x, rect.y, rect.width, rect.height) - } -} diff --git a/lib/rust/web/js/resize_observer.js b/lib/rust/web/js/resize_observer.js deleted file mode 100644 index 17dd143ace6..00000000000 --- a/lib/rust/web/js/resize_observer.js +++ /dev/null @@ -1,73 +0,0 @@ -// ============== -// === IxPool === -// ============== - -class IxPool { - constructor() { - this.next = 0 - this.free = [] - } - - reserve() { - let ix - if (this.free.length == 0) { - ix = this.next - this.next += 1 - } else { - ix = this.free.shift() - } - return ix - } - - drop(ix) { - this.free.unshift(ix) - } -} - -// ============ -// === Pool === -// ============ - -class Pool { - constructor(cons) { - this.cons = cons - this.ixs = new IxPool() - } - - reserve(...args) { - let ix = this.ixs.reserve() - this[ix] = this.cons(...args) - return ix - } - - drop(ix) { - this.ixs.drop(ix) - this[ix] = null - } -} - -// ====================== -// === ResizeObserver === -// ====================== - -let resizeObserverPool = new Pool((...args) => new ResizeObserver(...args)) - -export function resize_observe(target, f) { - let id = resizeObserverPool.reserve(resize_observer_update(f)) - // We are using the devicePixelContentBoxSize option to get correct results here, as explained in this - // article: https://webglfundamentals.org/webgl/lessons/webgl-resizing-the-canvas.html - resizeObserverPool[id].observe(target, { box: 'content-box' }) - return id -} - -export function resize_unobserve(id) { - resizeObserverPool[id].disconnect() - resizeObserverPool.drop(id) -} - -function resize_observer_update(f) { - return entries => { - let rect = entries[0].contentRect - f(rect.width, rect.height) - } -} diff --git a/lib/rust/web/src/binding/mock.rs b/lib/rust/web/src/binding/mock.rs index 8f460c7a58e..5eac0aafcf7 100644 --- a/lib/rust/web/src/binding/mock.rs +++ b/lib/rust/web/src/binding/mock.rs @@ -60,7 +60,7 @@ macro_rules! auto_impl_mock_default { }; } -auto_impl_mock_default!(bool, i16, i32, u32, f64, String); +auto_impl_mock_default!(bool, i16, i32, u16, u32, f64, String); @@ -75,11 +75,13 @@ pub trait MockData {} #[macro_export] macro_rules! mock_struct { ( $([$opt:ident])? + $(#[$meta:meta])* $name:ident $(<$( $param:ident $(: ?$param_tp:ident)? ),*>)? $(=> $deref:ident)? ) => { #[allow(missing_copy_implementations)] #[allow(non_snake_case)] #[allow(missing_docs)] + $(#[$meta])* pub struct $name $(<$($param $(:?$param_tp)?),*>)? { $($( $param : PhantomData<$param> ),*)? } @@ -269,12 +271,13 @@ macro_rules! mock_fn_gen_print { #[macro_export(local_inner_macros)] macro_rules! mock_data { ( $([$opt:ident])? + $(#[$meta:meta])* $name:ident $(<$( $param:ident $(: ?$param_tp:ident)? ),*>)? $(=> $deref:ident)? $( fn $fn_name:ident $(<$($fn_tp:ident),*>)? ($($args:tt)*) $(-> $out:ty)?; )* ) => { - mock_struct!{$([$opt])? $name $(<$($param $(:?$param_tp)?),*>)? $(=> $deref)?} + mock_struct!{$([$opt])? $(#[$meta])* $name $(<$($param $(:?$param_tp)?),*>)? $(=> $deref)?} impl $(<$($param $(:?$param_tp)?),*>)? $name $(<$($param),*>)? { $( mock_pub_fn!{$fn_name $(<$($fn_tp),*>)? ($($args)*) $(-> $out)?} @@ -511,18 +514,48 @@ mock_data! { Object => JsValue // === EventTarget === -mock_data! { EventTarget => Object - fn remove_event_listener_with_callback - (&self, tp: &str, f: &Function) -> Result<(), JsValue>; - fn remove_event_listener_with_callback_and_event_listener_options - (&self, tp: &str, f: &Function, opt: &EventListenerOptions) -> Result<(), JsValue>; - fn add_event_listener_with_callback - (&self, tp: &str, f: &Function) -> Result<(), JsValue>; - fn add_event_listener_with_callback_and_bool - (&self, tp: &str, f: &Function, opt: bool) -> Result<(), JsValue>; - fn add_event_listener_with_callback_and_add_event_listener_options - (&self, tp: &str, f: &Function, opt: &AddEventListenerOptions) - -> Result<(), JsValue>; +mock_data! { EventTarget => Object } + +// === RawCleanupHandle === + +mock_data! { RawCleanupHandle + fn cleanup(&self); +} +/// Register an event listener callback with JS-side cleanup. +pub fn register_event_listener( + target: &EventTarget, + event: &str, + callback: &Function, + options: &AddEventListenerOptions, +) -> RawCleanupHandle { + let _ = (target, event, callback, options); + RawCleanupHandle::const_new() +} +/// Register a timeout callback with JS-side cleanup. +pub fn register_timeout(callback: &Function, timeout: u32) -> RawCleanupHandle { + let _ = (callback, timeout); + RawCleanupHandle::const_new() +} +/// Register an interval callback with JS-side cleanup. +pub fn register_interval(callback: &Function, interval: u32) -> RawCleanupHandle { + let _ = (callback, interval); + RawCleanupHandle::const_new() +} +/// Register an animation frame callback with JS-side cleanup. +pub fn register_animation_frame(callback: &Function) -> RawCleanupHandle { + let _ = callback; + RawCleanupHandle::const_new() +} +/// Register a microtask callback with JS-side cleanup. +pub fn register_queue_microtask(callback: &Function) -> RawCleanupHandle { + let _ = callback; + RawCleanupHandle::const_new() +} + +/// Register a resize observer callback with JS-side cleanup. +pub fn register_resize_observer(target: &JsValue, callback: &Function) -> RawCleanupHandle { + let _ = (target, callback); + RawCleanupHandle::const_new() } @@ -543,16 +576,8 @@ mock_data! { Window => EventTarget fn document(&self) -> Option; fn open_with_url_and_target(&self, url: &str, target: &str) -> Result, JsValue>; - fn request_animation_frame(&self, callback: &Function) -> Result; - fn cancel_animation_frame(&self, handle: i32) -> Result<(), JsValue>; fn performance(&self) -> Option; fn device_pixel_ratio(&self) -> f64; - fn set_timeout_with_callback_and_timeout_and_arguments_0 - (&self, handler: &Function, timeout: i32) -> Result; - fn set_interval_with_callback_and_timeout_and_arguments_0 - (&self, handler: &Function, timeout: i32) -> Result; - fn clear_timeout_with_handle(&self, handle: i32); - fn clear_interval_with_handle(&self, handle: i32); } @@ -621,6 +646,12 @@ mock_data! { MouseEvent => Event } +// === DragEvent === +mock_data! { DragEvent => MouseEvent + fn data_transfer(&self) -> Option; +} + + // === WheelEvent === mock_data! { WheelEvent => MouseEvent fn delta_x(&self) -> f64; @@ -808,6 +839,57 @@ mock_data! { Performance => EventTarget fn time_origin(&self) -> f64; } +// === File === +mock_data! { DataTransfer => Object + fn files(&self) -> Option; +} + +mock_data! { FileList => Object + fn length(&self) -> u32; + fn get(&self, index: u32) -> Option; +} + +mock_data! { File => Blob + fn name(&self) -> String; + fn size(&self) -> f64; + fn type_(&self) -> String; +} +mock_data! { Blob => Object + fn stream(&self) -> ReadableStream; +} +mock_data! { ReadableStream => Object + fn get_reader(&self) -> Object; +} +mock_data! { ReadableStreamDefaultReader => Object } + +// === WebSocket === +#[allow(missing_docs)] +#[derive(Clone, Copy, Debug)] +pub enum BinaryType { + Blob, + Arraybuffer, +} + +mock_data! { #[derive(PartialEq)] WebSocket => EventTarget + fn new(url: &str) -> Result; + fn url(&self) -> String; + fn set_binary_type(&self, value: BinaryType); + fn close_with_code_and_reason(&self, code: u16, reason: &str) -> Result<(), JsValue>; + fn ready_state(&self) -> u16; + fn send_with_str(&self, data: &str) -> Result<(), JsValue>; + fn send_with_u8_array(&self, data: &[u8]) -> Result<(), JsValue>; +} + +#[allow(missing_docs)] +impl WebSocket { + pub const CONNECTING: u16 = 0; + pub const OPEN: u16 = 1; + pub const CLOSING: u16 = 2; + pub const CLOSED: u16 = 3; +} + +mock_data! { CloseEvent => Event } +mock_data! { MessageEvent => Event } // =============== diff --git a/lib/rust/web/src/binding/wasm.rs b/lib/rust/web/src/binding/wasm.rs index e74cd0cda87..23fa0664252 100644 --- a/lib/rust/web/src/binding/wasm.rs +++ b/lib/rust/web/src/binding/wasm.rs @@ -1,5 +1,8 @@ //! Native bindings to the web-api. +// === Non-Standard Linter Configuration === +#![allow(missing_docs)] + use crate::prelude::*; @@ -22,22 +25,33 @@ pub use wasm_bindgen::JsCast; pub use wasm_bindgen::JsValue; pub use web_sys::console; pub use web_sys::AddEventListenerOptions; +pub use web_sys::BinaryType; +pub use web_sys::Blob; pub use web_sys::CanvasRenderingContext2d; +pub use web_sys::CloseEvent; +pub use web_sys::DataTransfer; pub use web_sys::Document; +pub use web_sys::DragEvent; pub use web_sys::Element; pub use web_sys::Event; pub use web_sys::EventListenerOptions; pub use web_sys::EventTarget; +pub use web_sys::File; +pub use web_sys::FileList; pub use web_sys::HtmlCanvasElement; pub use web_sys::HtmlCollection; pub use web_sys::HtmlDivElement; pub use web_sys::HtmlElement; pub use web_sys::KeyboardEvent; +pub use web_sys::MessageEvent; pub use web_sys::MouseEvent; pub use web_sys::Node; pub use web_sys::Performance; +pub use web_sys::ReadableStream; +pub use web_sys::ReadableStreamDefaultReader; pub use web_sys::WebGl2RenderingContext; pub use web_sys::WebGlQuery; +pub use web_sys::WebSocket; pub use web_sys::WheelEvent; pub use web_sys::Window; @@ -141,3 +155,84 @@ wasm_lazy_global! { window : Window = get_window() } #[cfg(target_arch = "wasm32")] wasm_lazy_global! { document : Document = window.document().unwrap() } + + + +// ============================== +// === Listeners with cleanup === +// ============================== + +#[wasm_bindgen(module = "/js/callbacks_with_cleanup.ts")] +extern "C" { + /// Registered listener callback handle that has js-side cleanup. + /// + /// When wasm module is destroyed, all cleanup handles are automatically cleaned up on the JS + /// side, without invoking any rust code. This is especially important after rust panic, since + /// no rust code should be executed once a panic caused abort. + #[allow(unsafe_code)] + pub type RawCleanupHandle; + + /// Perform cleanup on the JS side. After cleanup, the listener will not be called anymore. + #[allow(unsafe_code)] + #[wasm_bindgen(structural, method)] + pub fn cleanup(this: &RawCleanupHandle); + + /// Register an event listener callback with JS-side cleanup. + #[allow(unsafe_code)] + #[wasm_bindgen(js_name = registerEventListener)] + pub fn register_event_listener( + target: &EventTarget, + event: &str, + callback: &Function, + options: &AddEventListenerOptions, + ) -> RawCleanupHandle; + + + /// Register a timeout callback with JS-side cleanup. + #[allow(unsafe_code)] + #[wasm_bindgen(js_name = registerTimeout)] + pub fn register_timeout(callback: &Function, timeout: u32) -> RawCleanupHandle; + + /// Register an interval callback with JS-side cleanup. + #[allow(unsafe_code)] + #[wasm_bindgen(js_name = registerInterval)] + pub fn register_interval(callback: &Function, interval: u32) -> RawCleanupHandle; + + /// Register an animation frame callback with JS-side cleanup. + #[allow(unsafe_code)] + #[wasm_bindgen(js_name = registerAnimationFrame)] + pub fn register_animation_frame(callback: &Function) -> RawCleanupHandle; + + /// Register a microtask callback with JS-side cleanup. + #[allow(unsafe_code)] + #[wasm_bindgen(js_name = registerQueueMicrotask)] + pub fn register_queue_microtask(callback: &Function) -> RawCleanupHandle; + + /// Register a resize observer callback with JS-side cleanup. + #[allow(unsafe_code)] + #[wasm_bindgen(js_name = registerResizeObserver)] + pub fn register_resize_observer(target: &JsValue, callback: &Function) -> RawCleanupHandle; + + /// Unregister all registered event listeners and other JS callbacks. Calling this function + /// will likely break the application, since it expects the registered callbacks to be called. + /// This function is intended only as a cleanup in the event of a panic. + #[allow(unsafe_code)] + #[wasm_bindgen(js_name = cleanupAllHandlers)] + pub fn cleanup_all_handlers(); +} + +/// EnsoGl app +#[wasm_bindgen] +extern "C" { + pub type EnsoglApp; + + /// Global JS instance of the Ensogl application. + #[wasm_bindgen(js_name = "ensoglApp")] + pub static ENSOGL_APP: EnsoglApp; + + /// Notify the JS application instance that a rust panic has occurred. This will cause the app + /// to display a panic message, allow the user to file a bug report and offer to reload the app. + #[allow(unsafe_code)] + #[wasm_bindgen(method, js_name = "handlePanic")] + pub fn handle_panic(this: &EnsoglApp, msg: &str); +} diff --git a/lib/rust/web/src/closure.rs b/lib/rust/web/src/closure.rs deleted file mode 100644 index dc7fe551e5e..00000000000 --- a/lib/rust/web/src/closure.rs +++ /dev/null @@ -1,8 +0,0 @@ -//! Helper code for dealing with web_sys's `Closure`. - - -// ============== -// === Export === -// ============== - -pub mod storage; diff --git a/lib/rust/web/src/closure/storage.rs b/lib/rust/web/src/closure/storage.rs deleted file mode 100644 index 54cbdce9705..00000000000 --- a/lib/rust/web/src/closure/storage.rs +++ /dev/null @@ -1,91 +0,0 @@ -// === Non-Standard Linter Configuration === -#![allow(missing_docs)] - -use crate::prelude::*; - -use derivative::Derivative; -use js_sys::Function; -use wasm_bindgen::convert::FromWasmAbi; -use wasm_bindgen::JsCast; - - -// ============== -// === Export === -// ============== - -pub use wasm_bindgen::prelude::Closure; - - - -// ====================== -// === ClosureStorage === -// ====================== - -/// Constraint for JS closure argument types -pub trait ClosureArg = FromWasmAbi + 'static; - -/// Function that can be wrapped into a `Closure`. -pub trait ClosureFn = FnMut(Arg) + 'static where Arg: ClosureArg; - -/// Stores an optional closure. -/// The purpose it reduce boilerplate repeating when setting JS callbacks. -#[derive(Debug, Derivative)] -#[derivative(Default(bound = ""))] -pub struct OptionalFmMutClosure { - /// The stored closure. - pub closure: Option>, -} - -impl OptionalFmMutClosure { - /// An empty closure storage. - pub fn new() -> OptionalFmMutClosure { - default() - } - - /// Stores the given closure. - pub fn store(&mut self, closure: Closure) -> &Function { - self.closure = Some(closure); - // TODO [mwu]: `insert` should be used when we bump rustc - and then get rid of unwrap. - // Blocked by https://github.com/enso-org/ide/issues/1028 - // The `unwrap` call is safe, because the line above set closure to `Some`. - self.js_ref().unwrap() - } - - /// Obtain JS reference to the closure (that can be passed e.g. as a callback - /// to an event handler). - pub fn js_ref(&self) -> Option<&Function> { - self.closure.as_ref().map(|closure| closure.as_ref().unchecked_ref()) - } - - /// Wraps given function into a Closure. - pub fn wrap(&mut self, f: impl ClosureFn) -> &Function { - let boxed = Box::new(f); - // Note: [mwu] Not sure exactly why, but compiler sometimes require this - // explicit type below and sometimes does not. - let wrapped: Closure = Closure::wrap(boxed); - self.store(wrapped) - } - - /// Clears the current closure. - /// Note: if reference to it is still used by JS, it will throw an exception - /// on calling attempt. Be careful of dangling references. - pub fn clear(&mut self) { - self.closure = None; - } - - /// Register this closure as an event handler. - /// No action is taken if there is no closure stored. - pub fn add_listener(&self, target: &EventType::Target) { - if let Some(function) = self.js_ref() { - EventType::add_listener(target, function) - } - } - - /// Unregister this closure as an event handler. The closure must be the same as when it was - /// registered. - pub fn remove_listener(&self, target: &EventType::Target) { - if let Some(function) = self.js_ref() { - EventType::remove_listener(target, function) - } - } -} diff --git a/lib/rust/web/src/event.rs b/lib/rust/web/src/event.rs index 1e9619bda3c..b4e34e6ed3f 100644 --- a/lib/rust/web/src/event.rs +++ b/lib/rust/web/src/event.rs @@ -1,9 +1,9 @@ //! Utilities for DOM events. -use js_sys::Function; -use wasm_bindgen::JsValue; -use web_sys::Event; -use web_sys::EventTarget; +use crate::Event; +use crate::EventTarget; +use crate::JsCast; +use crate::JsValue; // ============== @@ -31,28 +31,12 @@ pub mod listener; pub trait Type { /// The event value -- i.e. the Rust type of a value that will be passed as an argument /// to the listener. - /// For example `web_sys::CloseEvent`. - type Interface: AsRef; + /// For example `CloseEvent`. + type Interface: AsRef + JsCast + 'static; - /// The type of the EventTarget object that fires this type of event, e.g. `web_sys::WebSocket`. - type Target: AsRef + AsRef + Clone + PartialEq; + /// The type of the EventTarget object that fires this type of event, e.g. `WebSocket`. + type Target: AsRef + AsRef + JsCast + Clone + PartialEq; /// The type of the event as a string. For example `"close"`. const NAME: &'static str; - - /// Add a given function to the event's target as an event listener. It will be called each - /// time event fires until listener is removed through `remove_listener`. - fn add_listener(target: &Self::Target, listener: &Function) { - // The unwrap here is safe, as the `addEventListener` never throws. - EventTarget::add_event_listener_with_callback(target.as_ref(), Self::NAME, listener) - .unwrap() - } - - /// Remove the event listener. The `add_listener` method should have been called before with - /// the very same function argument. - fn remove_listener(target: &Self::Target, listener: &Function) { - // The unwrap here is safe, as the `addEventListener` never throws. - EventTarget::remove_event_listener_with_callback(target.as_ref(), Self::NAME, listener) - .unwrap() - } } diff --git a/lib/rust/web/src/event/listener.rs b/lib/rust/web/src/event/listener.rs index aa4ac9535db..711a9269d1f 100644 --- a/lib/rust/web/src/event/listener.rs +++ b/lib/rust/web/src/event/listener.rs @@ -3,10 +3,7 @@ use crate::prelude::*; -use crate::closure::storage::ClosureFn; -use crate::closure::storage::OptionalFmMutClosure; - -use derivative::Derivative; +use crate::event::Type; @@ -26,35 +23,41 @@ use derivative::Derivative; /// /// `Slot` owns callback and wraps it into JS closure. `Slot` also keeps reference to the target, /// so it must not be leaked. -#[derive(Derivative)] -#[derivative(Debug(bound = "EventType::Interface: Debug"))] -pub struct Slot { - #[derivative(Debug = "ignore")] - target: Option, - js_closure: OptionalFmMutClosure, +pub struct Slot { + target: Option, + callback: Option>>, + handler: Option, } -impl Slot { +impl Debug for Slot { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Slot").finish() + } +} + +impl Slot { /// Create a new `Slot`. As the initial target is provided, the listener will register once it /// gets a callback (see [[set_callback]]). pub fn new(target: &EventType::Target) -> Self { - Self { target: Some(target.clone()), js_closure: default() } + Self { target: Some(target.clone()), callback: None, handler: None } } /// Register the event listener if both target and callback are set. fn add_if_active(&mut self) { - if let (Some(target), Some(function)) = (self.target.as_ref(), self.js_closure.js_ref()) { - debug!("Attaching the callback."); - EventType::add_listener(target, function) + if let (Some(target), Some(callback)) = (self.target.as_ref(), self.callback.as_ref()) { + let callback = callback.clone(); + let handler = + crate::add_event_listener(target.as_ref(), EventType::NAME, move |event| { + let mut callback = callback.borrow_mut(); + callback(event.dyn_into().unwrap()); + }); + self.handler = Some(handler); } } /// Unregister the event listener if both target and callback are set. fn remove_if_active(&mut self) { - if let (Some(target), Some(function)) = (self.target.as_ref(), self.js_closure.js_ref()) { - debug!("Detaching the callback."); - EventType::remove_listener(target, function) - } + self.handler.take(); } /// Set a new target. @@ -86,9 +89,9 @@ impl Slot { /// /// Caveat: using this method will move the event listener to the end of the registered /// callbacks. This will affect the order of callback calls. - pub fn set_callback(&mut self, f: impl ClosureFn) { + pub fn set_callback(&mut self, f: impl FnMut(EventType::Interface) + 'static) { self.remove_if_active(); - self.js_closure.wrap(f); + self.callback = Some(Rc::new(RefCell::new(f))); self.add_if_active() } @@ -97,7 +100,7 @@ impl Slot { /// The stored closure will be dropped and event listener unregistered. pub fn clear_callback(&mut self) { self.remove_if_active(); - self.js_closure.clear(); + self.callback.take(); } /// Detach and attach the listener to the target. @@ -110,7 +113,7 @@ impl Slot { } /// Unregister listener on drop. -impl Drop for Slot { +impl Drop for Slot { fn drop(&mut self) { self.remove_if_active(); } diff --git a/lib/rust/web/src/lib.rs b/lib/rust/web/src/lib.rs index b2fb7f24ff1..e1e1f83bbd0 100644 --- a/lib/rust/web/src/lib.rs +++ b/lib/rust/web/src/lib.rs @@ -27,7 +27,6 @@ use crate::prelude::*; -use std::cell::Cell; use wasm_bindgen::prelude::wasm_bindgen; @@ -37,11 +36,9 @@ use wasm_bindgen::prelude::wasm_bindgen; pub mod binding; pub mod clipboard; -pub mod closure; pub mod event; pub mod platform; pub mod resize_observer; -pub mod stream; pub use std::time::Duration; pub use std::time::Instant; @@ -453,34 +450,10 @@ ops! { ReflectOps for Reflect ops! { WindowOps for Window trait { - fn request_animation_frame_with_closure( - &self, - f: &Closure, - ) -> Result; - fn request_animation_frame_with_closure_or_panic(&self, f: &Closure) -> i32; - fn cancel_animation_frame_or_warn(&self, id: i32); fn performance_or_panic(&self) -> Performance; } impl { - fn request_animation_frame_with_closure( - &self, - f: &Closure, - ) -> Result { - self.request_animation_frame(f.as_js_function()) - } - - fn request_animation_frame_with_closure_or_panic - (&self, f: &Closure) -> i32 { - self.request_animation_frame_with_closure(f).unwrap() - } - - fn cancel_animation_frame_or_warn(&self, id: i32) { - self.cancel_animation_frame(id).unwrap_or_else(|err| { - logging::error!("Error when canceling animation frame: {err:?}"); - }); - } - fn performance_or_panic(&self) -> Performance { self.performance().unwrap_or_else(|| panic!("Cannot access window.performance.")) } @@ -705,14 +678,19 @@ ops! { HtmlCanvasElementOps for HtmlCanvasElement // ============= /// Ignores context menu when clicking with the right mouse button. -pub fn ignore_context_menu(target: &EventTarget) -> EventListenerHandle { - let closure: Closure = Closure::new(move |event: MouseEvent| { - const RIGHT_MOUSE_BUTTON: i16 = 2; - if event.button() == RIGHT_MOUSE_BUTTON { - event.prevent_default(); - } - }); - add_event_listener_with_bool(target, "contextmenu", closure, true) +pub fn ignore_context_menu(target: &EventTarget) -> CleanupHandle { + add_event_listener_with_options( + target, + "contextmenu", + crate::ListenerOptions::new().capture(), + move |event: Event| { + let event: MouseEvent = event.dyn_into().unwrap(); + const RIGHT_MOUSE_BUTTON: i16 = 2; + if event.button() == RIGHT_MOUSE_BUTTON { + event.prevent_default(); + } + }, + ) } @@ -721,9 +699,9 @@ pub fn ignore_context_menu(target: &EventTarget) -> EventListenerHandle { // === Event Listeners === // ======================= -// === EventListenerHandleOptions === +// === ListenerOptions === -/// Structure representing event listener options used by [`EventListenerHandle`]. +/// Structure representing event listener options used by [`CleanupHandle`]. /// /// The handle cannot just use [`AddEventListenerOptions`], as it needs to construct also /// [`EventListenerOptions`] for removing the listener on drop, and values cannot be read from the @@ -731,7 +709,7 @@ pub fn ignore_context_menu(target: &EventTarget) -> EventListenerHandle { /// /// Description of fields is cited from the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#parameters) #[derive(Copy, Clone, Debug, Default)] -pub struct EventListenerHandleOptions { +pub struct ListenerOptions { /// From /// > A boolean value that, if true, indicates that the function specified by listener will /// > never call preventDefault() @@ -748,13 +726,13 @@ pub struct EventListenerHandleOptions { pub once: bool, } -impl EventListenerHandleOptions { +impl ListenerOptions { /// Create default options. pub fn new() -> Self { Self::default() } - /// Set listener explicitly as passive. See [`passive` field docs](EventListenerHandleOptions) + /// Set listener explicitly as passive. See [`passive` field docs](ListenerOptions) /// for more information. pub fn passive(mut self) -> Self { self.passive = Some(true); @@ -762,29 +740,29 @@ impl EventListenerHandleOptions { } /// Set listener explicitly as not passive. See [`passive` field - /// docs](EventListenerHandleOptions) for more information. + /// docs](ListenerOptions) for more information. pub fn not_passive(mut self) -> Self { self.passive = Some(false); self } /// Set listener to get events in the capture phase. See - /// [`capture` field docs](EventListenerHandleOptions) for more information. + /// [`capture` field docs](ListenerOptions) for more information. pub fn capture(mut self) -> Self { self.capture = true; self } /// The listener will be invoked only once. See - /// [`capture` field docs](EventListenerHandleOptions) for more information. + /// [`capture` field docs](ListenerOptions) for more information. pub fn once(mut self) -> Self { self.once = true; self } } -impl From for AddEventListenerOptions { - fn from(from: EventListenerHandleOptions) -> Self { +impl From for AddEventListenerOptions { + fn from(from: ListenerOptions) -> Self { let mut options = Self::new(); if let Some(passive) = from.passive { options.passive(passive); @@ -795,116 +773,100 @@ impl From for AddEventListenerOptions { } } -impl From for EventListenerOptions { - fn from(from: EventListenerHandleOptions) -> Self { - let mut options = EventListenerOptions::new(); - options.capture(from.capture); - options - } -} - - -// === EventListenerHandle === +// === CleanupHandle === /// The type of closures used for 'add_event_listener_*' functions. pub type JsEventHandler = Closure; -/// Handler for event listeners. Unregisters the listener when the last clone is dropped. -#[derive(Clone, CloneRef)] -pub struct EventListenerHandle { - rc: Rc, +/// Register a timeout listener. The callback will be called when the timeout expires. The timeout +/// will be cleared when the returned cleanup handle is dropped. +pub fn set_timeout(callback: impl FnOnce() + 'static, timeout: u32) -> CleanupHandle { + CleanupHandle::new(Closure::::once(callback), |f| register_timeout(f, timeout)) } -impl Debug for EventListenerHandle { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "EventListenerHandle") - } +/// Register an interval listener. The callback will be called repeatedly whenever the interval time +/// passes. The interval will be cleared when the returned cleanup handle is dropped. +pub fn set_interval(callback: impl FnMut() + 'static, interval: u32) -> CleanupHandle { + CleanupHandle::new(Closure::::new(callback), |f| register_interval(f, interval)) } -impl EventListenerHandle { - /// Constructor. - pub fn new( - target: EventTarget, - name: Rc, - closure: Closure, - options: EventListenerHandleOptions, - ) -> Self { - let closure = Box::new(closure); - let data = EventListenerHandleData { target, name, closure, options }; - let rc = Rc::new(data); - Self { rc } - } +/// Register a listener for animation frame. The callback will be called on the next animation +/// frame. Note that a lot of time might pass until that happens, as animation frames can be stopped +/// when the browser tab is in background. The listener will be automatically canceled when the +/// returned cleanup handle is dropped. +pub fn request_animation_frame(callback: impl FnOnce(f64) + 'static) -> CleanupHandle { + CleanupHandle::new(Closure::::once(callback), register_animation_frame) } -/// Internal structure for [`EventListenerHandle`]. +/// Queue a listener for the next microtask. If the cleanup handle is dropped before next microtask, +/// the callback will never be called. +pub fn queue_microtask(callback: impl FnOnce() + 'static) -> CleanupHandle { + CleanupHandle::new(Closure::::once(callback), register_queue_microtask) +} + +/// Add a browser event listener to given target object. The listener will be removed when the +/// returned cleanup handle is dropped. +pub fn add_event_listener( + target: &EventTarget, + event: &str, + callback: impl FnMut(Event) + 'static, +) -> CleanupHandle { + add_event_listener_with_options(target, event, default(), callback) +} + +/// Add a browser event listener to given target object. The listener will be removed when the +/// returned cleanup handle is dropped. +pub fn add_event_listener_with_options( + target: &EventTarget, + event: &str, + options: ListenerOptions, + callback: impl FnMut(Event) + 'static, +) -> CleanupHandle { + CleanupHandle::new(Closure::::new(callback), |f| { + register_event_listener(target, event, f, &options.into()) + }) +} + +/// A handle to JS event listener, timer or other async operation with a rust callback. The listener +/// will be removed when the handle is dropped. /// -/// # Implementation Notes -/// The [`_closure`] field contains a wasm_bindgen's [`Closure`]. Dropping it causes the -/// associated function to be pruned from memory. -struct EventListenerHandleData { - target: EventTarget, - name: Rc, - closure: Box, - options: EventListenerHandleOptions, +/// When wasm module is destroyed, all cleanup handles are automatically cleaned up on the JS side, +/// without invoking any rust code. This is especially important after rust panic, since no rust +/// code should be executed once a panic caused the thread to abort execution. +#[derive(Clone, CloneRef)] +pub struct CleanupHandle { + #[allow(dyn_drop)] + inner: Rc>, } -impl Drop for EventListenerHandleData { - fn drop(&mut self) { - let function = self.closure.as_js_function(); - self.target - .remove_event_listener_with_callback_and_event_listener_options( - &self.name, - function, - &self.options.into(), - ) - .ok(); +impl Debug for CleanupHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CleanupHandle").finish() } } -/// Wrapper for the function defined in web_sys which allows passing wasm_bindgen [`Closure`] -/// directly. -pub fn add_event_listener_with_options( - target: &EventTarget, - name: &str, - closure: Closure, - options: EventListenerHandleOptions, -) -> EventListenerHandle { - // Please note that using [`ok`] is safe here, as according to MDN this function never - // fails: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener. - target - .add_event_listener_with_callback_and_add_event_listener_options( - name, - closure.as_js_function(), - &options.into(), - ) - .ok(); - let target = target.clone(); - let name = Rc::new(name.to_string()); - EventListenerHandle::new(target, name, closure, options) +impl CleanupHandle { + pub(crate) fn new( + closure: Closure, + raw_constructor: impl FnOnce(&Function) -> RawCleanupHandle, + ) -> Self { + let raw = raw_constructor(closure.as_js_function()); + Self { inner: Rc::new(CleanupHandleInner { raw, closure }) } + } } -/// Wrapper for [`add_event_listener`] setting the default options. -pub fn add_event_listener( - target: &EventTarget, - name: &str, - closure: Closure, -) -> EventListenerHandle { - add_event_listener_with_options(target, name, closure, default()) +struct CleanupHandleInner { + raw: RawCleanupHandle, + #[allow(dead_code)] + closure: T, } -/// Wrapper for [`add_event_listener`] setting the `capture` option keeping other options default. -pub fn add_event_listener_with_bool( - target: &EventTarget, - name: &str, - closure: Closure, - capture: bool, -) -> EventListenerHandle { - let options = EventListenerHandleOptions { capture, ..default() }; - add_event_listener_with_options(target, name, closure, options) +impl Drop for CleanupHandleInner { + fn drop(&mut self) { + self.raw.cleanup(); + } } - - // ========================= // === Stack Trace Limit === // ========================= @@ -1004,18 +966,16 @@ pub fn simulate_sleep(duration: f64) { /// Enables forwarding panic messages to `console.error`. #[cfg(target_arch = "wasm32")] -pub fn forward_panic_hook_to_console() { +pub fn register_panic_hook() { std::panic::set_hook(Box::new(report_panic)) } #[cfg(target_arch = "wasm32")] fn report_panic(info: &std::panic::PanicInfo) { // Formats the info to display properly in the browser console. See crate docs for details. - let msg = console_error_panic_hook::format_panic(info); - if let Some(api) = enso_debug_api::console() { - api.error(&msg); - } - web_sys::console::error_1(&msg.into()); + let message = console_error_panic_hook::format_panic(info); + cleanup_all_handlers(); + ENSOGL_APP.handle_panic(&message); } @@ -1037,63 +997,3 @@ pub async fn sleep(duration: Duration) { #[cfg(not(target_arch = "wasm32"))] pub use async_std::task::sleep; - - - -// ==================== -// === FrameCounter === -// ==================== - -type Counter = Rc>; - -#[derive(Debug)] -/// A counter that counts the number of frames that have passed since its initialization. -/// -/// Uses `request_animation_frame` under the hood to count frames. -pub struct FrameCounter { - frames: Counter, - js_on_frame_handle_id: Rc>, - _closure_handle: Rc>>>, -} - -impl FrameCounter { - /// Creates a new frame counter. - pub fn start_counting() -> Self { - let frames: Counter = default(); - let frames_handle = Rc::downgrade(&frames); - let closure_handle = Rc::new(RefCell::new(None)); - let closure_handle_internal = Rc::downgrade(&closure_handle); - let js_on_frame_handle_id = Rc::new(Cell::new(0)); - let js_on_frame_handle_id_internal = Rc::downgrade(&js_on_frame_handle_id); - *closure_handle.as_ref().borrow_mut() = Some(Closure::new(move |_| { - frames_handle.upgrade().map(|fh| fh.as_ref().update(|value| value.saturating_add(1))); - if let Some(maybe_handle) = closure_handle_internal.upgrade() { - if let Some(handle) = maybe_handle.borrow_mut().as_ref() { - let new_handle_id = - window.request_animation_frame_with_closure_or_panic(handle); - if let Some(handle_id) = js_on_frame_handle_id_internal.upgrade() { - handle_id.as_ref().set(new_handle_id) - } - } - } - })); - - js_on_frame_handle_id.as_ref().set(window.request_animation_frame_with_closure_or_panic( - closure_handle.borrow().as_ref().unwrap(), - )); - - debug_assert!(closure_handle.borrow().is_some()); - Self { frames, js_on_frame_handle_id, _closure_handle: closure_handle } - } - - /// Returns the number of frames that have passed since the counter was created. - pub fn frames_since_start(&self) -> i32 { - self.frames.as_ref().get() - } -} - -impl Drop for FrameCounter { - fn drop(&mut self) { - window.cancel_animation_frame_or_warn(self.js_on_frame_handle_id.get()); - } -} diff --git a/lib/rust/web/src/resize_observer.rs b/lib/rust/web/src/resize_observer.rs index 80f17261c21..bd45d865a12 100644 --- a/lib/rust/web/src/resize_observer.rs +++ b/lib/rust/web/src/resize_observer.rs @@ -2,6 +2,7 @@ use crate::prelude::*; +use crate::CleanupHandle; use crate::Closure; use crate::JsValue; @@ -11,35 +12,6 @@ use crate::JsValue; // === Types === // ============= -/// Listener closure for the [`ResizeObserver`]. -pub type Listener = Closure; - - - -// =================== -// === JS Bindings === -// =================== - -#[cfg(target_arch = "wasm32")] -use wasm_bindgen::prelude::wasm_bindgen; - -#[cfg(target_arch = "wasm32")] -#[wasm_bindgen(module = "/js/resize_observer.js")] -extern "C" { - #[allow(unsafe_code)] - fn resize_observe(target: &JsValue, closure: &Listener) -> usize; - - #[allow(unsafe_code)] - fn resize_unobserve(id: usize); -} - -#[cfg(not(target_arch = "wasm32"))] -fn resize_observe(_target: &JsValue, _closure: &Listener) -> usize { - 0 -} -#[cfg(not(target_arch = "wasm32"))] -fn resize_unobserve(_id: usize) {} - // ====================== // === ResizeObserver === @@ -54,22 +26,17 @@ fn resize_unobserve(_id: usize) {} #[derive(Debug)] #[allow(missing_docs)] pub struct ResizeObserver { - pub target: JsValue, - pub listener: Listener, - pub observer_id: usize, + pub observer: CleanupHandle, } impl ResizeObserver { /// Constructor. - pub fn new(target: &JsValue, listener: Listener) -> Self { - let target = target.clone_ref(); - let observer_id = resize_observe(&target, &listener); - Self { target, listener, observer_id } - } -} - -impl Drop for ResizeObserver { - fn drop(&mut self) { - resize_unobserve(self.observer_id); + pub fn new(target: &JsValue, listener: impl FnMut(f32, f32) + 'static) -> Self { + Self { + observer: CleanupHandle::new( + Closure::::new(listener), + |callback| crate::register_resize_observer(target, callback), + ), + } } } diff --git a/lib/rust/web/src/stream.rs b/lib/rust/web/src/stream.rs deleted file mode 100644 index 19814ef6a49..00000000000 --- a/lib/rust/web/src/stream.rs +++ /dev/null @@ -1,65 +0,0 @@ -//! Helpers for the Web Streaming API in Rust, mostly the missing bindings in the [`web_sys`] crate. - -use wasm_bindgen::prelude::wasm_bindgen; -use wasm_bindgen::JsCast; -use wasm_bindgen::JsValue; - - - -// =================================== -// === ReadableStreamDefaultReader === -// =================================== - -#[wasm_bindgen] -extern "C" { - /// The wrapper for ReadableStreamDefaultReader js class. - /// - /// See https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamDefaultReader. - pub type ReadableStreamDefaultReader; - - // Returns a Promise providing access to the next chunk in the stream's internal queue. - // - // See https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamDefaultReader/read. - #[allow(unsafe_code)] - #[wasm_bindgen(method)] - pub fn read(this: &ReadableStreamDefaultReader) -> js_sys::Promise; -} - - - -// =============== -// === BlobExt === -// =============== - -/// The extension for [`js_sys::Blob`] API. -// TODO[ao] Those functions are part of the official API on newer web_sys versions, however the -// version bump is tricky, see https://github.com/enso-org/ide/issues/1591. -pub trait BlobExt { - /// Returns a ReadableStream which upon reading returns the data contained within the Blob. - /// See https://developer.mozilla.org/en-US/docs/Web/API/Blob/stream. - fn stream(&self) -> Result; - - /// Returns a Reader of the Blob data. It assumes that the reader is of - /// [`ReadableStreamDefaultReader`] type. See also - /// https://developer.mozilla.org/en-US/docs/Web/API/Blob/stream and - /// https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/getReader - fn stream_reader(&self) -> Result; -} - -impl BlobExt for web_sys::Blob { - #[allow(unused_qualifications)] - fn stream(&self) -> Result { - let this = self.as_ref(); - let method_as_value = js_sys::Reflect::get(this, &"stream".into())?; - let method = method_as_value.dyn_into::()?; - method.call0(this)?.dyn_into() - } - - #[allow(unused_qualifications)] - fn stream_reader(&self) -> Result { - let stream = self.stream(); - let method_as_value = js_sys::Reflect::get(&stream, &"getReader".into())?; - let method = method_as_value.dyn_into::()?; - method.call0(&stream)?.dyn_into() - } -} diff --git a/lib/rust/web/tsconfig.json b/lib/rust/web/tsconfig.json new file mode 100644 index 00000000000..6a7a8424cf4 --- /dev/null +++ b/lib/rust/web/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../ensogl/pack/js/tsconfig.json" +}