Skip to content

Commit

Permalink
fix(core): window-specific event delivery, closes #3302 (#3344)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasfernog committed Feb 6, 2022
1 parent 6330b66 commit 9b34055
Show file tree
Hide file tree
Showing 18 changed files with 311 additions and 168 deletions.
6 changes: 6 additions & 0 deletions .changes/fix-window-specific-event-system.md
@@ -0,0 +1,6 @@
---
"tauri": patch
---

The `tauri::Window#emit` functiow now correctly sends the event to all windows that has a registered listener.
**Breaking change:** `Window#emit_and_trigger` and `Window#emit` now requires the payload to be cloneable.
6 changes: 3 additions & 3 deletions core/tauri-runtime-wry/src/lib.rs
Expand Up @@ -14,7 +14,7 @@ use tauri_runtime::{
webview::{FileDropEvent, FileDropHandler, WebviewIpcHandler, WindowBuilder, WindowBuilderBase},
window::{
dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size},
DetachedWindow, PendingWindow, WindowEvent,
DetachedWindow, JsEventListenerKey, PendingWindow, WindowEvent,
},
ClipboardManager, Dispatch, Error, ExitRequestedEventAction, GlobalShortcutManager, Icon, Result,
RunEvent, RunIteration, Runtime, RuntimeHandle, UserAttentionType,
Expand Down Expand Up @@ -2700,7 +2700,7 @@ fn create_ipc_handler(
context: Context,
label: String,
menu_ids: Arc<Mutex<HashMap<MenuHash, MenuId>>>,
js_event_listeners: Arc<Mutex<HashMap<String, HashSet<u64>>>>,
js_event_listeners: Arc<Mutex<HashMap<JsEventListenerKey, HashSet<u64>>>>,
handler: WebviewIpcHandler<Wry>,
) -> Box<dyn Fn(&Window, String) + 'static> {
Box::new(move |window, request| {
Expand All @@ -2724,7 +2724,7 @@ fn create_file_drop_handler(
context: Context,
label: String,
menu_ids: Arc<Mutex<HashMap<MenuHash, MenuId>>>,
js_event_listeners: Arc<Mutex<HashMap<String, HashSet<u64>>>>,
js_event_listeners: Arc<Mutex<HashMap<JsEventListenerKey, HashSet<u64>>>>,
handler: FileDropHandler<Wry>,
) -> Box<dyn Fn(&Window, WryFileDropEvent) -> bool + 'static> {
Box::new(move |window, event| {
Expand Down
33 changes: 24 additions & 9 deletions core/tauri-runtime/src/window.rs
Expand Up @@ -105,14 +105,20 @@ pub struct PendingWindow<R: Runtime> {
/// Maps runtime id to a string menu id.
pub menu_ids: Arc<Mutex<HashMap<MenuHash, MenuId>>>,

/// A HashMap mapping JS event names with listener ids associated.
pub js_event_listeners: Arc<Mutex<HashMap<String, HashSet<u64>>>>,
/// A HashMap mapping JS event names with associated listener ids.
pub js_event_listeners: Arc<Mutex<HashMap<JsEventListenerKey, HashSet<u64>>>>,
}

fn validate_label(label: &str) {
pub fn is_label_valid(label: &str) -> bool {
label
.chars()
.all(|c| char::is_alphanumeric(c) || c == '-' || c == '/' || c == ':' || c == '_')
}

pub fn assert_label_is_valid(label: &str) {
assert!(
label.chars().all(char::is_alphanumeric),
"Window label must be alphanumeric"
is_label_valid(label),
"Window label must include only alphanumeric characters, `-`, `/`, `:` and `_`."
);
}

Expand All @@ -128,7 +134,7 @@ impl<R: Runtime> PendingWindow<R> {
get_menu_ids(&mut menu_ids, menu);
}
let label = label.into();
validate_label(&label);
assert_label_is_valid(&label);
Self {
window_builder,
webview_attributes,
Expand All @@ -154,7 +160,7 @@ impl<R: Runtime> PendingWindow<R> {
get_menu_ids(&mut menu_ids, menu);
}
let label = label.into();
validate_label(&label);
assert_label_is_valid(&label);
Self {
window_builder,
webview_attributes,
Expand Down Expand Up @@ -192,6 +198,15 @@ impl<R: Runtime> PendingWindow<R> {
}
}

/// Key for a JS event listener.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct JsEventListenerKey {
/// The associated window label.
pub window_label: Option<String>,
/// The event name.
pub event: String,
}

/// A webview window that is not yet managed by Tauri.
#[derive(Debug)]
pub struct DetachedWindow<R: Runtime> {
Expand All @@ -204,8 +219,8 @@ pub struct DetachedWindow<R: Runtime> {
/// Maps runtime id to a string menu id.
pub menu_ids: Arc<Mutex<HashMap<MenuHash, MenuId>>>,

/// A HashMap mapping JS event names with listener ids associated.
pub js_event_listeners: Arc<Mutex<HashMap<String, HashSet<u64>>>>,
/// A HashMap mapping JS event names with associated listener ids.
pub js_event_listeners: Arc<Mutex<HashMap<JsEventListenerKey, HashSet<u64>>>>,
}

impl<R: Runtime> Clone for DetachedWindow<R> {
Expand Down
2 changes: 1 addition & 1 deletion core/tauri/scripts/bundle.js

Large diffs are not rendered by default.

43 changes: 38 additions & 5 deletions core/tauri/src/endpoints/event.rs
Expand Up @@ -7,6 +7,7 @@ use crate::{
api::ipc::CallbackFn,
event::is_event_name_valid,
event::{listen_js, unlisten_js},
runtime::window::is_label_valid,
sealed::ManagerBase,
Manager, Runtime,
};
Expand All @@ -31,12 +32,35 @@ impl<'de> Deserialize<'de> for EventId {
}
}

pub struct WindowLabel(String);

impl<'de> Deserialize<'de> for WindowLabel {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let event_id = String::deserialize(deserializer)?;
if is_label_valid(&event_id) {
Ok(WindowLabel(event_id))
} else {
Err(serde::de::Error::custom(
"Window label must include only alphanumeric characters, `-`, `/`, `:` and `_`.",
))
}
}
}

/// The API descriptor.
#[derive(Deserialize, CommandModule)]
#[serde(tag = "cmd", rename_all = "camelCase")]
pub enum Cmd {
/// Listen to an event.
Listen { event: EventId, handler: CallbackFn },
#[serde(rename_all = "camelCase")]
Listen {
event: EventId,
window_label: Option<WindowLabel>,
handler: CallbackFn,
},
/// Unlisten to an event.
#[serde(rename_all = "camelCase")]
Unlisten { event_id: u64 },
Expand All @@ -45,7 +69,7 @@ pub enum Cmd {
#[serde(rename_all = "camelCase")]
Emit {
event: EventId,
window_label: Option<String>,
window_label: Option<WindowLabel>,
payload: Option<String>,
},
}
Expand All @@ -54,16 +78,25 @@ impl Cmd {
fn listen<R: Runtime>(
context: InvokeContext<R>,
event: EventId,
window_label: Option<WindowLabel>,
handler: CallbackFn,
) -> crate::Result<u64> {
let event_id = rand::random();

let window_label = window_label.map(|l| l.0);

context.window.eval(&listen_js(
context.window.manager().event_listeners_object_name(),
format!("'{}'", event.0),
event_id,
window_label.clone(),
format!("window['_{}']", handler.0),
))?;
context.window.register_js_listener(event.0, event_id);

context
.window
.register_js_listener(window_label, event.0, event_id);

Ok(event_id)
}

Expand All @@ -79,14 +112,14 @@ impl Cmd {
fn emit<R: Runtime>(
context: InvokeContext<R>,
event: EventId,
window_label: Option<String>,
window_label: Option<WindowLabel>,
payload: Option<String>,
) -> crate::Result<()> {
// dispatch the event to Rust listeners
context.window.trigger(&event.0, payload.clone());

if let Some(target) = window_label {
context.window.emit_to(&target, &event.0, payload)?;
context.window.emit_to(&target.0, &event.0, payload)?;
} else {
context.window.emit_all(&event.0, payload)?;
}
Expand Down
19 changes: 17 additions & 2 deletions core/tauri/src/event.rs
Expand Up @@ -18,6 +18,13 @@ pub fn is_event_name_valid(event: &str) -> bool {
.all(|c| c.is_alphanumeric() || c == '-' || c == '/' || c == ':' || c == '_')
}

pub fn assert_event_name_is_valid(event: &str) {
assert!(
is_event_name_valid(event),
"Event name must include only alphanumeric characters, `-`, `/`, `:` and `_`."
);
}

/// Represents an event handler.
#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct EventHandler(Uuid);
Expand Down Expand Up @@ -306,23 +313,31 @@ pub fn listen_js(
listeners_object_name: String,
event: String,
event_id: u64,
window_label: Option<String>,
handler: String,
) -> String {
format!(
"if (window['{listeners}'] === void 0) {{
window['{listeners}'] = Object.create(null)
Object.defineProperty(window, '{listeners}', {{ value: Object.create(null) }});
}}
if (window['{listeners}'][{event}] === void 0) {{
window['{listeners}'][{event}] = []
Object.defineProperty(window['{listeners}'], {event}, {{ value: [] }});
}}
window['{listeners}'][{event}].push({{
id: {event_id},
windowLabel: {window_label},
handler: {handler}
}});
",
listeners = listeners_object_name,
event = event,
event_id = event_id,
window_label = if let Some(l) = window_label {
crate::runtime::window::assert_label_is_valid(&l);
format!("'{}'", l)
} else {
"null".to_owned()
},
handler = handler
)
}
5 changes: 3 additions & 2 deletions core/tauri/src/lib.rs
Expand Up @@ -404,14 +404,14 @@ pub trait Manager<R: Runtime>: sealed::ManagerBase<R> {

/// Emits a event to all windows.
fn emit_all<S: Serialize + Clone>(&self, event: &str, payload: S) -> Result<()> {
self.manager().emit_filter(event, payload, |_| true)
self.manager().emit_filter(event, None, payload, |_| true)
}

/// Emits an event to a window with the specified label.
fn emit_to<S: Serialize + Clone>(&self, label: &str, event: &str, payload: S) -> Result<()> {
self
.manager()
.emit_filter(event, payload, |w| label == w.label())
.emit_filter(event, None, payload, |w| label == w.label())
}

/// Listen to a global event.
Expand Down Expand Up @@ -552,6 +552,7 @@ pub(crate) mod sealed {

self.manager().emit_filter(
"tauri://window-created",
None,
Some(WindowCreatedEvent {
label: window.label().into(),
}),
Expand Down
51 changes: 31 additions & 20 deletions core/tauri/src/manager.rs
Expand Up @@ -30,7 +30,7 @@ use crate::hooks::IsolationJavascript;
use crate::pattern::{format_real_schema, PatternJavascript};
use crate::{
app::{AppHandle, GlobalWindowEvent, GlobalWindowEventListener},
event::{is_event_name_valid, Event, EventHandler, Listeners},
event::{assert_event_name_is_valid, Event, EventHandler, Listeners},
hooks::{InvokeHandler, InvokePayload, InvokeResponder, OnPageLoad, PageLoadPayload},
plugin::PluginStore,
runtime::{
Expand Down Expand Up @@ -850,6 +850,7 @@ impl<R: Runtime> WindowManager<R> {
self.event_listeners_object_name(),
"eventName".into(),
0,
None,
"window['_' + window.__TAURI__.transformCallback(cb) ]".into()
)
),
Expand All @@ -865,18 +866,22 @@ impl<R: Runtime> WindowManager<R> {
fn event_initialization_script(&self) -> String {
return format!(
"
window['{function}'] = function (eventData) {{
const listeners = (window['{listeners}'] && window['{listeners}'][eventData.event]) || []
for (let i = listeners.length - 1; i >= 0; i--) {{
const listener = listeners[i]
eventData.id = listener.id
listener.handler(eventData)
}}
}}
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 || listener.windowLabel === eventData.windowLabel) {{
eventData.id = listener.id
listener.handler(eventData)
}}
}}
}}
}});
",
function = self.inner.listeners.function_name(),
listeners = self.inner.listeners.listeners_object_name()
function = self.event_emit_function_name(),
listeners = self.event_listeners_object_name()
);
}
}
Expand Down Expand Up @@ -1088,17 +1093,23 @@ impl<R: Runtime> WindowManager<R> {
self.windows_lock().remove(label);
}

pub fn emit_filter<S, F>(&self, event: &str, payload: S, filter: F) -> crate::Result<()>
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!(is_event_name_valid(event));
assert_event_name_is_valid(event);
self
.windows_lock()
.values()
.filter(|&w| filter(w))
.try_for_each(|window| window.emit(event, payload.clone()))
.try_for_each(|window| window.emit_internal(event, source_window_label, payload.clone()))
}

pub fn labels(&self) -> HashSet<String> {
Expand All @@ -1118,7 +1129,7 @@ impl<R: Runtime> WindowManager<R> {
}

pub fn trigger(&self, event: &str, window: Option<String>, data: Option<String>) {
assert!(is_event_name_valid(event));
assert_event_name_is_valid(event);
self.inner.listeners.trigger(event, window, data)
}

Expand All @@ -1128,7 +1139,7 @@ impl<R: Runtime> WindowManager<R> {
window: Option<String>,
handler: F,
) -> EventHandler {
assert!(is_event_name_valid(&event));
assert_event_name_is_valid(&event);
self.inner.listeners.listen(event, window, handler)
}

Expand All @@ -1138,7 +1149,7 @@ impl<R: Runtime> WindowManager<R> {
window: Option<String>,
handler: F,
) -> EventHandler {
assert!(is_event_name_valid(&event));
assert_event_name_is_valid(&event);
self.inner.listeners.once(event, window, handler)
}

Expand Down Expand Up @@ -1171,7 +1182,7 @@ fn on_window_event<R: Runtime>(
label: _,
signal_tx,
} => {
if window.has_js_listener(WINDOW_CLOSE_REQUESTED_EVENT) {
if window.has_js_listener(Some(window.label().into()), WINDOW_CLOSE_REQUESTED_EVENT) {
signal_tx.send(true).unwrap();
}
window.emit_and_trigger(WINDOW_CLOSE_REQUESTED_EVENT, ())?;
Expand Down Expand Up @@ -1210,7 +1221,7 @@ fn on_window_event<R: Runtime>(
Ok(())
}

#[derive(Serialize)]
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct ScaleFactorChanged {
scale_factor: f64,
Expand Down

0 comments on commit 9b34055

Please sign in to comment.