Bläddra i källkod

SystemTray (#3123)

* Added tray icon
Klemen 7 månader sedan
förälder
incheckning
292f757d5d

+ 1 - 0
Cargo.toml

@@ -235,6 +235,7 @@ cocoa = "0.26"
 core-foundation = "0.10.0"
 objc = { version = "0.2.7", features = ["exception"] }
 objc_id = "0.1.1"
+tray-icon = "0.19"
 
 # our release profile should be fast to compile and fast to run
 # when we ship our CI builds, we turn on LTO which improves perf leftover by turning on incremental

+ 2 - 0
packages/desktop/Cargo.toml

@@ -73,6 +73,8 @@ global-hotkey = "0.5.0"
 rfd = { version = "0.14", default-features = false, features = ["xdg-portal", "tokio"] }
 muda = "0.11.3"
 
+[target.'cfg(any(target_os = "windows",target_os = "macos",target_os = "linux"))'.dependencies]
+tray-icon = { workspace = true }
 
 [target.'cfg(target_os = "ios")'.dependencies]
 objc = "0.2.7"

+ 48 - 0
packages/desktop/src/app.rs

@@ -85,6 +85,10 @@ impl App {
         #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
         app.set_menubar_receiver();
 
+        // Wire up the tray icon receiver - this way any component can key into the menubar actions
+        #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
+        app.set_tray_icon_receiver();
+
         // Allow hotreloading to work - but only in debug mode
         #[cfg(all(feature = "devtools", debug_assertions))]
         app.connect_hotreload();
@@ -134,6 +138,29 @@ impl App {
             _ => (),
         }
     }
+    #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
+    pub fn handle_tray_menu_event(&mut self, event: tray_icon::menu::MenuEvent) {
+        _ = event;
+    }
+
+    #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
+    pub fn handle_tray_icon_event(&mut self, event: tray_icon::TrayIconEvent) {
+        if let tray_icon::TrayIconEvent::Click {
+            id: _,
+            position: _,
+            rect: _,
+            button,
+            button_state: _,
+        } = event
+        {
+            if button == tray_icon::MouseButton::Left {
+                for webview in self.webviews.values() {
+                    webview.desktop_context.window.set_visible(true);
+                    webview.desktop_context.window.set_focus();
+                }
+            }
+        }
+    }
 
     #[cfg(all(feature = "devtools", debug_assertions))]
     pub fn connect_hotreload(&self) {
@@ -360,6 +387,27 @@ impl App {
         }));
     }
 
+    #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
+    fn set_tray_icon_receiver(&self) {
+        let receiver = self.shared.proxy.clone();
+
+        // The event loop becomes the menu receiver
+        // This means we don't need to poll the receiver on every tick - we just get the events as they come in
+        // This is a bit more efficient than the previous implementation, but if someone else sets a handler, the
+        // receiver will become inert.
+        tray_icon::TrayIconEvent::set_event_handler(Some(move |t| {
+            // todo: should we unset the event handler when the app shuts down?
+            _ = receiver.send_event(UserWindowEvent::TrayIconEvent(t));
+        }));
+
+        // for whatever reason they had to make it separate
+        let receiver = self.shared.proxy.clone();
+        tray_icon::menu::MenuEvent::set_event_handler(Some(move |t| {
+            // todo: should we unset the event handler when the app shuts down?
+            _ = receiver.send_event(UserWindowEvent::TrayMenuEvent(t));
+        }));
+    }
+
     /// Do our best to preserve state about the window when the event loop is destroyed
     ///
     /// This will attempt to save the window position, size, and monitor into the environment before

+ 34 - 0
packages/desktop/src/hooks.rs

@@ -54,6 +54,40 @@ pub fn use_muda_event_handler(
     })
 }
 
+/// Register an event handler that runs when a tray icon menu event is processed.
+#[cfg_attr(
+    docsrs,
+    doc(cfg(any(target_os = "windows", target_os = "linux", target_os = "macos")))
+)]
+#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
+pub fn use_tray_menu_event_handler(
+    mut handler: impl FnMut(&tray_icon::menu::MenuEvent) + 'static,
+) -> WryEventHandler {
+    use_wry_event_handler(move |event, _| {
+        if let Event::UserEvent(UserWindowEvent::TrayMenuEvent(event)) = event {
+            handler(event);
+        }
+    })
+}
+
+/// Register an event handler that runs when a tray icon event is processed.
+/// This is only for tray icon and not it's menus.
+/// If you want to register tray icon menus handler use `use_tray_menu_event_handler` instead.
+#[cfg_attr(
+    docsrs,
+    doc(cfg(any(target_os = "windows", target_os = "linux", target_os = "macos")))
+)]
+#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
+pub fn use_tray_icon_event_handler(
+    mut handler: impl FnMut(&tray_icon::TrayIconEvent) + 'static,
+) -> WryEventHandler {
+    use_wry_event_handler(move |event, _| {
+        if let Event::UserEvent(UserWindowEvent::TrayIconEvent(event)) = event {
+            handler(event);
+        }
+    })
+}
+
 /// Provide a callback to handle asset loading yourself.
 ///
 /// The callback takes a path as requested by the web view, and it should return `Some(response)`

