diff --git a/gui/src/rust/ide/Cargo.toml b/gui/src/rust/ide/Cargo.toml index dc02b56fa67..9e955ce7208 100644 --- a/gui/src/rust/ide/Cargo.toml +++ b/gui/src/rust/ide/Cargo.toml @@ -62,8 +62,9 @@ features = [ 'CloseEvent', 'Document', 'Element', - "ErrorEvent", - "MessageEvent", + 'ErrorEvent', + 'EventTarget', + 'MessageEvent', 'HtmlElement', 'Node', 'WebSocket', diff --git a/gui/src/rust/ide/lib/json-rpc/src/transport.rs b/gui/src/rust/ide/lib/json-rpc/src/transport.rs index 65418488c49..e22b86711d3 100644 --- a/gui/src/rust/ide/lib/json-rpc/src/transport.rs +++ b/gui/src/rust/ide/lib/json-rpc/src/transport.rs @@ -42,5 +42,6 @@ pub enum TransportEvent { /// A socket has been opened. Opened, /// A socket has been closed by the peer. + /// This event may be also emitted when reconnecting has failed. Closed, } diff --git a/gui/src/rust/ide/src/executor/test_utils.rs b/gui/src/rust/ide/src/executor/test_utils.rs index 695ea83e0d2..cc84d89e4b1 100644 --- a/gui/src/rust/ide/src/executor/test_utils.rs +++ b/gui/src/rust/ide/src/executor/test_utils.rs @@ -29,6 +29,11 @@ impl TestWithLocalPoolExecutor { Self {executor,running_task_count} } + /// Check if there are any uncompleted tasks in the pool. + pub fn has_ongoing_task(&self) -> bool { + self.running_task_count.get() > 0 + } + /// Spawn new task in executor. pub fn run_task(&mut self, task: Task) where Task : Future + 'static { @@ -47,7 +52,7 @@ impl TestWithLocalPoolExecutor { pub fn when_stalled(&mut self, callback:Callback) where Callback : FnOnce() { self.run_until_stalled(); - if self.running_task_count.get() > 0 { + if self.has_ongoing_task() { callback(); } } @@ -59,7 +64,7 @@ impl TestWithLocalPoolExecutor { pub fn when_stalled_run_task(&mut self, task : Task) where Task : Future + 'static { self.run_until_stalled(); - if self.running_task_count.get() > 0 { + if self.has_ongoing_task() { self.run_task(task); } } diff --git a/gui/src/rust/ide/src/executor/web.rs b/gui/src/rust/ide/src/executor/web.rs index 6c46d296ae9..2f62ab052c7 100644 --- a/gui/src/rust/ide/src/executor/web.rs +++ b/gui/src/rust/ide/src/executor/web.rs @@ -1,6 +1,8 @@ //! Module defining `JsExecutor` - an executor that tries running until stalled //! on each animation frame callback call. +pub mod test; + use crate::prelude::*; use ensogl::control::callback; diff --git a/gui/src/rust/ide/src/executor/web/test.rs b/gui/src/rust/ide/src/executor/web/test.rs new file mode 100644 index 00000000000..7e0c649eff5 --- /dev/null +++ b/gui/src/rust/ide/src/executor/web/test.rs @@ -0,0 +1,8 @@ +//! Utilities for tests related to the web-based executors. + +/// Set up a global animation-frame-based executor. +/// Leaks it handle so it will run indefinitely. +/// To be used in asynchronous wasm tests. +pub fn setup_and_forget() { + std::mem::forget(crate::initializer::setup_global_executor()); +} diff --git a/gui/src/rust/ide/src/ide/initializer.rs b/gui/src/rust/ide/src/ide/initializer.rs index a132f237855..6ddb783c248 100644 --- a/gui/src/rust/ide/src/ide/initializer.rs +++ b/gui/src/rust/ide/src/ide/initializer.rs @@ -279,8 +279,8 @@ async fn create_project_model ) -> FallibleResult { info!(logger, "Establishing Language Server connection."); let client_id = Uuid::new_v4(); - let json_ws = WebSocket::new_opened(logger,json_endpoint).await?; - let binary_ws = WebSocket::new_opened(logger,binary_endpoint).await?; + let json_ws = WebSocket::new_opened(logger,&json_endpoint).await?; + let binary_ws = WebSocket::new_opened(logger,&binary_endpoint).await?; let client_json = language_server::Client::new(json_ws); let client_binary = binary::Client::new(logger,binary_ws); crate::executor::global::spawn(client_json.runner()); diff --git a/gui/src/rust/ide/src/transport/web.rs b/gui/src/rust/ide/src/transport/web.rs index ed9e39f4b2a..20104fd6fa2 100644 --- a/gui/src/rust/ide/src/transport/web.rs +++ b/gui/src/rust/ide/src/transport/web.rs @@ -2,8 +2,8 @@ use crate::prelude::*; -use ensogl_system_web::closure::storage::OptionalFmMutClosure; use ensogl_system_web::js_to_string; +use ensogl_system_web::event::listener::Slot; use failure::Error; use futures::channel::mpsc; use json_rpc::Transport; @@ -11,9 +11,6 @@ use json_rpc::TransportEvent; use utils::channel; use wasm_bindgen::JsCast; use web_sys::BinaryType; -use web_sys::CloseEvent; -use web_sys::Event; -use web_sys::MessageEvent; @@ -36,6 +33,14 @@ pub enum ConnectingError { FailedToConnect, } +impl ConnectingError { + /// Create a `ConstructionError` value from a JS value describing an error. + pub fn construction_error(js_val:impl AsRef) -> Self { + let text = js_to_string(js_val); + ConnectingError::ConstructionError(text) + } +} + /// Error that may occur when attempting to send the data over WebSocket /// transport. #[derive(Clone,Debug,Fail)] @@ -51,7 +56,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:JsValue) -> SendingError { - SendingError::FailedToSend(js_to_string(error)) + SendingError::FailedToSend(js_to_string(&error)) } } @@ -97,54 +102,204 @@ impl State { +// ================= +// === JS Events === +// ================= + +/// Description of events that can be emitted by JS WebSocket. +pub mod event { + use super::*; + use ensogl_system_web::event::Type; + + /// Represents WebSocket.open event. + #[derive(Clone,Copy,Debug)] + pub enum Open{} + impl Type for Open { + type Interface = web_sys::Event; + type Target = web_sys::WebSocket; + const NAME:&'static str = "open"; + } + + /// Represents WebSocket.close event. + #[derive(Clone,Copy,Debug)] + pub enum Close{} + impl Type for Close { + type Interface = web_sys::CloseEvent; + type Target = web_sys::WebSocket; + const NAME:&'static str = "close"; + } + + /// Represents WebSocket.message event. + #[derive(Clone,Copy,Debug)] + pub enum Message{} + impl Type for Message { + type Interface = web_sys::MessageEvent; + type Target = web_sys::WebSocket; + const NAME:&'static str = "message"; + } + + /// Represents WebSocket.error event. + #[derive(Clone,Copy,Debug)] + pub enum Error{} + impl Type for Error { + type Interface = web_sys::Event; + type Target = web_sys::WebSocket; + const NAME:&'static str = "error"; + } +} + + + +// ============= +// === Model === +// ============= + +/// An owning wrapper over JS `WebSocket` object and callbacks to its signals. +#[derive(Derivative)] +#[derivative(Debug)] +#[allow(missing_docs)] +struct Model { + // === User-provided callbacks === + pub on_close : Slot, + pub on_message : Slot, + pub on_open : Slot, + pub on_error : Slot, + + + // === Internal === + pub logger : Logger, + pub socket : web_sys::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. + pub on_close_internal : Slot, + /// When enabled, the WS will try to automatically reconnect whenever connection is lost. + pub auto_reconnect : bool, +} + +impl Model { + /// Wraps given WebSocket object. + pub fn new(socket:web_sys::WebSocket, logger:Logger) -> Model { + socket.set_binary_type(BinaryType::Arraybuffer); + Model { + on_close : Slot::new(&socket, &logger), + on_message : Slot::new(&socket, &logger), + on_open : Slot::new(&socket, &logger), + on_error : Slot::new(&socket, &logger), + on_close_internal : Slot::new(&socket, &logger), + auto_reconnect : true, + logger, + socket, + } + } + + /// Close the socket. + pub fn close(&mut self, reason:&str) -> Result<(),JsValue> { + // If socket was manually requested to close, it should not try to reconnect then. + self.auto_reconnect = false; + let normal_closure = 1000; + self.socket.close_with_code_and_reason(normal_closure,reason)?; + self.clear_callbacks(); + Ok(()) + } + + /// Clear all the available callbacks. + pub fn clear_callbacks(&mut self) { + // We list explicitly all the fields, to get a compiler error when a new slot as added + // but not handled here. + #[allow(clippy::unneeded_field_pattern)] + let Self{ + // Callback slots to be cleared. + on_close, on_error, on_message, on_open, on_close_internal, + // Explicitly ignored non-slot fields. + auto_reconnect:_, logger:_, socket:_ + } = self; + // We don't care if removing actually removed anything. + // If callbacks were not set, then they are clear from the start. + on_close.clear_callback(); + on_error.clear_callback(); + on_message.clear_callback(); + on_open.clear_callback(); + on_close_internal.clear_callback() + } + + /// 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<(),JsValue> { + if !self.auto_reconnect { + return Err(js_sys::Error::new("Reconnecting has been disabled").into()); + } + + let url = self.socket.url(); + info!(self.logger, "Reconnecting WS to {url}."); + + let new_ws = web_sys::WebSocket::new(&url)?; + + self.on_close. set_target(&new_ws); + self.on_error. set_target(&new_ws); + self.on_message. set_target(&new_ws); + self.on_open. set_target(&new_ws); + self.on_close_internal.set_target(&new_ws); + self.socket = new_ws; + + Ok(()) + } +} + +impl Drop for Model { + fn drop(&mut self) { + info!(self.logger, "Dropping WS model."); + if let Err(e) = self.close("Rust Value has been dropped.") { + error!(self.logger,"Error when closing socket due to being dropped: {js_to_string(&e)}") + } + } +} + + + // ================= // === WebSocket === // ================= -/// Wrapper over JS `WebSocket` object and callbacks to its signals. -#[derive(Debug)] +/// Wrapper over JS `WebSocket` meant for general use. +#[derive(Clone,CloneRef,Debug)] pub struct WebSocket { #[allow(missing_docs)] - pub logger : Logger, - /// Handle to the JS `WebSocket` object. - pub ws : web_sys::WebSocket, - /// Handle to a closure connected to `WebSocket.onmessage`. - pub on_message : OptionalFmMutClosure, - /// Handle to a closure connected to `WebSocket.onclose`. - pub on_close : OptionalFmMutClosure, - /// Handle to a closure connected to `WebSocket.onopen`. - pub on_open : OptionalFmMutClosure, - /// Handle to a closure connected to `WebSocket.onerror`. - pub on_error : OptionalFmMutClosure, + pub logger : Logger, + model : Rc>, } impl WebSocket { - /// Wraps given WebSocket object. - pub fn new - (ws:web_sys::WebSocket, parent:impl AnyLogger, name:impl AsRef) -> WebSocket { - ws.set_binary_type(BinaryType::Arraybuffer); - WebSocket { - ws, - logger : Logger::sub(parent,name), - on_message : default(), - on_close : default(), - on_open : default(), - on_error : default(), - } + /// Wrap given raw JS WebSocket object. + pub fn new(ws:web_sys::WebSocket, parent:impl AnyLogger) -> WebSocket { + let logger = Logger::sub(parent,ws.url()); + let model = Rc::new(RefCell::new(Model::new(ws,logger.clone()))); + WebSocket {logger,model} } /// 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 - (parent:impl AnyLogger, url:impl Str) -> Result { - let ws = web_sys::WebSocket::new(url.as_ref()).map_err(|e| { - ConnectingError::ConstructionError(js_to_string(e)) - })?; - let mut wst = WebSocket::new(ws,&parent,url.into()); + (parent:impl AnyLogger, url:&str) -> Result { + let ws = web_sys::WebSocket::new(url).map_err(ConnectingError::construction_error)?; + let mut wst = WebSocket::new(ws,&parent); 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) { + let model = Rc::downgrade(&self.model); + let logger = self.logger.clone(); + move |_| { + if let Some(model) = model.upgrade() { + if let Err(e) = model.borrow_mut().reconnect() { + error!(logger,"Failed to reconnect: {js_to_string(&e)}"); + } + } + } + } + /// Awaits until `open` signal has been emitted. Clears any callbacks on /// this `WebSocket`, if any has been set. async fn wait_until_open(&mut self) -> Result<(),ConnectingError> { @@ -152,6 +307,7 @@ impl WebSocket { // We shall wait for whatever comes first. let (transmitter, mut receiver) = mpsc::unbounded::>(); let transmitter_clone = transmitter.clone(); + self.set_on_close(move |_| { // Note [mwu] Ignore argument, `CloseEvent` here contains rubbish // anyway, nothing useful to pass to caller. Error code or reason @@ -164,7 +320,8 @@ impl WebSocket { match receiver.next().await { Some(Ok(())) => { - self.clear_callbacks(); + self.model.borrow_mut().clear_callbacks(); + self.model.borrow_mut().on_close_internal.set_callback(self.reconnect_trigger()); info!(self.logger, "Connection opened."); Ok(()) } @@ -174,43 +331,35 @@ impl WebSocket { /// Checks the current state of the connection. pub fn state(&self) -> State { - State::query_ws(&self.ws) + State::query_ws(&self.model.borrow().socket) + } + + fn with_borrow_mut_model(&mut self, f:impl FnOnce(&mut Model) -> R) -> R { + with(self.model.borrow_mut(), |mut model| f(model.deref_mut())) } /// Sets callback for the `close` event. - pub fn set_on_close(&mut self, f:impl FnMut(CloseEvent) + 'static) { - self.on_close.wrap(f); - self.ws.set_onclose(self.on_close.js_ref()); + pub fn set_on_close(&mut self, f:impl FnMut(web_sys::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. + model.on_close_internal.reattach(); + }); } /// Sets callback for the `error` event. - pub fn set_on_error(&mut self, f:impl FnMut(Event) + 'static) { - self.on_error.wrap(f); - self.ws.set_onerror(self.on_error.js_ref()); + pub fn set_on_error(&mut self, f:impl FnMut(web_sys::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(MessageEvent) + 'static) { - self.on_message.wrap(f); - self.ws.set_onmessage(self.on_message.js_ref()); + pub fn set_on_message(&mut self, f:impl FnMut(web_sys::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(Event) + 'static) { - self.on_open.wrap(f); - self.ws.set_onopen(self.on_open.js_ref()); - } - - /// Clears all the available callbacks. - pub fn clear_callbacks(&mut self) { - self.on_close .clear(); - self.on_error .clear(); - self.on_message.clear(); - self.on_open .clear(); - self.ws.set_onclose(None); - self.ws.set_onerror(None); - self.ws.set_onmessage(None); - self.ws.set_onopen(None); + pub fn set_on_open(&mut self, f:impl FnMut(web_sys::Event) + 'static) { + self.with_borrow_mut_model(move |model| model.on_open.set_callback(f)) } /// Executes a given function with a mutable reference to the socket. @@ -218,7 +367,9 @@ impl WebSocket { /// /// Fails if the socket is not opened or if the sending function failed. /// The error from `F` shall be translated into `SendingError`. - pub fn send_with_open_socket(&mut self, f:F) -> Result + /// + /// 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 { // Sending through the closed WebSocket can return Ok() with error only // appearing in the log. We explicitly check for this to get failure as @@ -230,7 +381,7 @@ impl WebSocket { if state != State::Open { Err(SendingError::NotOpen(state).into()) } else { - let result = f(&mut self.ws); + let result = f(&mut self.model.borrow_mut().socket); result.map_err(|e| SendingError::from_send_error(e).into()) } } @@ -238,13 +389,13 @@ impl WebSocket { impl Transport for WebSocket { fn send_text(&mut self, message:&str) -> Result<(), Error> { - info!(self.logger, "Sending text message of length {message.len()}"); + info!(self.logger, "Sending text message of length {message.len()}."); debug!(self.logger, "Message contents: {message}"); self.send_with_open_socket(|ws| ws.send_with_str(message)) } fn send_binary(&mut self, message:&[u8]) -> Result<(), Error> { - info!(self.logger, "Sending binary message of length {message.len()}"); + info!(self.logger, "Sending binary message of length {message.len()}."); debug!(self.logger,|| format!("Message contents: {:x?}", message)); // TODO [mwu] // Here we workaround issue from wasm-bindgen 0.2.58: @@ -291,3 +442,43 @@ impl Transport for WebSocket { }); } } + +#[cfg(test)] +mod tests { + use super::*; + + use ensogl::system::web; + use std::time::Duration; + + + /// Provisional code allowing testing WS behavior and its events. + /// Keeping it for future debug purposes. + /// To run uncomment attribute line and invoke: + /// `cargo watch -- wasm-pack test .\ide\ --chrome -- websocket_test` + //#[wasm_bindgen_test::wasm_bindgen_test] + #[allow(dead_code)] + async fn websocket_tests() { + web::set_stdout(); + executor::web::test::setup_and_forget(); + let logger = DefaultTraceLogger::new("Test"); + info!(logger,"Started"); + + // Create WebSocket + let ws = WebSocket::new_opened(&logger,"ws://localhost:30445").await; + let mut ws = ws.expect("Couldn't connect to WebSocket server."); + info!(logger,"WebSocket opened: {ws:?}"); + + // Log events + let handler = ws.establish_event_stream().for_each(f!([logger](event) { + info!(logger,"Socket emitted event: {event:?}"); + futures::future::ready(()) + })); + + // Spawn task to process events stream. + executor::global::spawn(handler); + + // Close socket after some delay. + web::sleep(Duration::from_secs(20)).await; + info!(logger,"Finished"); + } +} diff --git a/gui/src/rust/ide/view/graph-editor/src/component/visualization/foreign/java_script/instance.rs b/gui/src/rust/ide/view/graph-editor/src/component/visualization/foreign/java_script/instance.rs index 3e236cf32fc..165210705e0 100644 --- a/gui/src/rust/ide/view/graph-editor/src/component/visualization/foreign/java_script/instance.rs +++ b/gui/src/rust/ide/view/graph-editor/src/component/visualization/foreign/java_script/instance.rs @@ -277,7 +277,7 @@ impl Instance { use enso_frp::web::js_to_string; let logger = self.model.logger.clone(); error!(logger,"Failed to trigger initial preprocessor update from JS: \ - {js_to_string(js_error)}"); + {js_to_string(&js_error)}"); } self } diff --git a/gui/src/rust/lib/system/web/src/closure/storage.rs b/gui/src/rust/lib/system/web/src/closure/storage.rs index a44cf388641..96f2bc0c132 100644 --- a/gui/src/rust/lib/system/web/src/closure/storage.rs +++ b/gui/src/rust/lib/system/web/src/closure/storage.rs @@ -32,8 +32,12 @@ impl OptionalFmMutClosure { } /// Stores the given closure. - pub fn store(&mut self, closure: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 @@ -43,12 +47,12 @@ impl OptionalFmMutClosure { } /// Wraps given function into a Closure. - pub fn wrap(&mut self, f:impl ClosureFn) { + 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); + self.store(wrapped) } /// Clears the current closure. @@ -57,4 +61,20 @@ impl OptionalFmMutClosure { 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/gui/src/rust/lib/system/web/src/event.rs b/gui/src/rust/lib/system/web/src/event.rs new file mode 100644 index 00000000000..8263da9c399 --- /dev/null +++ b/gui/src/rust/lib/system/web/src/event.rs @@ -0,0 +1,51 @@ +//! Utilities for DOM events. + +pub mod listener; + +use crate::prelude::*; + +use js_sys::Function; +use web_sys::EventTarget; + + + +// ============= +// === Event === +// ============= + +/// This trait represents a type of event that may fire from some specific JS `EventTarget`. +/// +/// For example, `WebSocket.close` is such an event, where `close` is event type and `WebSocket` is +/// the `EventTarget`. +/// +/// The purpose is to increase type safety by grouping event type name, event target type and event +/// value type together. +/// +/// Typically this trait is to be implemented for uncreatable types, created for the sole +/// purpose of denoting a particular event type within a context of an event target. +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; + + /// 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 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/gui/src/rust/lib/system/web/src/event/listener.rs b/gui/src/rust/lib/system/web/src/event/listener.rs new file mode 100644 index 00000000000..1d0db538e9f --- /dev/null +++ b/gui/src/rust/lib/system/web/src/event/listener.rs @@ -0,0 +1,117 @@ +use crate::prelude::*; + +use crate::closure::storage::ClosureFn; +use crate::closure::storage::OptionalFmMutClosure; + + + +// ============ +// === Slot === +// ============ + +/// A Slot stores a callback and manages its connection with JS `EventTarget`. +/// +/// Both callback and the target can be set independently using `set_target` and `set_callback`. +/// Additionally, callback can be cleared at any point by `clear_callback`. +/// +/// When both target and callback are set, slot ensures that the callback is registered as an +/// event listener in the target. +/// +/// When changing target, `Slot` reattaches callback. +/// +/// `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 { + logger : Logger, + #[derivative(Debug="ignore")] + target : Option, + js_closure : OptionalFmMutClosure, +} + +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, logger:impl AnyLogger) -> Self { + Self { + logger : Logger::sub(logger, EventType::NAME), + target : Some(target.clone()), + js_closure : default(), + } + } + + /// 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!(self.logger,"Attaching the callback."); + EventType::add_listener(target, function) + } + } + + /// 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!(self.logger,"Detaching the callback."); + EventType::remove_listener(target, function) + } + } + + /// Set a new target. + /// + /// If callback is set, it will be reattached as a listener to a newly set target. + pub fn set_target(&mut self, target:&EventType::Target) { + // Prevent spurious reattaching that could affect listeners order. + if Some(target) != self.target.as_ref() { + self.remove_if_active(); + self.target = Some(target.clone()); + self.add_if_active() + } + } + + /// Clear event target. + /// + /// If callback is set, it will be unregistered. + pub fn clear_target(&mut self, target:&EventType::Target) { + // Prevent spurious reattaching that could affect listeners order. + if Some(target) != self.target.as_ref() { + self.remove_if_active(); + self.target = None; + } + } + + /// Assign a new event callback closure and register it in the target. + /// + /// If the listener was registered with the previous closure, it will unregister first. + /// + /// 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) { + self.remove_if_active(); + self.js_closure.wrap(f); + self.add_if_active() + } + + /// Erase the callback. + /// + /// The stored closure will be dropped and event listener unregistered. + pub fn clear_callback(&mut self) { + self.remove_if_active(); + self.js_closure.clear(); + } + + /// Detach and attach the listener to the target. + /// + /// The purpose is to move this slot to the end of the listeners list. + pub fn reattach(&mut self) { + self.remove_if_active(); + self.add_if_active(); + } +} + +/// Unregister listener on drop. +impl Drop for Slot { + fn drop(&mut self) { + self.remove_if_active(); + } +} diff --git a/gui/src/rust/lib/system/web/src/lib.rs b/gui/src/rust/lib/system/web/src/lib.rs index 8f9e8822063..2bdb1d50b29 100644 --- a/gui/src/rust/lib/system/web/src/lib.rs +++ b/gui/src/rust/lib/system/web/src/lib.rs @@ -7,11 +7,14 @@ pub mod clipboard; pub mod closure; +pub mod event; pub mod resize_observer; pub mod platform; /// Common types that should be visible across the whole crate. pub mod prelude { + pub use enso_logger::*; + pub use enso_logger::DefaultInfoLogger as Logger; pub use enso_prelude::*; pub use wasm_bindgen::prelude::*; } @@ -77,11 +80,15 @@ impl From for Error { #[wasm_bindgen] extern "C" { - /// Converts given `JsValue` into a `String`. Uses JS's `String` function, - /// see: https://www.w3schools.com/jsref/jsref_string.asp #[allow(unsafe_code)] #[wasm_bindgen(js_name="String")] - pub fn js_to_string(s: JsValue) -> String; + fn js_to_string_inner(s:&JsValue) -> String; +} + +/// Converts given `JsValue` into a `String`. Uses JS's `String` function, +/// see: https://www.w3schools.com/jsref/jsref_string.asp +pub fn js_to_string(s:impl AsRef) -> String { + js_to_string_inner(s.as_ref()) } @@ -651,7 +658,7 @@ pub fn reflect_get_nested_string(target:&JsValue, keys:&[&str]) -> Result