refactor: enhance event system rust apis (#7996)

Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
This commit is contained in:
Amr Bashir 2023-10-23 21:10:01 +03:00 committed by GitHub
parent 198abe3c2c
commit 93c8a77b34
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 526 additions and 352 deletions

View File

@ -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`

View File

@ -246,7 +246,7 @@ impl<R: Runtime> GlobalWindowEvent<R> {
&self.event
}
/// The window that the menu belongs to.
/// The window that the event belongs to.
pub fn window(&self) -> &Window<R> {
&self.window
}

View File

@ -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<R: Runtime> {
Unlisten(EventId),
Listen(EventId, String, Handler),
Trigger(String, Option<String>, Option<String>),
Listen(EventId, String, Handler<R>),
Emit(EmitArgs),
}
/// Stored in [`Listeners`] to be called upon when the event that stored it is triggered.
struct Handler {
window: Option<String>,
struct Handler<R: Runtime> {
window: Option<Window<R>>,
callback: Box<dyn Fn(Event) + Send>,
}
/// Holds event handlers and pending event handlers, along with the salts associating them.
struct InnerListeners {
handlers: Mutex<HashMap<String, HashMap<EventId, Handler>>>,
pending: Mutex<Vec<Pending>>,
struct InnerListeners<R: Runtime> {
handlers: Mutex<HashMap<String, HashMap<EventId, Handler<R>>>>,
pending: Mutex<Vec<Pending<R>>>,
function_name: &'static str,
listeners_object_name: &'static str,
next_event_id: Arc<AtomicU32>,
}
/// A self-contained event manager.
pub struct Listeners {
inner: Arc<InnerListeners>,
pub struct Listeners<R: Runtime> {
inner: Arc<InnerListeners<R>>,
}
impl Default for Listeners {
impl<R: Runtime> Default for Listeners<R> {
fn default() -> Self {
Self {
inner: Arc::new(InnerListeners {
@ -55,7 +57,7 @@ impl Default for Listeners {
}
}
impl Clone for Listeners {
impl<R: Runtime> Clone for Listeners<R> {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
@ -63,7 +65,7 @@ impl Clone for Listeners {
}
}
impl Listeners {
impl<R: Runtime> Listeners<R> {
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<R>) {
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<R>) {
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<F: Fn(Event) + Send + 'static>(
&self,
event: String,
window: Option<String>,
window: Option<Window<R>>,
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<F: FnOnce(Event) + Send + 'static>(
&self,
event: String,
window: Option<String>,
window: Option<Window<R>>,
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<String>, payload: Option<String>) {
/// Emits the given event with its payload based on a filter.
pub(crate) fn emit_filter<F>(&self, emit_args: &EmitArgs, filter: Option<F>) -> crate::Result<()>
where
F: Fn(&Window<R>) -> 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::<Vec<_>>()
} else {
handlers.iter().collect::<Vec<_>>()
};
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<R>) -> 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<MockRuntime> = 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<MockRuntime> = 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<MockRuntime> = 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();

View File

@ -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<S: Serialize>(
event: &str,
source_window_label: Option<&str>,
payload: S,
) -> crate::Result<Self> {
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<String>,
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<String> {
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)
}}
}}
}}
}});
"
)
}

View File

@ -37,6 +37,12 @@ impl<'de> Deserialize<'de> for EventName {
pub struct WindowLabel(String);
impl AsRef<str> for WindowLabel {
fn as_ref(&self) -> &str {
&self.0
}
}
impl<'de> Deserialize<'de> for WindowLabel {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
@ -75,25 +81,10 @@ pub fn emit<R: Runtime>(
window_label: Option<WindowLabel>,
payload: Option<JsonValue>,
) -> 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)
}
}

View File

@ -546,67 +546,7 @@ pub trait Manager<R: Runtime>: sealed::ManagerBase<R> {
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<S: Serialize + Clone>(&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<S, F>(&self, event: &str, payload: S, filter: F) -> Result<()>
where
S: Serialize + Clone,
F: Fn(&Window<R>) -> 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<S: Serialize + Clone>(&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<R: Runtime>: sealed::ManagerBase<R> {
/// #[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,40 +574,6 @@ pub trait Manager<R: Runtime>: sealed::ManagerBase<R> {
self.manager().listen(event.into(), None, handler)
}
/// Listen to a global event only once.
///
/// See [`Self::listen_global`] for more information.
fn once_global<F>(&self, event: impl Into<String>, handler: F)
where
F: FnOnce(Event) + Send + 'static,
{
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`].
///
/// A global event does not have a source or target window attached.
///
/// # 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 all listeners registed in Rust
/// app.trigger_global("download-progress", Some(i.to_string()));
/// }
/// }
/// ```
fn trigger_global(&self, event: &str, data: Option<String>) {
self.manager().trigger(event, None, data)
}
/// Remove an event listener.
///
/// # Examples
@ -696,6 +602,82 @@ pub trait Manager<R: Runtime>: sealed::ManagerBase<R> {
self.manager().unlisten(id)
}
/// Listen to a global event only once.
///
/// See [`Self::listen_global`] for more information.
fn once_global<F>(&self, event: impl Into<String>, handler: F)
where
F: FnOnce(Event) + Send + 'static,
{
self.manager().once(event.into(), None, handler)
}
/// 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<S: Serialize + Clone>(&self, event: &str, payload: S) -> Result<()> {
self.manager().emit(event, None, payload)
}
/// Emits an event to the window with the specified label.
///
/// If using [`Window`] to emit the event, it will be used as the source.
///
/// # 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<S: Serialize + Clone>(&self, label: &str, event: &str, payload: S) -> Result<()> {
self
.manager()
.emit_filter(event, None, payload, |w| label == w.label())
}
/// 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::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 emit_filter<S, F>(&self, event: &str, payload: S, filter: F) -> Result<()>
where
S: Serialize + Clone,
F: Fn(&Window<R>) -> bool,
{
self.manager().emit_filter(event, None, payload, filter)
}
/// Fetch a single window from the manager.
fn get_window(&self, label: &str) -> Option<Window<R>> {
self.manager().get_window(label)

View File

@ -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<R: Runtime> {
pub(crate) windows: Mutex<HashMap<String, Window<R>>>,
pub(crate) plugins: Mutex<PluginStore<R>>,
listeners: Listeners,
listeners: Listeners<R>,
pub(crate) state: Arc<StateManager>,
/// The JS message handler.
@ -847,7 +847,10 @@ impl<R: Runtime> WindowManager<R> {
}
.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<R: Runtime> WindowManager<R> {
.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<R> {
&self.inner.listeners
}
}
@ -1146,26 +1127,6 @@ impl<R: Runtime> WindowManager<R> {
self.windows_lock().remove(label);
}
pub fn emit_filter<S, F>(
&self,
event: &str,
source_window_label: Option<&str>,
payload: S,
filter: F,
) -> crate::Result<()>
where
S: Serialize + Clone,
F: Fn(&Window<R>) -> 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<S: Into<String>>(&self, script: S) -> crate::Result<()> {
let script = script.into();
self
@ -1186,21 +1147,20 @@ impl<R: Runtime> WindowManager<R> {
&self.inner.package_info
}
pub fn trigger(&self, event: &str, window: Option<String>, data: Option<String>) {
assert_event_name_is_valid(event);
self.listeners().trigger(event, window, data)
}
pub fn listen<F: Fn(Event) + Send + 'static>(
&self,
event: String,
window: Option<String>,
window: Option<Window<R>>,
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<F: FnOnce(Event) + Send + 'static>(
&self,
event: String,
@ -1208,19 +1168,63 @@ impl<R: Runtime> WindowManager<R> {
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<S, F>(
&self,
event: &str,
source_window_label: Option<&str>,
payload: S,
filter: F,
) -> crate::Result<()>
where
S: Serialize + Clone,
F: Fn(&Window<R>) -> 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))?;
Ok(())
}
pub fn event_emit_function_name(&self) -> &str {
self.inner.listeners.function_name()
}
pub fn emit<S: Serialize + Clone>(
&self,
event: &str,
source_window_label: Option<&str>,
payload: S,
) -> crate::Result<()> {
assert_event_name_is_valid(event);
pub fn unlisten(&self, id: EventId) {
self.listeners().unlisten(id)
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<Window<R>> {
@ -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<MockRuntime>,
window: Window<MockRuntime>,
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::<String>(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::<String>(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::<String>(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::<String>(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]);
}
}

View File

@ -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(())
}

View File

@ -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<S: Serialize>(
event: &str,
source_window_label: Option<&str>,
payload: S,
) -> crate::Result<Self> {
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<R: Runtime> PartialEq for Window<R> {
}
impl<R: Runtime> Manager<R> for Window<R> {
fn emit<S: Serialize + Clone>(&self, event: &str, payload: S) -> crate::Result<()> {
self.manager().emit(event, Some(self.label()), payload)?;
Ok(())
}
fn emit_to<S: Serialize + Clone>(
&self,
label: &str,
@ -987,10 +972,14 @@ impl<R: Runtime> Manager<R> for Window<R> {
.emit_filter(event, Some(self.label()), payload, |w| label == w.label())
}
fn emit_all<S: Serialize + Clone>(&self, event: &str, payload: S) -> crate::Result<()> {
fn emit_filter<S, F>(&self, event: &str, payload: S, filter: F) -> crate::Result<()>
where
S: Serialize + Clone,
F: Fn(&Window<R>) -> bool,
{
self
.manager()
.emit_filter(event, Some(self.label()), payload, |_| true)
.emit_filter(event, Some(self.label()), payload, filter)
}
}
impl<R: Runtime> ManagerBase<R> for Window<R> {
@ -2337,6 +2326,14 @@ impl<R: Runtime> Window<R> {
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<String>, event: &str) -> bool {
self
@ -2445,75 +2442,8 @@ impl<R: Runtime> Window<R> {
/// Event system APIs.
impl<R: Runtime> Window<R> {
/// 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<S: Serialize + Clone>(
&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<S: Serialize + Clone>(&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<R: Runtime> Window<R> {
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<R: Runtime> Window<R> {
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<F>(&self, event: impl Into<String>, handler: F)
@ -2575,26 +2506,6 @@ impl<R: Runtime> Window<R> {
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<String>) {
let label = self.window.label.clone();
self.manager.trigger(event, Some(label), data)
}
}
/// The [`WindowEffectsConfig`] object builder