From 93c8a77b347b9934ec0732784d4b78b3260abc08 Mon Sep 17 00:00:00 2001 From: Amr Bashir Date: Mon, 23 Oct 2023 21:10:01 +0300 Subject: [PATCH] refactor: enhance event system rust apis (#7996) Co-authored-by: Lucas Nogueira --- .changes/tauri-event-system-apis.md | 12 ++ core/tauri/src/app.rs | 2 +- core/tauri/src/event/listener.rs | 113 +++++++---- core/tauri/src/event/mod.rs | 102 ++++++++-- core/tauri/src/event/plugin.rs | 27 +-- core/tauri/src/lib.rs | 164 +++++++-------- core/tauri/src/manager.rs | 301 ++++++++++++++++++++++------ core/tauri/src/scope/fs.rs | 10 +- core/tauri/src/window/mod.rs | 137 +++---------- 9 files changed, 521 insertions(+), 347 deletions(-) create mode 100644 .changes/tauri-event-system-apis.md diff --git a/.changes/tauri-event-system-apis.md b/.changes/tauri-event-system-apis.md new file mode 100644 index 000000000000..adc60a5a0fb9 --- /dev/null +++ b/.changes/tauri-event-system-apis.md @@ -0,0 +1,12 @@ +--- +'tauri': 'major:breaking' +--- + +The event system APIS on Rust is recieving a few changes for consistency and quality of life improvements: + +- Renamed `Manager::emit_all` to just `Manager::emit` and will now both trigger the events on JS side as well as Rust. +- Removed `Manager::trigger_global`, use `Manager::emit` +- Added `Manager::emit_filter`. +- Removed `Window::emit`, and moved the implementation to `Manager::emit`. +- Removed `Window::emit_and_trigger` and `Window::trigger`, use `Window::emit` instead. +- Changed `Window::emit_to` to only trigger the target window listeners so it won't be catched by `Manager::listen_global` diff --git a/core/tauri/src/app.rs b/core/tauri/src/app.rs index 2c953f5aa35f..fa993606971b 100644 --- a/core/tauri/src/app.rs +++ b/core/tauri/src/app.rs @@ -246,7 +246,7 @@ impl GlobalWindowEvent { &self.event } - /// The window that the menu belongs to. + /// The window that the event belongs to. pub fn window(&self) -> &Window { &self.window } diff --git a/core/tauri/src/event/listener.rs b/core/tauri/src/event/listener.rs index 6b9fa2f9075b..a6182414fee7 100644 --- a/core/tauri/src/event/listener.rs +++ b/core/tauri/src/event/listener.rs @@ -2,7 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use super::{Event, EventId}; +use crate::{Runtime, Window}; + +use super::{EmitArgs, Event, EventId}; use std::{ boxed::Box, @@ -15,33 +17,33 @@ use std::{ }; /// What to do with the pending handler when resolving it? -enum Pending { +enum Pending { Unlisten(EventId), - Listen(EventId, String, Handler), - Trigger(String, Option, Option), + Listen(EventId, String, Handler), + Emit(EmitArgs), } /// Stored in [`Listeners`] to be called upon when the event that stored it is triggered. -struct Handler { - window: Option, +struct Handler { + window: Option>, callback: Box, } /// Holds event handlers and pending event handlers, along with the salts associating them. -struct InnerListeners { - handlers: Mutex>>, - pending: Mutex>, +struct InnerListeners { + handlers: Mutex>>>, + pending: Mutex>>, function_name: &'static str, listeners_object_name: &'static str, next_event_id: Arc, } /// A self-contained event manager. -pub struct Listeners { - inner: Arc, +pub struct Listeners { + inner: Arc>, } -impl Default for Listeners { +impl Default for Listeners { fn default() -> Self { Self { inner: Arc::new(InnerListeners { @@ -55,7 +57,7 @@ impl Default for Listeners { } } -impl Clone for Listeners { +impl Clone for Listeners { fn clone(&self) -> Self { Self { inner: self.inner.clone(), @@ -63,7 +65,7 @@ impl Clone for Listeners { } } -impl Listeners { +impl Listeners { pub(crate) fn next_event_id(&self) -> EventId { self.inner.next_event_id.fetch_add(1, Ordering::Relaxed) } @@ -79,7 +81,7 @@ impl Listeners { } /// Insert a pending event action to the queue. - fn insert_pending(&self, action: Pending) { + fn insert_pending(&self, action: Pending) { self .inner .pending @@ -89,7 +91,7 @@ impl Listeners { } /// Finish all pending event actions. - fn flush_pending(&self) { + fn flush_pending(&self) -> crate::Result<()> { let pending = { let mut lock = self .inner @@ -102,13 +104,17 @@ impl Listeners { for action in pending { match action { Pending::Unlisten(id) => self.unlisten(id), - Pending::Listen(id, event, handler) => self.listen_(id, event, handler), - Pending::Trigger(ref event, window, payload) => self.trigger(event, window, payload), + Pending::Listen(id, event, handler) => self.listen_with_id(id, event, handler), + Pending::Emit(args) => { + self.emit(&args)?; + } } } + + Ok(()) } - fn listen_(&self, id: EventId, event: String, handler: Handler) { + fn listen_with_id(&self, id: EventId, event: String, handler: Handler) { match self.inner.handlers.try_lock() { Err(_) => self.insert_pending(Pending::Listen(id, event, handler)), Ok(mut lock) => { @@ -117,11 +123,11 @@ impl Listeners { } } - /// Adds an event listener for JS events. + /// Adds an event listener. pub(crate) fn listen( &self, event: String, - window: Option, + window: Option>, handler: F, ) -> EventId { let id = self.next_event_id(); @@ -130,16 +136,16 @@ impl Listeners { callback: Box::new(handler), }; - self.listen_(id, event, handler); + self.listen_with_id(id, event, handler); id } - /// Listen to a JS event and immediately unlisten. + /// Listen to an event and immediately unlisten. pub(crate) fn once( &self, event: String, - window: Option, + window: Option>, handler: F, ) { let self_ = self.clone(); @@ -164,19 +170,42 @@ impl Listeners { } } - /// Triggers the given global event with its payload. - pub(crate) fn trigger(&self, event: &str, window: Option, payload: Option) { + /// Emits the given event with its payload based on a filter. + pub(crate) fn emit_filter(&self, emit_args: &EmitArgs, filter: Option) -> crate::Result<()> + where + F: Fn(&Window) -> bool, + { let mut maybe_pending = false; match self.inner.handlers.try_lock() { - Err(_) => self.insert_pending(Pending::Trigger(event.to_owned(), window, payload)), + Err(_) => self.insert_pending(Pending::Emit(emit_args.clone())), Ok(lock) => { - if let Some(handlers) = lock.get(event) { - for (&id, handler) in handlers { - if handler.window.is_none() || window == handler.window { + if let Some(handlers) = lock.get(&emit_args.event_name) { + let handlers = if let Some(filter) = filter { + handlers + .iter() + .filter(|h| { + h.1 + .window + .as_ref() + .map(|w| { + // clippy sees this as redundant closure but + // fixing it will result in a compiler error + #[allow(clippy::redundant_closure)] + filter(w) + }) + .unwrap_or(false) + }) + .collect::>() + } else { + handlers.iter().collect::>() + }; + + if !handlers.is_empty() { + for (&id, handler) in handlers { maybe_pending = true; (handler.callback)(self::Event { id, - data: payload.clone(), + data: emit_args.payload.clone(), }) } } @@ -185,14 +214,22 @@ impl Listeners { } if maybe_pending { - self.flush_pending(); + self.flush_pending()?; } + + Ok(()) + } + + /// Emits the given event with its payload. + pub(crate) fn emit(&self, emit_args: &EmitArgs) -> crate::Result<()> { + self.emit_filter(emit_args, None::<&dyn Fn(&Window) -> bool>) } } #[cfg(test)] mod test { use super::*; + use crate::test::MockRuntime; use proptest::prelude::*; // dummy event handler function @@ -206,7 +243,7 @@ mod test { // check to see if listen() is properly passing keys into the LISTENERS map #[test] fn listeners_check_key(e in "[a-z]+") { - let listeners: Listeners = Default::default(); + let listeners: Listeners = Default::default(); // clone e as the key let key = e.clone(); // pass e and an dummy func into listen @@ -222,7 +259,7 @@ mod test { // check to see if listen inputs a handler function properly into the LISTENERS map. #[test] fn listeners_check_fn(e in "[a-z]+") { - let listeners: Listeners = Default::default(); + let listeners: Listeners = Default::default(); // clone e as the key let key = e.clone(); // pass e and an dummy func into listen @@ -248,11 +285,11 @@ mod test { // check to see if on_event properly grabs the stored function from listen. #[test] fn check_on_event(key in "[a-z]+", d in "[a-z]+") { - let listeners: Listeners = Default::default(); - // call listen with e and the event_fn dummy func + let listeners: Listeners = Default::default(); + // call listen with key and the event_fn dummy func listeners.listen(key.clone(), None, event_fn); - // call on event with e and d. - listeners.trigger(&key, None, Some(d)); + // call on event with key and d. + listeners.emit(&EmitArgs { event_name: key.clone(), event: serde_json::to_string(&key).unwrap(), source_window_label: "null".into(), payload: serde_json::to_string(&d).unwrap() })?; // lock the mutex let l = listeners.inner.handlers.lock().unwrap(); diff --git a/core/tauri/src/event/mod.rs b/core/tauri/src/event/mod.rs index decb189b2c05..b16dcb515ed4 100644 --- a/core/tauri/src/event/mod.rs +++ b/core/tauri/src/event/mod.rs @@ -5,6 +5,7 @@ mod listener; pub(crate) mod plugin; pub(crate) use listener::Listeners; +use serde::Serialize; /// Checks if an event name is valid. pub fn is_event_name_valid(event: &str) -> bool { @@ -23,11 +24,39 @@ pub fn assert_event_name_is_valid(event: &str) { /// Unique id of an event. pub type EventId = u32; -/// An event that was triggered. +/// Serialized emit arguments. +#[derive(Clone)] +pub struct EmitArgs { + /// Raw event name. + pub event_name: String, + /// Serialized event name. + pub event: String, + /// Serialized source window label ("null" for global events) + pub source_window_label: String, + /// Serialized payload. + pub payload: String, +} + +impl EmitArgs { + pub fn from( + event: &str, + source_window_label: Option<&str>, + payload: S, + ) -> crate::Result { + Ok(EmitArgs { + event_name: event.into(), + event: serde_json::to_string(event)?, + source_window_label: serde_json::to_string(&source_window_label)?, + payload: serde_json::to_string(&payload)?, + }) + } +} + +/// An event that was emitted. #[derive(Debug, Clone)] pub struct Event { id: EventId, - data: Option, + data: String, } impl Event { @@ -37,27 +66,11 @@ impl Event { } /// The event payload. - pub fn payload(&self) -> Option<&str> { - self.data.as_deref() + pub fn payload(&self) -> &str { + &self.data } } -pub fn unlisten_js(listeners_object_name: &str, event_name: &str, event_id: EventId) -> String { - format!( - " - (function () {{ - const listeners = (window['{listeners_object_name}'] || {{}})['{event_name}'] - if (listeners) {{ - const index = window['{listeners_object_name}']['{event_name}'].findIndex(e => e.id === {event_id}) - if (index > -1) {{ - window['{listeners_object_name}']['{event_name}'].splice(index, 1) - }} - }} - }})() - ", - ) -} - pub fn listen_js( listeners_object_name: &str, event: &str, @@ -92,3 +105,52 @@ pub fn listen_js( }, ) } + +pub fn emit_js(event_emit_function_name: &str, emit_args: &EmitArgs) -> crate::Result { + Ok(format!( + "(function () {{ const fn = window['{}']; fn && fn({{event: {}, windowLabel: {}, payload: {}}}) }})()", + event_emit_function_name, + emit_args.event, + emit_args.source_window_label, + emit_args.payload + )) +} + +pub fn unlisten_js(listeners_object_name: &str, event_name: &str, event_id: EventId) -> String { + format!( + " + (function () {{ + const listeners = (window['{listeners_object_name}'] || {{}})['{event_name}'] + if (listeners) {{ + const index = window['{listeners_object_name}']['{event_name}'].findIndex(e => e.id === {event_id}) + if (index > -1) {{ + window['{listeners_object_name}']['{event_name}'].splice(index, 1) + }} + }} + }})() + ", + ) +} + +pub fn event_initialization_script(function: &str, listeners: &str) -> String { + format!( + " + Object.defineProperty(window, '{function}', {{ + value: function (eventData) {{ + const listeners = (window['{listeners}'] && window['{listeners}'][eventData.event]) || [] + + for (let i = listeners.length - 1; i >= 0; i--) {{ + const listener = listeners[i] + if ( + (listener.windowLabel && listener.windowLabel === eventData.windowLabel) || + (!listener.windowLabel && (listener.windowLabel === null || eventData.windowLabel === null)) + ) {{ + eventData.id = listener.id + listener.handler(eventData) + }} + }} + }} + }}); + " + ) +} diff --git a/core/tauri/src/event/plugin.rs b/core/tauri/src/event/plugin.rs index 236c1a6eb362..004023dbebd4 100644 --- a/core/tauri/src/event/plugin.rs +++ b/core/tauri/src/event/plugin.rs @@ -37,6 +37,12 @@ impl<'de> Deserialize<'de> for EventName { pub struct WindowLabel(String); +impl AsRef for WindowLabel { + fn as_ref(&self) -> &str { + &self.0 + } +} + impl<'de> Deserialize<'de> for WindowLabel { fn deserialize(deserializer: D) -> std::result::Result where @@ -75,25 +81,10 @@ pub fn emit( window_label: Option, payload: Option, ) -> Result<()> { - // dispatch the event to Rust listeners - window.trigger( - &event.0, - payload.as_ref().and_then(|p| { - serde_json::to_string(&p) - .map_err(|e| { - #[cfg(debug_assertions)] - eprintln!("{e}"); - e - }) - .ok() - }), - ); - - // emit event to JS - if let Some(target) = window_label { - window.emit_to(&target.0, &event.0, payload) + if let Some(label) = window_label { + window.emit_filter(&event.0, payload, |w| label.as_ref() == w.label()) } else { - window.emit_all(&event.0, payload) + window.emit(&event.0, payload) } } diff --git a/core/tauri/src/lib.rs b/core/tauri/src/lib.rs index 61a6b2fe662e..fb56aa01e0ea 100644 --- a/core/tauri/src/lib.rs +++ b/core/tauri/src/lib.rs @@ -546,67 +546,7 @@ pub trait Manager: sealed::ManagerBase { self.manager().package_info() } - /// Emits an event to all windows. - /// - /// Only the webviews receives this event. - /// To trigger Rust listeners, use [`Self::trigger_global`], [`Window::trigger`] or [`Window::emit_and_trigger`]. - /// - /// # Examples - /// ``` - /// use tauri::Manager; - /// - /// #[tauri::command] - /// fn synchronize(app: tauri::AppHandle) { - /// // emits the synchronized event to all windows - /// app.emit_all("synchronized", ()); - /// } - /// ``` - fn emit_all(&self, event: &str, payload: S) -> Result<()> { - self.manager().emit_filter(event, None, payload, |_| true) - } - - /// Emits an event to windows matching the filter critera. - /// - /// # Examples - /// ``` - /// use tauri::Manager; - /// - /// #[tauri::command] - /// fn synchronize(app: tauri::AppHandle) { - /// // emits the synchronized event to all windows - /// app.emit_filter("synchronized", (), |w| w.label().starts_with("foo-")); - /// } - /// ``` - fn emit_filter(&self, event: &str, payload: S, filter: F) -> Result<()> - where - S: Serialize + Clone, - F: Fn(&Window) -> bool, - { - self.manager().emit_filter(event, None, payload, filter) - } - - /// Emits an event to the window with the specified label. - /// - /// # Examples - /// ``` - /// use tauri::Manager; - /// - /// #[tauri::command] - /// fn download(app: tauri::AppHandle) { - /// for i in 1..100 { - /// std::thread::sleep(std::time::Duration::from_millis(150)); - /// // emit a download progress event to the updater window - /// app.emit_to("updater", "download-progress", i); - /// } - /// } - /// ``` - fn emit_to(&self, label: &str, event: &str, payload: S) -> Result<()> { - self - .manager() - .emit_filter(event, None, payload, |w| label == w.label()) - } - - /// Listen to a event triggered on any window ([`Window::trigger`] or [`Window::emit_and_trigger`]) or with [`Self::trigger_global`]. + /// Listen to an event emitted on any window. /// /// # Examples /// ``` @@ -615,7 +555,7 @@ pub trait Manager: sealed::ManagerBase { /// #[tauri::command] /// fn synchronize(window: tauri::Window) { /// // emits the synchronized event to all windows - /// window.emit_and_trigger("synchronized", ()); + /// window.emit("synchronized", ()); /// } /// /// tauri::Builder::default() @@ -634,6 +574,34 @@ pub trait Manager: sealed::ManagerBase { self.manager().listen(event.into(), None, handler) } + /// Remove an event listener. + /// + /// # Examples + /// ``` + /// use tauri::Manager; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// let handle = app.handle().clone(); + /// let handler = app.listen_global("ready", move |event| { + /// println!("app is ready"); + /// + /// // we no longer need to listen to the event + /// // we also could have used `app.once_global` instead + /// handle.unlisten(event.id()); + /// }); + /// + /// // stop listening to the event when you do not need it anymore + /// app.unlisten(handler); + /// + /// + /// Ok(()) + /// }); + /// ``` + fn unlisten(&self, id: EventId) { + self.manager().unlisten(id) + } + /// Listen to a global event only once. /// /// See [`Self::listen_global`] for more information. @@ -644,12 +612,27 @@ pub trait Manager: sealed::ManagerBase { self.manager().once(event.into(), None, handler) } - /// Trigger a global event to Rust listeners. - /// To send the events to the webview, see [`Self::emit_all`] and [`Self::emit_to`]. - /// To trigger listeners registed on an specific window, see [`Window::trigger`]. - /// To trigger all listeners, see [`Window::emit_and_trigger`]. + /// Emits an event to all windows. + /// + /// If using [`Window`] to emit the event, it will be used as the source. + /// + /// # Examples + /// ``` + /// use tauri::Manager; + /// + /// #[tauri::command] + /// fn synchronize(app: tauri::AppHandle) { + /// // emits the synchronized event to all windows + /// app.emit("synchronized", ()); + /// } + /// ``` + fn emit(&self, event: &str, payload: S) -> Result<()> { + self.manager().emit(event, None, payload) + } + + /// Emits an event to the window with the specified label. /// - /// A global event does not have a source or target window attached. + /// If using [`Window`] to emit the event, it will be used as the source. /// /// # Examples /// ``` @@ -659,41 +642,40 @@ pub trait Manager: sealed::ManagerBase { /// fn download(app: tauri::AppHandle) { /// for i in 1..100 { /// std::thread::sleep(std::time::Duration::from_millis(150)); - /// // emit a download progress event to all listeners registed in Rust - /// app.trigger_global("download-progress", Some(i.to_string())); + /// // emit a download progress event to the updater window + /// app.emit_to("updater", "download-progress", i); /// } /// } /// ``` - fn trigger_global(&self, event: &str, data: Option) { - self.manager().trigger(event, None, data) + fn emit_to(&self, label: &str, event: &str, payload: S) -> Result<()> { + self + .manager() + .emit_filter(event, None, payload, |w| label == w.label()) } - /// Remove an event listener. + /// Emits an event to specific windows based on a filter. + /// + /// If using [`Window`] to emit the event, it will be used as the source. /// /// # Examples /// ``` /// use tauri::Manager; /// - /// tauri::Builder::default() - /// .setup(|app| { - /// let handle = app.handle().clone(); - /// let handler = app.listen_global("ready", move |event| { - /// println!("app is ready"); - /// - /// // we no longer need to listen to the event - /// // we also could have used `app.once_global` instead - /// handle.unlisten(event.id()); - /// }); - /// - /// // stop listening to the event when you do not need it anymore - /// app.unlisten(handler); - /// - /// - /// Ok(()) - /// }); + /// #[tauri::command] + /// fn download(app: tauri::AppHandle) { + /// for i in 1..100 { + /// std::thread::sleep(std::time::Duration::from_millis(150)); + /// // emit a download progress event to the updater window + /// app.emit_filter("download-progress", i, |w| w.label() == "main" ); + /// } + /// } /// ``` - fn unlisten(&self, id: EventId) { - self.manager().unlisten(id) + fn emit_filter(&self, event: &str, payload: S, filter: F) -> Result<()> + where + S: Serialize + Clone, + F: Fn(&Window) -> bool, + { + self.manager().emit_filter(event, None, payload, filter) } /// Fetch a single window from the manager. diff --git a/core/tauri/src/manager.rs b/core/tauri/src/manager.rs index 358d541e9625..2154b0979e81 100644 --- a/core/tauri/src/manager.rs +++ b/core/tauri/src/manager.rs @@ -27,7 +27,7 @@ use tauri_utils::{ html::{SCRIPT_NONCE_TOKEN, STYLE_NONCE_TOKEN}, }; -use crate::window::WindowEmitArgs; +use crate::event::EmitArgs; use crate::{ app::{ AppHandle, GlobalWindowEvent, GlobalWindowEventListener, OnPageLoad, PageLoadPayload, @@ -225,7 +225,7 @@ fn replace_csp_nonce( pub struct InnerWindowManager { pub(crate) windows: Mutex>>, pub(crate) plugins: Mutex>, - listeners: Listeners, + listeners: Listeners, pub(crate) state: Arc, /// The JS message handler. @@ -847,7 +847,10 @@ impl WindowManager { } .render_default(&Default::default())? .into_string(), - event_initialization_script: &self.event_initialization_script(), + event_initialization_script: &crate::event::event_initialization_script( + self.listeners().function_name(), + self.listeners().listeners_object_name(), + ), plugin_initialization_script, freeze_prototype, } @@ -856,29 +859,7 @@ impl WindowManager { .map_err(Into::into) } - fn event_initialization_script(&self) -> String { - format!( - " - Object.defineProperty(window, '{function}', {{ - value: function (eventData) {{ - const listeners = (window['{listeners}'] && window['{listeners}'][eventData.event]) || [] - - for (let i = listeners.length - 1; i >= 0; i--) {{ - const listener = listeners[i] - if (listener.windowLabel === null || eventData.windowLabel === null || listener.windowLabel === eventData.windowLabel) {{ - eventData.id = listener.id - listener.handler(eventData) - }} - }} - }} - }}); - ", - function = self.listeners().function_name(), - listeners = self.listeners().listeners_object_name() - ) - } - - pub(crate) fn listeners(&self) -> &Listeners { + pub(crate) fn listeners(&self) -> &Listeners { &self.inner.listeners } } @@ -1146,26 +1127,6 @@ impl WindowManager { self.windows_lock().remove(label); } - pub fn emit_filter( - &self, - event: &str, - source_window_label: Option<&str>, - payload: S, - filter: F, - ) -> crate::Result<()> - where - S: Serialize + Clone, - F: Fn(&Window) -> bool, - { - let emit_args = WindowEmitArgs::from(event, source_window_label, payload)?; - assert_event_name_is_valid(event); - self - .windows() - .values() - .filter(|&w| filter(w)) - .try_for_each(|window| window.emit_internal(&emit_args)) - } - pub fn eval_script_all>(&self, script: S) -> crate::Result<()> { let script = script.into(); self @@ -1186,21 +1147,20 @@ impl WindowManager { &self.inner.package_info } - pub fn trigger(&self, event: &str, window: Option, data: Option) { - assert_event_name_is_valid(event); - self.listeners().trigger(event, window, data) - } - pub fn listen( &self, event: String, - window: Option, + window: Option>, handler: F, ) -> EventId { assert_event_name_is_valid(&event); self.listeners().listen(event, window, handler) } + pub fn unlisten(&self, id: EventId) { + self.listeners().unlisten(id) + } + pub fn once( &self, event: String, @@ -1208,19 +1168,63 @@ impl WindowManager { handler: F, ) { assert_event_name_is_valid(&event); - self.listeners().once(event, window, handler) + self + .listeners() + .once(event, window.and_then(|w| self.get_window(&w)), handler) } - pub fn event_listeners_object_name(&self) -> &str { - self.inner.listeners.listeners_object_name() - } + pub fn emit_filter( + &self, + event: &str, + source_window_label: Option<&str>, + payload: S, + filter: F, + ) -> crate::Result<()> + where + S: Serialize + Clone, + F: Fn(&Window) -> bool, + { + assert_event_name_is_valid(event); + + let emit_args = EmitArgs::from(event, source_window_label, payload)?; + + self + .windows_lock() + .values() + .filter(|w| { + w.has_js_listener(None, event) + || w.has_js_listener(source_window_label.map(Into::into), event) + }) + .filter(|w| filter(w)) + .try_for_each(|window| window.emit_js(&emit_args))?; + + self.listeners().emit_filter(&emit_args, Some(filter))?; - pub fn event_emit_function_name(&self) -> &str { - self.inner.listeners.function_name() + Ok(()) } - pub fn unlisten(&self, id: EventId) { - self.listeners().unlisten(id) + pub fn emit( + &self, + event: &str, + source_window_label: Option<&str>, + payload: S, + ) -> crate::Result<()> { + assert_event_name_is_valid(event); + + let emit_args = EmitArgs::from(event, source_window_label, payload)?; + + self + .windows_lock() + .values() + .filter(|w| { + w.has_js_listener(None, event) + || w.has_js_listener(source_window_label.map(Into::into), event) + }) + .try_for_each(|window| window.emit_js(&emit_args))?; + + self.listeners().emit(&emit_args)?; + + Ok(()) } pub fn get_window(&self, label: &str) -> Option> { @@ -1346,10 +1350,25 @@ mod tests { #[cfg(test)] mod test { - use crate::{generate_context, plugin::PluginStore, StateManager, Wry}; + use std::{ + sync::mpsc::{channel, Receiver, Sender}, + time::Duration, + }; + + use crate::{ + generate_context, + plugin::PluginStore, + test::{mock_app, MockRuntime}, + App, Manager, StateManager, Window, WindowBuilder, Wry, + }; use super::WindowManager; + const WINDOW_LISTEN_ID: &str = "Window::listen"; + const WINDOW_LISTEN_GLOBAL_ID: &str = "Window::listen_global"; + const APP_LISTEN_GLOBAL_ID: &str = "App::listen_global"; + const TEST_EVENT_NAME: &str = "event"; + #[test] fn check_get_url() { let context = generate_context!("test/fixture/src-tauri/tauri.conf.json", crate); @@ -1380,4 +1399,164 @@ mod test { #[cfg(dev)] assert_eq!(manager.get_url().to_string(), "http://localhost:4000/"); } + + struct EventSetup { + app: App, + window: Window, + tx: Sender<(&'static str, String)>, + rx: Receiver<(&'static str, String)>, + } + + fn setup_events() -> EventSetup { + let app = mock_app(); + let window = WindowBuilder::new(&app, "main", Default::default()) + .build() + .unwrap(); + + let (tx, rx) = channel(); + + let tx_ = tx.clone(); + window.listen(TEST_EVENT_NAME, move |evt| { + tx_ + .send(( + WINDOW_LISTEN_ID, + serde_json::from_str::(evt.payload()).unwrap(), + )) + .unwrap(); + }); + + let tx_ = tx.clone(); + window.listen_global(TEST_EVENT_NAME, move |evt| { + tx_ + .send(( + WINDOW_LISTEN_GLOBAL_ID, + serde_json::from_str::(evt.payload()).unwrap(), + )) + .unwrap(); + }); + + let tx_ = tx.clone(); + app.listen_global(TEST_EVENT_NAME, move |evt| { + tx_ + .send(( + APP_LISTEN_GLOBAL_ID, + serde_json::from_str::(evt.payload()).unwrap(), + )) + .unwrap(); + }); + + EventSetup { + app, + window, + tx, + rx, + } + } + + fn assert_events(received: &[&str], expected: &[&str]) { + for e in expected { + assert!(received.contains(e), "{e} did not receive global event"); + } + assert_eq!( + received.len(), + expected.len(), + "received {:?} events but expected {:?}", + received, + expected + ); + } + + #[test] + fn app_global_events() { + let EventSetup { + app, + window: _, + tx: _, + rx, + } = setup_events(); + + let mut received = Vec::new(); + let payload = "global-payload"; + app.emit(TEST_EVENT_NAME, payload).unwrap(); + while let Ok((source, p)) = rx.recv_timeout(Duration::from_secs(1)) { + assert_eq!(p, payload); + received.push(source); + } + assert_events( + &received, + &[ + WINDOW_LISTEN_ID, + WINDOW_LISTEN_GLOBAL_ID, + APP_LISTEN_GLOBAL_ID, + ], + ); + } + + #[test] + fn window_global_events() { + let EventSetup { + app: _, + window, + tx: _, + rx, + } = setup_events(); + + let mut received = Vec::new(); + let payload = "global-payload"; + window.emit(TEST_EVENT_NAME, payload).unwrap(); + while let Ok((source, p)) = rx.recv_timeout(Duration::from_secs(1)) { + assert_eq!(p, payload); + received.push(source); + } + assert_events( + &received, + &[ + WINDOW_LISTEN_ID, + WINDOW_LISTEN_GLOBAL_ID, + APP_LISTEN_GLOBAL_ID, + ], + ); + } + + #[test] + fn window_local_events() { + let EventSetup { + app, + window, + tx, + rx, + } = setup_events(); + + let mut received = Vec::new(); + let payload = "global-payload"; + window + .emit_to(window.label(), TEST_EVENT_NAME, payload) + .unwrap(); + while let Ok((source, p)) = rx.recv_timeout(Duration::from_secs(1)) { + assert_eq!(p, payload); + received.push(source); + } + assert_events(&received, &[WINDOW_LISTEN_ID]); + + received.clear(); + let other_window_listen_id = "OtherWindow::listen"; + let other_window = WindowBuilder::new(&app, "other", Default::default()) + .build() + .unwrap(); + other_window.listen(TEST_EVENT_NAME, move |evt| { + tx.send(( + other_window_listen_id, + serde_json::from_str::(evt.payload()).unwrap(), + )) + .unwrap(); + }); + window + .emit_to(other_window.label(), TEST_EVENT_NAME, payload) + .unwrap(); + while let Ok((source, p)) = rx.recv_timeout(Duration::from_secs(1)) { + assert_eq!(p, payload); + received.push(source); + } + assert_events(&received, &[other_window_listen_id]); + } } diff --git a/core/tauri/src/scope/fs.rs b/core/tauri/src/scope/fs.rs index 498c9b73c2ed..c68b2d8ff4c4 100644 --- a/core/tauri/src/scope/fs.rs +++ b/core/tauri/src/scope/fs.rs @@ -180,7 +180,7 @@ impl Scope { self.event_listeners.lock().unwrap().remove(&id); } - fn trigger(&self, event: Event) { + fn emit(&self, event: Event) { let listeners = self.event_listeners.lock().unwrap(); let handlers = listeners.values(); for listener in handlers { @@ -204,7 +204,7 @@ impl Scope { escaped_pattern_with(p, if recursive { "**" } else { "*" }) })?; } - self.trigger(Event::PathAllowed(path.to_path_buf())); + self.emit(Event::PathAllowed(path.to_path_buf())); Ok(()) } @@ -218,7 +218,7 @@ impl Scope { path, escaped_pattern, )?; - self.trigger(Event::PathAllowed(path.to_path_buf())); + self.emit(Event::PathAllowed(path.to_path_buf())); Ok(()) } @@ -237,7 +237,7 @@ impl Scope { escaped_pattern_with(p, if recursive { "**" } else { "*" }) })?; } - self.trigger(Event::PathForbidden(path.to_path_buf())); + self.emit(Event::PathForbidden(path.to_path_buf())); Ok(()) } @@ -251,7 +251,7 @@ impl Scope { path, escaped_pattern, )?; - self.trigger(Event::PathForbidden(path.to_path_buf())); + self.emit(Event::PathForbidden(path.to_path_buf())); Ok(()) } diff --git a/core/tauri/src/window/mod.rs b/core/tauri/src/window/mod.rs index bcc39f0d962c..52802f797582 100644 --- a/core/tauri/src/window/mod.rs +++ b/core/tauri/src/window/mod.rs @@ -15,7 +15,7 @@ use crate::TitleBarStyle; use crate::{ app::{AppHandle, UriSchemeResponder}, command::{CommandArg, CommandItem}, - event::{Event, EventId}, + event::{EmitArgs, Event, EventId}, ipc::{ CallbackFn, Invoke, InvokeBody, InvokeError, InvokeMessage, InvokeResolver, OwnedInvokeResponder, @@ -71,26 +71,6 @@ struct WindowCreatedEvent { label: String, } -pub(crate) struct WindowEmitArgs { - pub event: String, - pub source_window_label: String, - pub payload: String, -} - -impl WindowEmitArgs { - pub fn from( - event: &str, - source_window_label: Option<&str>, - payload: S, - ) -> crate::Result { - Ok(WindowEmitArgs { - event: serde_json::to_string(event)?, - source_window_label: serde_json::to_string(&source_window_label)?, - payload: serde_json::to_string(&payload)?, - }) - } -} - /// Monitor descriptor. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] @@ -976,6 +956,11 @@ impl PartialEq for Window { } impl Manager for Window { + fn emit(&self, event: &str, payload: S) -> crate::Result<()> { + self.manager().emit(event, Some(self.label()), payload)?; + Ok(()) + } + fn emit_to( &self, label: &str, @@ -987,10 +972,14 @@ impl Manager for Window { .emit_filter(event, Some(self.label()), payload, |w| label == w.label()) } - fn emit_all(&self, event: &str, payload: S) -> crate::Result<()> { + fn emit_filter(&self, event: &str, payload: S, filter: F) -> crate::Result<()> + where + S: Serialize + Clone, + F: Fn(&Window) -> bool, + { self .manager() - .emit_filter(event, Some(self.label()), payload, |_| true) + .emit_filter(event, Some(self.label()), payload, filter) } } impl ManagerBase for Window { @@ -2337,6 +2326,14 @@ impl Window { Ok(()) } + pub(crate) fn emit_js(&self, emit_args: &EmitArgs) -> crate::Result<()> { + self.eval(&crate::event::emit_js( + self.manager().listeners().function_name(), + emit_args, + )?)?; + Ok(()) + } + /// Whether this window registered a listener to an event from the given window and event name. pub(crate) fn has_js_listener(&self, window_label: Option, event: &str) -> bool { self @@ -2445,75 +2442,8 @@ impl Window { /// Event system APIs. impl Window { - /// Emits an event to both the JavaScript and the Rust listeners. - /// - /// This API is a combination of [`Self::trigger`] and [`Self::emit`]. - /// - /// # Examples - /// ``` - /// use tauri::Manager; - /// - /// #[tauri::command] - /// fn download(window: tauri::Window) { - /// window.emit_and_trigger("download-started", ()); - /// - /// for i in 1..100 { - /// std::thread::sleep(std::time::Duration::from_millis(150)); - /// // emit a download progress event to all listeners - /// window.emit_and_trigger("download-progress", i); - /// } - /// } - /// ``` - pub fn emit_and_trigger( - &self, - event: &str, - payload: S, - ) -> crate::Result<()> { - self.trigger(event, Some(serde_json::to_string(&payload)?)); - self.emit(event, payload) - } - - pub(crate) fn emit_internal(&self, emit_args: &WindowEmitArgs) -> crate::Result<()> { - self.eval(&format!( - "(function () {{ const fn = window['{}']; fn && fn({{event: {}, windowLabel: {}, payload: {}}}) }})()", - self.manager.event_emit_function_name(), - emit_args.event, - emit_args.source_window_label, - emit_args.payload - ))?; - Ok(()) - } - - /// Emits an event to the JavaScript listeners on the current window or globally. - /// - /// # Examples - /// ``` - /// use tauri::Manager; - /// - /// #[tauri::command] - /// fn download(window: tauri::Window) { - /// for i in 1..100 { - /// std::thread::sleep(std::time::Duration::from_millis(150)); - /// // emit a download progress event to all listeners registed in the webview - /// window.emit("download-progress", i); - /// } - /// } - /// ``` - pub fn emit(&self, event: &str, payload: S) -> crate::Result<()> { - self - .manager - .emit_filter(event, Some(self.label()), payload, |w| { - w.has_js_listener(None, event) || w.has_js_listener(Some(self.label().into()), event) - })?; - Ok(()) - } - /// Listen to an event on this window. /// - /// This listener only receives events that are triggered using the - /// [`trigger`](Window#method.trigger) and [`emit_and_trigger`](Window#method.emit_and_trigger) methods or - /// the `emit` function from the window plugin (`@tauri-apps/api/window` package). - /// /// # Examples /// ``` /// use tauri::Manager; @@ -2532,8 +2462,9 @@ impl Window { where F: Fn(Event) + Send + 'static, { - let label = self.window.label.clone(); - self.manager.listen(event.into(), Some(label), handler) + self + .manager + .listen(event.into(), Some(self.clone()), handler) } /// Unlisten to an event on this window. @@ -2565,7 +2496,7 @@ impl Window { self.manager.unlisten(id) } - /// Listen to an event on this window a single time. + /// Listen to an event on this window only once. /// /// See [`Self::listen`] for more information. pub fn once(&self, event: impl Into, handler: F) @@ -2575,26 +2506,6 @@ impl Window { let label = self.window.label.clone(); self.manager.once(event.into(), Some(label), handler) } - - /// Triggers an event to the Rust listeners on this window or global listeners. - /// - /// # Examples - /// ``` - /// use tauri::Manager; - /// - /// #[tauri::command] - /// fn download(window: tauri::Window) { - /// for i in 1..100 { - /// std::thread::sleep(std::time::Duration::from_millis(150)); - /// // emit a download progress event to all listeners registed on `window` in Rust - /// window.trigger("download-progress", Some(i.to_string())); - /// } - /// } - /// ``` - pub fn trigger(&self, event: &str, data: Option) { - let label = self.window.label.clone(); - self.manager.trigger(event, Some(label), data) - } } /// The [`WindowEffectsConfig`] object builder