Repository: tauri-apps/tauri (language: rust)

Issue:
[bug] TrayIcon mouse events don't work if the icon was created from async command
### Describe the bug

After upgrading my app to tauri v2 I could not make the tray icon behave as it did in v1 at all. It would not react to mouse at all, not even context menu would show up. Debugging showed that events were not firing.

After hours of trying things in a dummy create-tauri-app to isolate the issue I noticed that if the tray icon is created in a synchronous command it works as expected, but if the command is `async` then the icon will be created but it does not react to mouse at all.

This was not an issue in tauri v1.

### Reproduction

Use `npm create tauri-app@latest` and modify the `greet` command like this
```rust
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
#[tauri::command]
async fn greet(name: &str, app: AppHandle) -> String {
    tauri::tray::TrayIconBuilder::with_id("TRAY")
        .icon(app.default_window_icon().unwrap().clone())
        .on_tray_icon_event(|_icon, event| {
            println!("Tray icon event received {:?}", event);
            match event {
                TrayIconEvent::Click {
                    button: MouseButton::Left,
                    button_state: MouseButtonState::Up,
                    ..
                } => {
                    println!("Tray icon clicked");
                }
                _ => {}
            }
        })
        .on_menu_event(|_app, _event| {})
        .build(&app)
        .ok();
    format!("Hello, {}! You've been greeted from Rust!", name)
}
```

Notice that the icon ignores mouse.
But if you drop the `async` then it works as expected.


### Expected behavior

Icon works properly with both sync and async commands.

### Full `tauri info` output

```text
[✘] Environment
    - OS: Windows 10.0.19045 x86_64 (X64)
    ✔ WebView2: 129.0.2792.89
    ✘ Couldn't detect any Visual Studio or VS Build Tools instance with MSVC and SDK components. Download from https://aka.ms/vs/17/release/vs_BuildTools.exe
    ✔ rustc: 1.80.0 (051478957 2024-07-21)
    ✔ cargo: 1.80.0 (376290515 2024-07-16)
    ✔ rustup: 1.27.1 (54dd3d00f 2024-04-24)
    ✔ Rust toolchain: stable-x86_64-pc-windows-msvc (default)
    - node: 16.14.2
    - pnpm: 8.6.2
    - npm: 8.5.0

[-] Packages
    - tauri 🦀: 2.0.6      
    - tauri-build 🦀: 2.0.2
    - wry 🦀: 0.46.3
    - tao 🦀: 0.30.3
    - tauri-cli 🦀: 1.3.1
    - @tauri-apps/api : 2.0.0 (outdated, latest: 2.0.3)
    - @tauri-apps/cli : 2.0.3 (outdated, latest: 2.0.4)

[-] Plugins
    - tauri-plugin-notification 🦀: 2.0.1
    - @tauri-apps/plugin-notification : 2.0.0
    - tauri-plugin-dialog 🦀: 2.0.3
    - @tauri-apps/plugin-dialog : 2.0.1
    - tauri-plugin-fs 🦀: 2.0.3
    - @tauri-apps/plugin-fs : 2.0.1
    - tauri-plugin-cli 🦀: 2.0.1
    - @tauri-apps/plugin-cli : 2.0.0
    - tauri-plugin-shell 🦀: 2.0.2
    - @tauri-apps/plugin-shell : 2.0.1

[-] App
    - build-type: bundle
    - CSP: unset
    - frontendDist: ../dist
    - devUrl: http://localhost:8080/
    - framework: React
    - bundler: Webpack
```


### Stack trace

_No response_

### Additional context

This may be windows specific.


Discussion / hints:

Fix patch:
diff --git a/.changes/tray-async-command.md b/.changes/tray-async-command.md
new file mode 100644
index 000000000000..9e40178190e2
--- /dev/null
+++ b/.changes/tray-async-command.md
@@ -0,0 +1,5 @@
+---
+"tauri": "patch:bug"
+---
+
+Fix tray events not fired for tray icons created inside an async command.
diff --git a/crates/tauri/src/lib.rs b/crates/tauri/src/lib.rs
index ee9f58f9a517..e37df1624d07 100644
--- a/crates/tauri/src/lib.rs
+++ b/crates/tauri/src/lib.rs
@@ -1011,6 +1011,15 @@ pub(crate) mod sealed {
   }
 }
 
+struct UnsafeSend<T>(T);
+unsafe impl<T> Send for UnsafeSend<T> {}
+
+impl<T> UnsafeSend<T> {
+  fn take(self) -> T {
+    self.0
+  }
+}
+
 #[allow(unused)]
 macro_rules! run_main_thread {
   ($handle:ident, $ex:expr) => {{
diff --git a/crates/tauri/src/menu/mod.rs b/crates/tauri/src/menu/mod.rs
index 2797a5543ea2..2ae33d971b89 100644
--- a/crates/tauri/src/menu/mod.rs
+++ b/crates/tauri/src/menu/mod.rs
@@ -75,7 +75,6 @@ macro_rules! gen_wrappers {
         app_handle: $crate::AppHandle<R>,
       }
 
-
       /// # Safety
       ///
       /// We make sure it always runs on the main thread.
@@ -96,11 +95,9 @@ macro_rules! gen_wrappers {
 
       impl<R: Runtime> Drop for $inner<R> {
         fn drop(&mut self) {
-          struct SafeSend<T>(T);
-          unsafe impl<T> Send for SafeSend<T> {}
-
           let inner = self.inner.take();
-          let inner = SafeSend(inner);
+          // SAFETY: inner was created on main thread and is being dropped on main thread
+          let inner = $crate::UnsafeSend(inner);
           let _ = self.app_handle.run_on_main_thread(move || {
             drop(inner);
           });
diff --git a/crates/tauri/src/tray/mod.rs b/crates/tauri/src/tray/mod.rs
index 21623ff2b7d5..5c053f52b66d 100644
--- a/crates/tauri/src/tray/mod.rs
+++ b/crates/tauri/src/tray/mod.rs
@@ -10,6 +10,7 @@ use crate::app::{GlobalMenuEventListener, GlobalTrayIconEventListener};
 use crate::menu::ContextMenu;
 use crate::menu::MenuEvent;
 use crate::resources::Resource;
+use crate::UnsafeSend;
 use crate::{
   image::Image, menu::run_item_main_thread, AppHandle, Manager, PhysicalPosition, Rect, Runtime,
 };
@@ -342,10 +343,24 @@ impl<R: Runtime> TrayIconBuilder<R> {
   /// Builds and adds a new [`TrayIcon`] to the system tray.
   pub fn build<M: Manager<R>>(self, manager: &M) -> crate::Result<TrayIcon<R>> {
     let id = self.id().clone();
-    let inner = self.inner.build()?;
+
+    // SAFETY:
+    // the menu within this builder was created on main thread
+    // and will be accessed on the main thread
+    let unsafe_builder = UnsafeSend(self.inner);
+
+    let (tx, rx) = std::sync::mpsc::channel();
+    let unsafe_tray = manager
+      .app_handle()
+      .run_on_main_thread(move || {
+        // SAFETY: will only be accessed on main thread
+        let _ = tx.send(unsafe_builder.take().build().map(UnsafeSend));
+      })
+      .and_then(|_| rx.recv().map_err(|_| crate::Error::FailedToReceiveMessage))??;
+
     let icon = TrayIcon {
       id,
-      inner,
+      inner: unsafe_tray.take(),
       app_handle: manager.app_handle().clone(),
     };
 


Test patch: