diff --git a/.changes/config.json b/.changes/config.json index d768478d..0fd1e402 100644 --- a/.changes/config.json +++ b/.changes/config.json @@ -7,7 +7,7 @@ "getPublishedVersion": "cargo search ${ pkg.pkg } --limit 1 | sed -nE 's/^[^\"]*\"//; s/\".*//1p' -", "prepublish": [ "sudo apt-get update", - "sudo apt-get install -y libgtk-3-dev libxdo-dev libayatana-appindicator3-dev" + "sudo apt-get install -y libgtk-3-dev libayatana-appindicator3-dev libdbus-1-dev" ], "publish": [ { diff --git a/.github/workflows/clippy-fmt.yml b/.github/workflows/clippy-fmt.yml index 8065661e..aefa1a55 100644 --- a/.github/workflows/clippy-fmt.yml +++ b/.github/workflows/clippy-fmt.yml @@ -29,7 +29,7 @@ jobs: if: matrix.platform == 'ubuntu-latest' run: | sudo apt-get update - sudo apt-get install -y libgtk-3-dev libxdo-dev libayatana-appindicator3-dev + sudo apt-get install -y libgtk-3-dev libayatana-appindicator3-dev libdbus-1-dev - uses: dtolnay/rust-toolchain@stable with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c541ce0c..d94c09df 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,7 +33,7 @@ jobs: if: matrix.platform == 'ubuntu-latest' run: | sudo apt-get update - sudo apt-get install -y libgtk-3-dev libxdo-dev libayatana-appindicator3-dev + sudo apt-get install -y libgtk-3-dev libayatana-appindicator3-dev libdbus-1-dev - uses: dtolnay/rust-toolchain@1.71 - run: cargo build diff --git a/Cargo.lock b/Cargo.lock index a56417a3..b0360689 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -171,6 +171,15 @@ dependencies = [ "libc", ] +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + [[package]] name = "anyhow" version = "1.0.95" @@ -198,6 +207,12 @@ dependencies = [ "x11rb", ] +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + [[package]] name = "arg_enum_proc_macro" version = "0.3.4" @@ -459,6 +474,17 @@ dependencies = [ "zvariant", ] +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.4.0" @@ -731,6 +757,21 @@ dependencies = [ "libc", ] +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags 1.3.2", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + [[package]] name = "clipboard-win" version = "5.4.0" @@ -953,6 +994,37 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" +[[package]] +name = "dbus" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b" +dependencies = [ + "libc", + "libdbus-sys", + "winapi", +] + +[[package]] +name = "dbus-codegen" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a49da9fdfbe872d4841d56605dc42efa5e6ca3291299b87f44e1cde91a28617c" +dependencies = [ + "clap", + "dbus", + "xml-rs", +] + +[[package]] +name = "dbus-tree" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f456e698ae8e54575e19ddb1f9b7bce2298568524f215496b248eb9498b4f508" +dependencies = [ + "dbus", +] + [[package]] name = "digest" version = "0.10.7" @@ -1900,6 +1972,15 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "hermit-abi" version = "0.4.0" @@ -2225,6 +2306,18 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" +[[package]] +name = "ksni" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4934310bdd016e55725482b8d35ac0c16fd058c1b955d8959aa2d953b918c85b" +dependencies = [ + "dbus", + "dbus-codegen", + "dbus-tree", + "thiserror 1.0.69", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -2267,6 +2360,15 @@ version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +[[package]] +name = "libdbus-sys" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72" +dependencies = [ + "pkg-config", +] + [[package]] name = "libfuzzer-sys" version = "0.4.8" @@ -2308,25 +2410,6 @@ dependencies = [ "redox_syscall 0.5.8", ] -[[package]] -name = "libxdo" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00333b8756a3d28e78def82067a377de7fa61b24909000aeaa2b446a948d14db" -dependencies = [ - "libxdo-sys", -] - -[[package]] -name = "libxdo-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db23b9e7e2b7831bbd8aac0bbeeeb7b68cbebc162b227e7052e8e55829a09212" -dependencies = [ - "libc", - "x11", -] - [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -2447,14 +2530,13 @@ dependencies = [ [[package]] name = "muda" version = "0.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdae9c00e61cc0579bcac625e8ad22104c60548a025bfc972dc83868a28e1484" +source = "git+https://github.com/dfaust/muda?branch=ksni#d513bc26894a2e0cd508908cdffa6226cdedce1b" dependencies = [ + "arc-swap", "crossbeam-channel", "dpi", "gtk", "keyboard-types", - "libxdo", "objc2 0.5.2", "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", @@ -3078,7 +3160,7 @@ checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi", + "hermit-abi 0.4.0", "pin-project-lite", "rustix", "tracing", @@ -3633,6 +3715,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + [[package]] name = "syn" version = "1.0.109" @@ -3756,6 +3844,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -3933,11 +4030,13 @@ dependencies = [ name = "tray-icon" version = "0.19.2" dependencies = [ + "arc-swap", "crossbeam-channel", "dirs", "eframe", "gtk", "image", + "ksni", "libappindicator", "muda", "objc2 0.6.0", @@ -4045,6 +4144,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + [[package]] name = "version-compare" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index acaadcde..4f13382c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,10 +10,10 @@ categories = ["gui"] rust-version = "1.71" [features] -default = ["libxdo"] -libxdo = ["muda/libxdo"] +default = [] serde = ["muda/serde", "dep:serde"] common-controls-v6 = ["muda/common-controls-v6"] +linux-ksni = ["dep:ksni", "dep:arc-swap", "muda/linux-ksni"] [dependencies] muda = { version = "0.15", default-features = false } @@ -34,6 +34,8 @@ features = [ [target."cfg(target_os = \"linux\")".dependencies] libappindicator = "0.9" +arc-swap = { version = "1.7.1", optional = true } +ksni = { version = "0.2.2", optional = true } dirs = "6" [target."cfg(target_os = \"linux\")".dev-dependencies] @@ -88,3 +90,6 @@ tao = "0.31" image = "0.25" eframe = "0.30" serde_json = "1" + +[patch.crates-io] +muda = { git = "https://github.com/dfaust/muda", branch = "ksni" } diff --git a/README.md b/README.md index f131af18..b054ccb2 100644 --- a/README.md +++ b/README.md @@ -4,33 +4,33 @@ tray-icon lets you create tray icons for desktop applications. - Windows - macOS -- Linux (gtk Only) +- Linux ## Platform-specific notes: - On Windows and Linux, an event loop must be running on the thread, on Windows, a win32 event loop and on Linux, a gtk event loop. It doesn't need to be the main thread but you have to create the tray icon on the same thread as the event loop. - On macOS, an event loop must be running on the main thread so you also need to create the tray icon on the main thread. -### Cargo Features +## Cargo Features - `common-controls-v6`: Use `TaskDialogIndirect` API from `ComCtl32.dll` v6 on Windows for showing the predefined `About` menu item dialog. -- `libxdo`: Enables linking to `libxdo` which is used for the predfined `Copy`, `Cut`, `Paste` and `SelectAll` menu item, see https://github.com/tauri-apps/muda#cargo-features - `serde`: Enables de/serializing derives. +- `linux-ksni`: Use ksni and the xdg standard to create and manage tray icons on Linux. (experimental) ## Dependencies (Linux Only) -On Linux, `gtk`, `libxdo` is used to make the predfined `Copy`, `Cut`, `Paste` and `SelectAll` menu items work and `libappindicator` or `libayatnat-appindicator` are used to create the tray icon, so make sure to install them on your system. +On Linux, `gtk`, `libappindicator` or `libayatana-appindicator` are used to create the tray icon. When using the `linux-ksni` feature, `libdbus-1-dev` is needed as well. So make sure to install these packages on your system. #### Arch Linux / Manjaro: ```sh -pacman -S gtk3 xdotool libappindicator-gtk3 #or libayatana-appindicator +pacman -S gtk3 libappindicator-gtk3 # or `libayatana-appindicator` and optionally `dbus` ``` #### Debian / Ubuntu: ```sh -sudo apt install libgtk-3-dev libxdo-dev libappindicator3-dev #or libayatana-appindicator3-dev +sudo apt install libgtk-3-dev libappindicator3-dev # or `libayatana-appindicator3-dev` and optionally `libdbus-1-dev` ``` ## Examples diff --git a/examples/counter.rs b/examples/counter.rs new file mode 100644 index 00000000..4d4089a0 --- /dev/null +++ b/examples/counter.rs @@ -0,0 +1,86 @@ +// Copyright 2022-2022 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#![allow(unused)] + +use tao::event_loop::{ControlFlow, EventLoopBuilder}; +use tray_icon::{ + menu::{ + AboutMetadata, CheckMenuItem, IconMenuItem, Menu, MenuEvent, MenuItem, PredefinedMenuItem, + Submenu, + }, + TrayIconBuilder, TrayIconEvent, +}; + +fn main() { + let path = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/icon.png"); + + let event_loop = EventLoopBuilder::new().build(); + + let mut counter = 0; + let tray_menu = Menu::new(); + + let counter_i = MenuItem::new(format!("Counter: {counter}"), true, None); + tray_menu.append_items(&[&counter_i]); + + let mut tray_icon = None; + + let menu_channel = MenuEvent::receiver(); + let tray_channel = TrayIconEvent::receiver(); + + event_loop.run(move |event, _, control_flow| { + // We add delay of 16 ms (60fps) to event_loop to reduce cpu load. + // Alternatively, you can set ControlFlow::Wait or use TrayIconEvent::set_event_handler, + // see https://github.com/tauri-apps/tray-icon/issues/83#issuecomment-1697773065 + *control_flow = ControlFlow::Poll; + std::thread::sleep(std::time::Duration::from_millis(16)); + + if let tao::event::Event::NewEvents(tao::event::StartCause::Init) = event { + let icon = load_tray_icon(std::path::Path::new(path)); + + // We create the icon once the event loop is actually running + // to prevent issues like https://github.com/tauri-apps/tray-icon/issues/90 + tray_icon = Some( + TrayIconBuilder::new() + .with_menu(Box::new(tray_menu.clone())) + .with_tooltip("tao - awesome windowing lib") + .with_icon(icon) + .build() + .unwrap(), + ); + + // We have to request a redraw here to have the icon actually show up. + // Tao only exposes a redraw method on the Window so we use core-foundation directly. + #[cfg(target_os = "macos")] + unsafe { + use core_foundation::runloop::{CFRunLoopGetMain, CFRunLoopWakeUp}; + + let rl = CFRunLoopGetMain(); + CFRunLoopWakeUp(rl); + } + } + + counter += 1; + counter_i.set_text(format!("Counter: {counter}")); + }) +} + +fn load_icon(path: &std::path::Path) -> (Vec, u32, u32) { + let image = image::open(path) + .expect("Failed to open icon path") + .into_rgba8(); + let (width, height) = image.dimensions(); + let rgba = image.into_raw(); + (rgba, width, height) +} + +fn load_tray_icon(path: &std::path::Path) -> tray_icon::Icon { + let (icon_rgba, icon_width, icon_height) = load_icon(path); + tray_icon::Icon::from_rgba(icon_rgba, icon_width, icon_height).expect("Failed to open icon") +} + +fn load_menu_icon(path: &std::path::Path) -> muda::Icon { + let (icon_rgba, icon_width, icon_height) = load_icon(path); + muda::Icon::from_rgba(icon_rgba, icon_width, icon_height).expect("Failed to open icon") +} diff --git a/examples/tao.rs b/examples/tao.rs index 2fad1546..f6073dc3 100644 --- a/examples/tao.rs +++ b/examples/tao.rs @@ -9,7 +9,10 @@ use tao::{ event_loop::{ControlFlow, EventLoopBuilder}, }; use tray_icon::{ - menu::{AboutMetadata, Menu, MenuEvent, MenuItem, PredefinedMenuItem}, + menu::{ + AboutMetadata, CheckMenuItem, IconMenuItem, Menu, MenuEvent, MenuItem, PredefinedMenuItem, + Submenu, + }, TrayIconBuilder, TrayIconEvent, }; @@ -37,6 +40,16 @@ fn main() { let tray_menu = Menu::new(); + let icon_i = IconMenuItem::new( + "Icon", + true, + Some(load_menu_icon(std::path::Path::new(path))), + None, + ); + let check_i = CheckMenuItem::new("Check", true, false, None); + let subitem_i = MenuItem::new("Subitem", true, None); + let submenu_i = Submenu::new("Submenu", true); + submenu_i.append(&subitem_i); let quit_i = MenuItem::new("Quit", true, None); tray_menu.append_items(&[ &PredefinedMenuItem::about( @@ -48,6 +61,9 @@ fn main() { }), ), &PredefinedMenuItem::separator(), + &icon_i, + &check_i, + &submenu_i, &quit_i, ]); @@ -61,7 +77,7 @@ fn main() { match event { Event::NewEvents(tao::event::StartCause::Init) => { - let icon = load_icon(std::path::Path::new(path)); + let icon = load_tray_icon(std::path::Path::new(path)); // We create the icon once the event loop is actually running // to prevent issues like https://github.com/tauri-apps/tray-icon/issues/90 @@ -103,14 +119,21 @@ fn main() { }) } -fn load_icon(path: &std::path::Path) -> tray_icon::Icon { - let (icon_rgba, icon_width, icon_height) = { - let image = image::open(path) - .expect("Failed to open icon path") - .into_rgba8(); - let (width, height) = image.dimensions(); - let rgba = image.into_raw(); - (rgba, width, height) - }; +fn load_icon(path: &std::path::Path) -> (Vec, u32, u32) { + let image = image::open(path) + .expect("Failed to open icon path") + .into_rgba8(); + let (width, height) = image.dimensions(); + let rgba = image.into_raw(); + (rgba, width, height) +} + +fn load_tray_icon(path: &std::path::Path) -> tray_icon::Icon { + let (icon_rgba, icon_width, icon_height) = load_icon(path); tray_icon::Icon::from_rgba(icon_rgba, icon_width, icon_height).expect("Failed to open icon") } + +fn load_menu_icon(path: &std::path::Path) -> muda::Icon { + let (icon_rgba, icon_width, icon_height) = load_icon(path); + muda::Icon::from_rgba(icon_rgba, icon_width, icon_height).expect("Failed to open icon") +} diff --git a/src/lib.rs b/src/lib.rs index 3dcafeca..3fa4389b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,27 +10,33 @@ //! //! - Windows //! - macOS -//! - Linux (gtk Only) +//! - Linux //! //! # Platform-specific notes: //! //! - On Windows and Linux, an event loop must be running on the thread, on Windows, a win32 event loop and on Linux, a gtk event loop. It doesn't need to be the main thread but you have to create the tray icon on the same thread as the event loop. //! - On macOS, an event loop must be running on the main thread so you also need to create the tray icon on the main thread. You must make sure that the event loop is already running and not just created before creating a TrayIcon to prevent issues with fullscreen apps. In Winit for example the earliest you can create icons is on [`StartCause::Init`](https://docs.rs/winit/latest/winit/event/enum.StartCause.html#variant.Init). //! +//! # Cargo Features +//! +//! - `common-controls-v6`: Use `TaskDialogIndirect` API from `ComCtl32.dll` v6 on Windows for showing the predefined `About` menu item dialog. +//! - `serde`: Enables de/serializing derives. +//! - `linux-ksni`: Use ksni and the xdg standard to create and manage tray icons on Linux. (experimental) +//! //! # Dependencies (Linux Only) //! -//! On Linux, `gtk`, `libxdo` is used to make the predfined `Copy`, `Cut`, `Paste` and `SelectAll` menu items work and `libappindicator` or `libayatnat-appindicator` are used to create the tray icon, so make sure to install them on your system. +//! On Linux, `gtk`, `libappindicator` or `libayatana-appindicator` are used to create the tray icon. When using the `linux-ksni` feature, `libdbus-1-dev` is needed as well. So make sure to install these packages on your system. //! //! #### Arch Linux / Manjaro: //! //! ```sh -//! pacman -S gtk3 xdotool libappindicator-gtk3 #or libayatana-appindicator +//! pacman -S gtk3 libappindicator-gtk3 # or `libayatana-appindicator` and optionally `dbus` //! ``` //! //! #### Debian / Ubuntu: //! //! ```sh -//! sudo apt install libgtk-3-dev libxdo-dev libappindicator3-dev #or libayatana-appindicator3-dev +//! sudo apt install libgtk-3-dev libappindicator3-dev # or `libayatana-appindicator3-dev` and optionally `libdbus-1-dev` //! ``` //! //! # Examples @@ -120,11 +126,10 @@ //! [winit]: https://docs.rs/winit //! [tao]: https://docs.rs/tao -use std::{ - cell::RefCell, - path::{Path, PathBuf}, - rc::Rc, -}; +use std::{cell::RefCell, rc::Rc}; + +#[cfg(all(target_os = "linux", not(feature = "linux-ksni")))] +use std::path::{Path, PathBuf}; use counter::Counter; use crossbeam_channel::{unbounded, Receiver, Sender}; @@ -154,14 +159,14 @@ pub struct TrayIconAttributes { /// /// ## Platform-specific: /// - /// - **Linux:** Unsupported. + /// - **Linux:** Unsupported. Works with feature `linux-ksni`. pub tooltip: Option, /// Tray menu /// /// ## Platform-specific: /// - /// - **Linux**: once a menu is set, it cannot be removed. + /// - **Linux:** Once a menu is set it cannot be removed so `None` has no effect. Works with feature `linux-ksni`. pub menu: Option>, /// Tray icon @@ -170,9 +175,11 @@ pub struct TrayIconAttributes { /// /// - **Linux:** Sometimes the icon won't be visible unless a menu is set. /// Setting an empty [`Menu`](crate::menu::Menu) is enough. + /// Works with feature `linux-ksni`. pub icon: Option, /// Tray icon temp dir path. **Linux only**. + #[cfg(all(target_os = "linux", not(feature = "linux-ksni")))] pub temp_dir_path: Option, /// Use the icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only**. @@ -190,6 +197,7 @@ pub struct TrayIconAttributes { /// updated information. In general, it shouldn't be shown unless a /// user requests it as it can take up a significant amount of space /// on the user's panel. This may not be shown in all visualizations. + /// Works with feature `linux-ksni`. /// - **Windows:** Unsupported. pub title: Option, } @@ -200,6 +208,7 @@ impl Default for TrayIconAttributes { tooltip: None, menu: None, icon: None, + #[cfg(all(target_os = "linux", not(feature = "linux-ksni")))] temp_dir_path: None, icon_is_template: false, menu_on_left_click: true, @@ -236,7 +245,7 @@ impl TrayIconBuilder { /// /// ## Platform-specific: /// - /// - **Linux**: once a menu is set, it cannot be removed or replaced but you can change its content. + /// - **Linux:** Once a menu is set it cannot be removed so `None` has no effect. Works with feature `linux-ksni`. pub fn with_menu(mut self, menu: Box) -> Self { self.attrs.menu = Some(menu); self @@ -247,7 +256,8 @@ impl TrayIconBuilder { /// ## Platform-specific: /// /// - **Linux:** Sometimes the icon won't be visible unless a menu is set. - /// Setting an empty [`Menu`](crate::menu::Menu) is enough. + /// Setting an empty [`Menu`](crate::menu::Menu) is enough. + /// Works with feature `linux-ksni`. pub fn with_icon(mut self, icon: Icon) -> Self { self.attrs.icon = Some(icon); self @@ -257,7 +267,7 @@ impl TrayIconBuilder { /// /// ## Platform-specific: /// - /// - **Linux:** Unsupported. + /// - **Linux:** Unsupported. Works with feature `linux-ksni`. pub fn with_tooltip>(mut self, s: S) -> Self { self.attrs.tooltip = Some(s.as_ref().to_string()); self @@ -267,11 +277,6 @@ impl TrayIconBuilder { /// /// ## Platform-specific /// - /// - **Linux:** The title will not be shown unless there is an icon - /// as well. The title is useful for numerical and other frequently - /// updated information. In general, it shouldn't be shown unless a - /// user requests it as it can take up a significant amount of space - /// on the user's panel. This may not be shown in all visualizations. /// - **Windows:** Unsupported. pub fn with_title>(mut self, title: S) -> Self { self.attrs.title.replace(title.as_ref().to_string()); @@ -280,8 +285,11 @@ impl TrayIconBuilder { /// Set tray icon temp dir path. **Linux only**. /// + /// Not availabe with feature `linux-ksni`. + /// /// On Linux, we need to write the icon to the disk and usually it will /// be `$XDG_RUNTIME_DIR/tray-icon` or `$TEMP/tray-icon`. + #[cfg(all(target_os = "linux", not(feature = "linux-ksni")))] pub fn with_temp_dir_path>(mut self, s: P) -> Self { self.attrs.temp_dir_path = Some(s.as_ref().to_path_buf()); self @@ -322,11 +330,6 @@ pub struct TrayIcon { impl TrayIcon { /// Builds and adds a new tray icon to the system tray. - /// - /// ## Platform-specific: - /// - /// - **Linux:** Sometimes the icon won't be visible unless a menu is set. - /// Setting an empty [`Menu`](crate::menu::Menu) is enough. pub fn new(attrs: TrayIconAttributes) -> Result { let id = TrayIconId(COUNTER.next().to_string()); Ok(Self { @@ -358,6 +361,12 @@ impl TrayIcon { } /// Set new tray icon. If `None` is provided, it will remove the icon. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Sometimes the icon won't be visible unless a menu is set. + /// Setting an empty [`Menu`](crate::menu::Menu) is enough. + /// Works with feature `linux-ksni`. pub fn set_icon(&self, icon: Option) -> Result<()> { self.tray.borrow_mut().set_icon(icon) } @@ -366,7 +375,7 @@ impl TrayIcon { /// /// ## Platform-specific: /// - /// - **Linux**: once a menu is set it cannot be removed so `None` has no effect + /// - **Linux:** Once a menu is set it cannot be removed so `None` has no effect. Works with feature `linux-ksni`. pub fn set_menu(&self, menu: Option>) { self.tray.borrow_mut().set_menu(menu) } @@ -375,7 +384,7 @@ impl TrayIcon { /// /// ## Platform-specific: /// - /// - **Linux:** Unsupported + /// - **Linux:** Unsupported. Works with feature `linux-ksni`. pub fn set_tooltip>(&self, tooltip: Option) -> Result<()> { self.tray.borrow_mut().set_tooltip(tooltip) } @@ -384,30 +393,25 @@ impl TrayIcon { /// /// ## Platform-specific: /// - /// - **Linux:** The title will not be shown unless there is an icon - /// as well. The title is useful for numerical and other frequently - /// updated information. In general, it shouldn't be shown unless a - /// user requests it as it can take up a significant amount of space - /// on the user's panel. This may not be shown in all visualizations. - /// - **Windows:** Unsupported + /// - **Windows:** Unsupported. pub fn set_title>(&self, title: Option) { self.tray.borrow_mut().set_title(title) } - /// Show or hide this tray icon - pub fn set_visible(&self, visible: bool) -> Result<()> { - self.tray.borrow_mut().set_visible(visible) - } - /// Sets the tray icon temp dir path. **Linux only**. /// + /// Not availabe with feature `linux-ksni`. + /// /// On Linux, we need to write the icon to the disk and usually it will /// be `$XDG_RUNTIME_DIR/tray-icon` or `$TEMP/tray-icon`. + #[cfg(all(target_os = "linux", not(feature = "linux-ksni")))] pub fn set_temp_dir_path>(&self, path: Option

) { - #[cfg(target_os = "linux")] self.tray.borrow_mut().set_temp_dir_path(path); - #[cfg(not(target_os = "linux"))] - let _ = path; + } + + /// Show or hide this tray icon + pub fn set_visible(&self, visible: bool) -> Result<()> { + self.tray.borrow_mut().set_visible(visible) } /// Set the current icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only**. @@ -458,8 +462,9 @@ impl TrayIcon { /// /// ## Platform-specific: /// -/// - **Linux**: Unsupported. The event is not emmited even though the icon is shown +/// - **Linux**: The event is not emmited even though the icon is shown /// and will still show a context menu on right click. +/// With feature `linux-ksni`, only `Click` is supported. #[derive(Debug, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(tag = "type"))] diff --git a/src/platform_impl/linux/icon.rs b/src/platform_impl/linux/icon.rs new file mode 100644 index 00000000..9c0b1bb5 --- /dev/null +++ b/src/platform_impl/linux/icon.rs @@ -0,0 +1,64 @@ +// Copyright 2022-2022 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::icon::BadIcon; + +#[derive(Debug, Clone)] +pub struct PlatformIcon { + argb: Vec, + width: i32, + height: i32, +} + +impl PlatformIcon { + pub fn from_rgba(rgba: Vec, width: u32, height: u32) -> Result { + if rgba.len() % 4 != 0 { + return Err(BadIcon::ByteCountNotDivisibleBy4 { + byte_count: rgba.len(), + }); + } + + // convert from rgba to argb + let mut bytes = rgba; + for i in 0..(bytes.len() / 4) { + let j = i * 4; + let a = bytes[j + 3]; + bytes[j + 3] = bytes[j + 2]; + bytes[j + 2] = bytes[j + 1]; + bytes[j + 1] = bytes[j]; + bytes[j] = a; + } + + Ok(Self { + argb: bytes, + width: width as i32, + height: height as i32, + }) + } + + pub fn into_rgba(self) -> (Vec, u32, u32) { + // convert from argb to rgba + let mut bytes = self.argb; + for i in 0..(bytes.len() / 4) { + let j = i * 4; + let a = bytes[j]; + bytes[j] = bytes[j + 1]; + bytes[j + 1] = bytes[j + 2]; + bytes[j + 2] = bytes[j + 3]; + bytes[j + 3] = a; + } + + (bytes, self.width as u32, self.height as u32) + } +} + +impl From for ksni::Icon { + fn from(icon: PlatformIcon) -> Self { + ksni::Icon { + width: icon.width, + height: icon.height, + data: icon.argb, + } + } +} diff --git a/src/platform_impl/linux/menu.rs b/src/platform_impl/linux/menu.rs new file mode 100644 index 00000000..9139a7ad --- /dev/null +++ b/src/platform_impl/linux/menu.rs @@ -0,0 +1,69 @@ +use std::sync::Arc; + +use arc_swap::ArcSwap; +use muda::{AboutDialog, PredefinedMenuItemKind}; + +use super::tray::Tray; + +pub fn muda_to_ksni_menu_item( + item: Arc>, +) -> ksni::menu::MenuItem { + match &**item.load() { + muda::CompatMenuItem::Standard(menu_item) => { + let id = menu_item.id.clone(); + match &menu_item.predefined_menu_item_kind { + Some(PredefinedMenuItemKind::About(Some(metadata))) => { + let about_dialog = AboutDialog::new(metadata.clone()); + ksni::menu::StandardItem { + label: menu_item.label.clone(), + enabled: menu_item.enabled, + icon_data: menu_item.icon.clone().unwrap_or_default(), + activate: Box::new(move |_| { + about_dialog.show(); + }), + ..Default::default() + } + .into() + } + _ => ksni::menu::StandardItem { + label: menu_item.label.clone(), + enabled: menu_item.enabled, + icon_data: menu_item.icon.clone().unwrap_or_default(), + activate: Box::new(move |_| send_menu_event(&id)), + ..Default::default() + } + .into(), + } + } + muda::CompatMenuItem::Checkmark(check_menu_item) => { + let id = check_menu_item.id.clone(); + ksni::menu::CheckmarkItem { + label: check_menu_item.label.clone(), + enabled: check_menu_item.enabled, + checked: check_menu_item.checked, + activate: Box::new(move |_| send_menu_event(&id)), + ..Default::default() + } + .into() + } + muda::CompatMenuItem::SubMenu(submenu) => ksni::menu::SubMenu { + label: submenu.label.clone(), + enabled: submenu.enabled, + submenu: submenu + .submenu + .iter() + .cloned() + .map(muda_to_ksni_menu_item) + .collect(), + ..Default::default() + } + .into(), + muda::CompatMenuItem::Separator => ksni::menu::MenuItem::Separator, + } +} + +fn send_menu_event(id: &str) { + muda::MenuEvent::send(muda::MenuEvent { + id: muda::MenuId(id.to_string()), + }) +} diff --git a/src/platform_impl/linux/mod.rs b/src/platform_impl/linux/mod.rs new file mode 100644 index 00000000..c0691e6e --- /dev/null +++ b/src/platform_impl/linux/mod.rs @@ -0,0 +1,129 @@ +// Copyright 2022-2022 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +mod icon; +mod menu; +mod tray; + +use std::{ + sync::{atomic::AtomicBool, Arc}, + thread, +}; + +pub(crate) use icon::PlatformIcon; +use tray::Tray; + +use crate::{icon::Icon, TrayIconAttributes, TrayIconId}; + +pub struct TrayIcon { + tray_handle: ksni::Handle, + shutdown: Arc, +} + +impl TrayIcon { + pub fn new(id: TrayIconId, attrs: TrayIconAttributes) -> crate::Result { + let icon = attrs.icon.map(|icon| icon.inner.into()); + let title = attrs.title.unwrap_or_default(); + let tooltip = attrs.tooltip.unwrap_or_default(); + + let menu = attrs + .menu + .as_ref() + .map(|menu| menu.compat_items()) + .unwrap_or_default(); + + let shutdown = Arc::new(AtomicBool::new(false)); + + let tray_service = ksni::TrayService::new(Tray::new(id, icon, title, tooltip, menu)); + let tray_handle = tray_service.handle(); + tray_service.spawn(); + + let update_tray_handle = tray_handle.clone(); + let update_shutdown = shutdown.clone(); + thread::spawn(move || { + while muda::recv_menu_update().is_ok() { + if update_shutdown.load(std::sync::atomic::Ordering::Relaxed) { + break; + } + update_tray_handle.update(|_| {}); + } + }); + + Ok(Self { + tray_handle, + shutdown, + }) + } + + pub fn set_icon(&mut self, icon: Option) -> crate::Result<()> { + let icon = icon.map(|icon| icon.inner.into()); + + self.tray_handle.update(|tray| { + tray.set_icon(icon); + }); + + Ok(()) + } + + pub fn set_menu(&mut self, menu: Option>) { + let menu = menu + .as_ref() + .map(|menu| menu.compat_items()) + .unwrap_or_default(); + + self.tray_handle.update(|tray| { + tray.set_menu(menu); + }); + } + + pub fn set_tooltip>(&mut self, tooltip: Option) -> crate::Result<()> { + let tooltip = tooltip + .as_ref() + .map(AsRef::as_ref) + .unwrap_or_default() + .to_string(); + + self.tray_handle.update(|tray| { + tray.set_tooltip(tooltip); + }); + + Ok(()) + } + + pub fn set_title>(&mut self, title: Option) { + let title = title + .as_ref() + .map(AsRef::as_ref) + .unwrap_or_default() + .to_string(); + + self.tray_handle.update(|tray| { + tray.set_title(title); + }); + } + + pub fn set_visible(&mut self, visible: bool) -> crate::Result<()> { + self.tray_handle.update(|tray| { + if visible { + tray.set_status(ksni::Status::Active); + } else { + tray.set_status(ksni::Status::Passive); + } + }); + + Ok(()) + } + + pub fn rect(&self) -> Option { + None + } +} + +impl Drop for TrayIcon { + fn drop(&mut self) { + self.shutdown + .store(true, std::sync::atomic::Ordering::Relaxed); + muda::send_menu_update(); + } +} diff --git a/src/platform_impl/linux/tray.rs b/src/platform_impl/linux/tray.rs new file mode 100644 index 00000000..151106fc --- /dev/null +++ b/src/platform_impl/linux/tray.rs @@ -0,0 +1,112 @@ +use std::sync::Arc; + +use arc_swap::ArcSwap; + +use crate::{MouseButton, MouseButtonState, TrayIconEvent, TrayIconId}; + +use super::menu::muda_to_ksni_menu_item; + +pub struct Tray { + id: TrayIconId, + icon: Vec, + title: String, + status: ksni::Status, + tooltip: String, + menu: Vec>>, +} + +impl Tray { + pub fn new( + id: TrayIconId, + icon: Option, + title: String, + tooltip: String, + menu: Vec>>, + ) -> Self { + Tray { + id, + icon: icon.into_iter().collect(), + title, + status: ksni::Status::Active, + tooltip, + menu, + } + } + + pub fn set_icon(&mut self, icon: Option) { + self.icon = icon.into_iter().collect(); + } + + pub fn set_title(&mut self, title: String) { + self.title = title; + } + + pub fn set_status(&mut self, status: ksni::Status) { + self.status = status; + } + + pub fn set_tooltip(&mut self, tooltip: String) { + self.tooltip = tooltip; + } + + pub fn set_menu(&mut self, menu: Vec>>) { + self.menu = menu; + } +} + +impl ksni::Tray for Tray { + fn id(&self) -> String { + self.id.0.clone() + } + + fn status(&self) -> ksni::Status { + self.status + } + + fn icon_pixmap(&self) -> Vec { + self.icon.clone() + } + + fn title(&self) -> String { + self.title.clone() + } + + fn tool_tip(&self) -> ksni::ToolTip { + ksni::ToolTip { + icon_pixmap: self.icon.clone(), + title: self.title.clone(), + description: self.tooltip.clone(), + ..Default::default() + } + } + + fn menu(&self) -> Vec> { + self.menu + .iter() + .cloned() + .map(muda_to_ksni_menu_item) + .collect() + } + + fn activate(&mut self, x: i32, y: i32) { + let event = TrayIconEvent::Click { + id: self.id.clone(), + position: muda::dpi::PhysicalPosition::new(x as f64, y as f64), + rect: Default::default(), + button: MouseButton::Left, + button_state: MouseButtonState::Up, + }; + TrayIconEvent::send(event); + } + + fn secondary_activate(&mut self, x: i32, y: i32) { + let event = TrayIconEvent::Click { + id: self.id.clone(), + position: muda::dpi::PhysicalPosition::new(x as f64, y as f64), + rect: Default::default(), + button: MouseButton::Middle, + button_state: MouseButtonState::Up, + }; + TrayIconEvent::send(event); + } +} diff --git a/src/platform_impl/mod.rs b/src/platform_impl/mod.rs index 3dabaee8..9b6b907b 100644 --- a/src/platform_impl/mod.rs +++ b/src/platform_impl/mod.rs @@ -5,9 +5,12 @@ #[cfg(target_os = "windows")] #[path = "windows/mod.rs"] mod platform; -#[cfg(target_os = "linux")] +#[cfg(all(target_os = "linux", not(feature = "linux-ksni")))] #[path = "gtk/mod.rs"] mod platform; +#[cfg(all(target_os = "linux", feature = "linux-ksni"))] +#[path = "linux/mod.rs"] +mod platform; #[cfg(target_os = "macos")] #[path = "macos/mod.rs"] mod platform;