+ 6 - 0
packages/desktop/src/ipc.rs

@@ -11,6 +11,12 @@ pub enum UserWindowEvent {
     #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
     MudaMenuEvent(muda::MenuEvent),
 
+    #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
+    TrayIconEvent(tray_icon::TrayIconEvent),
+
+    #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
+    TrayMenuEvent(tray_icon::menu::MenuEvent),
+
     /// Poll the virtualdom
     Poll(WindowId),
 

+ 6 - 0
packages/desktop/src/launch.rs

@@ -42,6 +42,12 @@ pub fn launch_virtual_dom_blocking(virtual_dom: VirtualDom, desktop_config: Conf
                 #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
                 UserWindowEvent::MudaMenuEvent(evnt) => app.handle_menu_event(evnt),
 
+                #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
+                UserWindowEvent::TrayMenuEvent(evnt) => app.handle_tray_menu_event(evnt),
+
+                #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
+                UserWindowEvent::TrayIconEvent(evnt) => app.handle_tray_icon_event(evnt),
+
                 #[cfg(all(feature = "devtools", debug_assertions))]
                 UserWindowEvent::HotReloadEvent(msg) => app.handle_hot_reload_msg(msg),
 

+ 4 - 0
packages/desktop/src/lib.rs

@@ -41,6 +41,10 @@ pub use wry;
 #[cfg(not(any(target_os = "ios", target_os = "android")))]
 pub use muda;
 
+// Tray icon
+#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
+pub mod trayicon;
+
 // Public exports
 pub use assets::AssetRequest;
 pub use config::{Config, WindowCloseBehaviour};

+ 68 - 0
packages/desktop/src/trayicon.rs

@@ -0,0 +1,68 @@
+//! tray icon
+
+use dioxus_core::{
+    prelude::{provide_context, try_consume_context},
+    use_hook,
+};
+
+#[cfg(not(any(target_os = "ios", target_os = "android")))]
+pub use tray_icon::*;
+
+/// tray icon menu type trait
+#[cfg(not(any(target_os = "ios", target_os = "android")))]
+pub type DioxusTrayMenu = tray_icon::menu::Menu;
+#[cfg(any(target_os = "ios", target_os = "android"))]
+pub type DioxusTrayMenu = ();
+
+/// tray icon icon type trait
+#[cfg(not(any(target_os = "ios", target_os = "android")))]
+pub type DioxusTrayIcon = tray_icon::Icon;
+#[cfg(any(target_os = "ios", target_os = "android"))]
+pub type DioxusTrayIcon = ();
+
+/// tray icon type trait
+#[cfg(not(any(target_os = "ios", target_os = "android")))]
+pub type DioxusTray = tray_icon::TrayIcon;
+#[cfg(any(target_os = "ios", target_os = "android"))]
+pub type DioxusTray = ();
+
+/// initializes a tray icon
+#[allow(unused)]
+pub fn init_tray_icon(menu: DioxusTrayMenu, icon: Option<DioxusTrayIcon>) -> DioxusTray {
+    #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
+    {
+        let builder = tray_icon::TrayIconBuilder::new()
+            .with_menu(Box::new(menu))
+            .with_menu_on_left_click(false)
+            .with_icon(match icon {
+                Some(value) => value,
+                None => tray_icon::Icon::from_rgba(
+                    include_bytes!("./assets/default_icon.bin").to_vec(),
+                    460,
+                    460,
+                )
+                .expect("image parse failed"),
+            });
+
+        provide_context(builder.build().expect("tray icon builder failed"))
+    }
+}
+
+/// Returns a default tray icon menu
+pub fn default_tray_icon() -> DioxusTrayMenu {
+    #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
+    {
+        use tray_icon::menu::{Menu, PredefinedMenuItem};
+        let tray_menu = Menu::new();
+        tray_menu
+            .append_items(&[&PredefinedMenuItem::quit(None)])
+            .unwrap();
+        tray_menu
+    }
+}
+
+/// Provides a hook to the tray icon
+#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
+pub fn use_tray_icon() -> Option<tray_icon::TrayIcon> {
+    use_hook(try_consume_context)
+}