From 3b9e5990e8ca0d7ab8a3d3967090704130a0ff76 Mon Sep 17 00:00:00 2001 From: Daniel Faust Date: Sun, 20 Oct 2024 17:18:51 +0200 Subject: [PATCH 01/13] Replace libappindicator with ksni --- Cargo.toml | 2 +- README.md | 6 +- examples/tao.rs | 22 ++++- src/lib.rs | 30 +----- src/platform_impl/gtk/icon.rs | 38 ------- src/platform_impl/gtk/mod.rs | 156 ----------------------------- src/platform_impl/linux/icon.rs | 64 ++++++++++++ src/platform_impl/linux/menu.rs | 169 ++++++++++++++++++++++++++++++++ src/platform_impl/linux/mod.rs | 110 +++++++++++++++++++++ src/platform_impl/linux/tray.rs | 104 ++++++++++++++++++++ src/platform_impl/mod.rs | 2 +- 11 files changed, 474 insertions(+), 229 deletions(-) delete mode 100644 src/platform_impl/gtk/icon.rs delete mode 100644 src/platform_impl/gtk/mod.rs create mode 100644 src/platform_impl/linux/icon.rs create mode 100644 src/platform_impl/linux/menu.rs create mode 100644 src/platform_impl/linux/mod.rs create mode 100644 src/platform_impl/linux/tray.rs diff --git a/Cargo.toml b/Cargo.toml index acaadcde..5c6ca146 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ features = [ ] [target."cfg(target_os = \"linux\")".dependencies] -libappindicator = "0.9" +ksni = "0.2.2" dirs = "6" [target."cfg(target_os = \"linux\")".dev-dependencies] diff --git a/README.md b/README.md index f131af18..8111c91b 100644 --- a/README.md +++ b/README.md @@ -19,18 +19,18 @@ tray-icon lets you create tray icons for desktop applications. ## 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`, `libxdo` is used to make the predfined `Copy`, `Cut`, `Paste` and `SelectAll` menu items work, so make sure to install them on your system. #### Arch Linux / Manjaro: ```sh -pacman -S gtk3 xdotool libappindicator-gtk3 #or libayatana-appindicator +pacman -S gtk3 xdotool ``` #### Debian / Ubuntu: ```sh -sudo apt install libgtk-3-dev libxdo-dev libappindicator3-dev #or libayatana-appindicator3-dev +sudo apt install libgtk-3-dev libxdo-dev ``` ## Examples diff --git a/examples/tao.rs b/examples/tao.rs index 2fad1546..41e77c5f 100644 --- a/examples/tao.rs +++ b/examples/tao.rs @@ -9,7 +9,7 @@ use tao::{ event_loop::{ControlFlow, EventLoopBuilder}, }; use tray_icon::{ - menu::{AboutMetadata, Menu, MenuEvent, MenuItem, PredefinedMenuItem}, + menu::{AboutMetadata, Menu, MenuEvent, MenuItem, CheckMenuItem, IconMenuItem, PredefinedMenuItem, Submenu}, TrayIconBuilder, TrayIconEvent, }; @@ -37,6 +37,11 @@ fn main() { let tray_menu = Menu::new(); + let icon_i = IconMenuItem::new("Icon", true, Some(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 +53,9 @@ fn main() { }), ), &PredefinedMenuItem::separator(), + &icon_i, + &check_i, + &submenu_i, &quit_i, ]); @@ -114,3 +122,15 @@ fn load_icon(path: &std::path::Path) -> tray_icon::Icon { }; tray_icon::Icon::from_rgba(icon_rgba, icon_width, icon_height).expect("Failed to open icon") } + +fn menu_icon(path: &std::path::Path) -> muda::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) + }; + 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..a6fd23d7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -120,11 +120,7 @@ //! [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}; use counter::Counter; use crossbeam_channel::{unbounded, Receiver, Sender}; @@ -172,9 +168,6 @@ pub struct TrayIconAttributes { /// Setting an empty [`Menu`](crate::menu::Menu) is enough. pub icon: Option, - /// Tray icon temp dir path. **Linux only**. - pub temp_dir_path: Option, - /// Use the icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only**. pub icon_is_template: bool, @@ -200,7 +193,6 @@ impl Default for TrayIconAttributes { tooltip: None, menu: None, icon: None, - temp_dir_path: None, icon_is_template: false, menu_on_left_click: true, title: None, @@ -278,15 +270,6 @@ impl TrayIconBuilder { self } - /// Set tray icon temp dir path. **Linux only**. - /// - /// 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`. - pub fn with_temp_dir_path>(mut self, s: P) -> Self { - self.attrs.temp_dir_path = Some(s.as_ref().to_path_buf()); - self - } - /// Use the icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only**. pub fn with_icon_as_template(mut self, is_template: bool) -> Self { self.attrs.icon_is_template = is_template; @@ -399,17 +382,6 @@ impl TrayIcon { self.tray.borrow_mut().set_visible(visible) } - /// Sets the tray icon temp dir path. **Linux only**. - /// - /// 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`. - 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; - } - /// Set the current icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only**. pub fn set_icon_as_template(&self, is_template: bool) { #[cfg(target_os = "macos")] diff --git a/src/platform_impl/gtk/icon.rs b/src/platform_impl/gtk/icon.rs deleted file mode 100644 index 9a9c6dcd..00000000 --- a/src/platform_impl/gtk/icon.rs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2022-2022 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -use std::{fs::File, io::BufWriter, path::Path}; - -use crate::icon::BadIcon; - -#[derive(Debug, Clone)] -pub struct PlatformIcon { - rgba: Vec, - width: i32, - height: i32, -} - -impl PlatformIcon { - pub fn from_rgba(rgba: Vec, width: u32, height: u32) -> Result { - Ok(Self { - rgba, - width: width as i32, - height: height as i32, - }) - } - - pub fn write_to_png(&self, path: impl AsRef) -> crate::Result<()> { - let png = File::create(path)?; - let w = &mut BufWriter::new(png); - - let mut encoder = png::Encoder::new(w, self.width as _, self.height as _); - encoder.set_color(png::ColorType::Rgba); - encoder.set_depth(png::BitDepth::Eight); - - let mut writer = encoder.write_header()?; - writer.write_image_data(&self.rgba)?; - - Ok(()) - } -} diff --git a/src/platform_impl/gtk/mod.rs b/src/platform_impl/gtk/mod.rs deleted file mode 100644 index 58e82ebe..00000000 --- a/src/platform_impl/gtk/mod.rs +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright 2022-2022 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -mod icon; -use std::path::{Path, PathBuf}; - -use crate::icon::Icon; -pub(crate) use icon::PlatformIcon; - -use crate::{TrayIconAttributes, TrayIconId, COUNTER}; -use libappindicator::{AppIndicator, AppIndicatorStatus}; - -pub struct TrayIcon { - id: u32, - indicator: AppIndicator, - temp_dir_path: Option, - path: PathBuf, - counter: u32, - menu: Option>, -} - -impl TrayIcon { - pub fn new(_id: TrayIconId, attrs: TrayIconAttributes) -> crate::Result { - let id = COUNTER.next(); - let mut indicator = AppIndicator::new("tray-icon tray app", ""); - indicator.set_status(AppIndicatorStatus::Active); - - let (parent_path, icon_path) = temp_icon_path(attrs.temp_dir_path.as_ref(), id, 0)?; - - if let Some(icon) = attrs.icon { - icon.inner.write_to_png(&icon_path)?; - } - - indicator.set_icon_theme_path(&parent_path.to_string_lossy()); - indicator.set_icon_full(&icon_path.to_string_lossy(), "icon"); - - if let Some(menu) = &attrs.menu { - indicator.set_menu(&mut menu.gtk_context_menu()); - } - - if let Some(title) = attrs.title { - indicator.set_label(title.as_str(), ""); - } - - Ok(Self { - id, - indicator, - path: icon_path, - temp_dir_path: attrs.temp_dir_path, - counter: 0, - menu: attrs.menu, - }) - } - pub fn set_icon(&mut self, icon: Option) -> crate::Result<()> { - let _ = std::fs::remove_file(&self.path); - - self.counter += 1; - - let (parent_path, icon_path) = - temp_icon_path(self.temp_dir_path.as_ref(), self.id, self.counter)?; - - if let Some(icon) = icon { - icon.inner.write_to_png(&icon_path)?; - } - - self.indicator - .set_icon_theme_path(&parent_path.to_string_lossy()); - self.indicator - .set_icon_full(&icon_path.to_string_lossy(), "tray icon"); - self.path = icon_path; - - Ok(()) - } - - pub fn set_menu(&mut self, menu: Option>) { - if let Some(menu) = &menu { - self.indicator.set_menu(&mut menu.gtk_context_menu()); - } - self.menu = menu; - } - - pub fn set_tooltip>(&mut self, _tooltip: Option) -> crate::Result<()> { - Ok(()) - } - - pub fn set_title>(&mut self, title: Option) { - self.indicator - .set_label(title.as_ref().map(|t| t.as_ref()).unwrap_or(""), ""); - } - - pub fn set_visible(&mut self, visible: bool) -> crate::Result<()> { - if visible { - self.indicator.set_status(AppIndicatorStatus::Active); - } else { - self.indicator.set_status(AppIndicatorStatus::Passive); - } - - Ok(()) - } - - pub fn set_temp_dir_path>(&mut self, path: Option

) { - self.temp_dir_path = path.map(|p| p.as_ref().to_path_buf()); - } - - pub fn rect(&self) -> Option { - None - } -} - -impl Drop for TrayIcon { - fn drop(&mut self) { - self.indicator.set_status(AppIndicatorStatus::Passive); - let _ = std::fs::remove_file(&self.path); - } -} - -/// Generates an icon path in one of the following dirs: -/// 1. If `temp_icon_dir` is `Some` use that. -/// 2. `$XDG_RUNTIME_DIR/tray-icon` -/// 3. `/tmp/tray-icon` -fn temp_icon_path( - temp_icon_dir: Option<&PathBuf>, - id: u32, - counter: u32, -) -> std::io::Result<(PathBuf, PathBuf)> { - let parent_path = match temp_icon_dir.as_ref() { - Some(path) => path.to_path_buf(), - None => dirs::runtime_dir() - .unwrap_or_else(std::env::temp_dir) - .join("tray-icon"), - }; - - std::fs::create_dir_all(&parent_path)?; - let icon_path = parent_path.join(format!("tray-icon-{}-{}.png", id, counter)); - Ok((parent_path, icon_path)) -} - -#[test] -fn temp_icon_path_preference_order() { - let runtime_dir = option_env!("XDG_RUNTIME_DIR"); - let override_dir = PathBuf::from("/tmp/tao-tests"); - - let (dir1, _file1) = temp_icon_path(Some(&override_dir), 00, 00).unwrap(); - let (dir2, _file1) = temp_icon_path(None, 00, 00).unwrap(); - std::env::remove_var("XDG_RUNTIME_DIR"); - let (dir3, _file2) = temp_icon_path(None, 00, 00).unwrap(); - - assert_eq!(dir1, override_dir); - if let Some(runtime_dir) = runtime_dir { - std::env::set_var("XDG_RUNTIME_DIR", runtime_dir); - assert_eq!(dir2, PathBuf::from(format!("{}/tray-icon", runtime_dir))); - } - - assert_eq!(dir3, PathBuf::from("/tmp/tray-icon")); -} 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..6e9b7df5 --- /dev/null +++ b/src/platform_impl/linux/menu.rs @@ -0,0 +1,169 @@ +use muda::{AboutDialog, PredefinedMenuItemType}; + +use super::tray::Tray; + +#[derive(Debug, Clone)] +pub struct StandardItem { + id: String, + label: String, + enabled: bool, + icon: Option>, + predefined_menu_item_type: Option, +} + +#[derive(Debug, Clone)] +pub struct CheckmarkItem { + id: String, + label: String, + enabled: bool, + checked: bool, +} + +#[derive(Debug, Clone)] +pub struct SubMenuItem { + label: String, + enabled: bool, + submenu: Vec, +} + +#[allow(clippy::large_enum_variant)] +#[derive(Debug, Clone)] +pub enum MenuItem { + Standard(StandardItem), + Checkmark(CheckmarkItem), + SubMenu(SubMenuItem), + Separator, +} + +impl From for MenuItem { + fn from(item: StandardItem) -> Self { + Self::Standard(item) + } +} + +impl From for MenuItem { + fn from(item: CheckmarkItem) -> Self { + Self::Checkmark(item) + } +} + +impl From for MenuItem { + fn from(item: SubMenuItem) -> Self { + Self::SubMenu(item) + } +} + +impl From for MenuItem { + fn from(item: muda::MenuItemKind) -> Self { + match item { + muda::MenuItemKind::MenuItem(menu_item) => StandardItem { + id: menu_item.id().0.clone(), + label: menu_item.text().replace('&', ""), + enabled: menu_item.is_enabled(), + icon: None, + predefined_menu_item_type: None, + } + .into(), + muda::MenuItemKind::Submenu(submenu) => SubMenuItem { + label: submenu.text().replace('&', ""), + enabled: submenu.is_enabled(), + submenu: submenu.items().into_iter().map(Into::into).collect(), + } + .into(), + muda::MenuItemKind::Predefined(predefined_menu_item) => { + match predefined_menu_item.predefined_item_type() { + Some(PredefinedMenuItemType::Separator) => MenuItem::Separator, + Some(predefined_menu_item_type) => StandardItem { + id: predefined_menu_item.id().0.clone(), + label: predefined_menu_item.text().replace('&', ""), + enabled: true, + icon: None, + predefined_menu_item_type: Some(predefined_menu_item_type), + } + .into(), + _ => StandardItem { + id: predefined_menu_item.id().0.clone(), + label: predefined_menu_item.text().replace('&', ""), + enabled: true, + icon: None, + predefined_menu_item_type: None, + } + .into(), + } + } + muda::MenuItemKind::Check(check_menu_item) => CheckmarkItem { + id: check_menu_item.id().0.clone(), + label: check_menu_item.text().replace('&', ""), + enabled: check_menu_item.is_enabled(), + checked: check_menu_item.is_checked(), + } + .into(), + muda::MenuItemKind::Icon(icon_menu_item) => StandardItem { + id: icon_menu_item.id().0.clone(), + label: icon_menu_item.text().replace('&', ""), + enabled: icon_menu_item.is_enabled(), + icon: icon_menu_item.icon().map(|icon| icon.to_png()), + predefined_menu_item_type: None, + } + .into(), + } + } +} + +impl From for ksni::MenuItem { + fn from(item: MenuItem) -> Self { + match item { + MenuItem::Standard(menu_item) => { + let id = menu_item.id; + match menu_item.predefined_menu_item_type { + Some(PredefinedMenuItemType::About(Some(metadata))) => { + let about_dialog = AboutDialog::new(metadata); + ksni::menu::StandardItem { + label: menu_item.label, + enabled: menu_item.enabled, + icon_data: menu_item.icon.unwrap_or_default(), + activate: Box::new(move |_| { + about_dialog.show(); + }), + ..Default::default() + } + .into() + } + _ => ksni::menu::StandardItem { + label: menu_item.label, + enabled: menu_item.enabled, + icon_data: menu_item.icon.unwrap_or_default(), + activate: Box::new(move |_| send_menu_event(&id)), + ..Default::default() + } + .into(), + } + } + MenuItem::Checkmark(check_menu_item) => { + let id = check_menu_item.id; + ksni::menu::CheckmarkItem { + label: check_menu_item.label, + enabled: check_menu_item.enabled, + checked: check_menu_item.checked, + activate: Box::new(move |_| send_menu_event(&id)), + ..Default::default() + } + .into() + } + MenuItem::SubMenu(submenu) => ksni::menu::SubMenu { + label: submenu.label, + enabled: submenu.enabled, + submenu: submenu.submenu.into_iter().map(Into::into).collect(), + ..Default::default() + } + .into(), + MenuItem::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..8ea61e76 --- /dev/null +++ b/src/platform_impl/linux/mod.rs @@ -0,0 +1,110 @@ +// 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; + +pub(crate) use icon::PlatformIcon; +use tray::Tray; + +use crate::{icon::Icon, TrayIconAttributes, TrayIconId}; + +pub struct TrayIcon { + tray_handle: ksni::Handle, +} + +impl TrayIcon { + pub fn new(id: TrayIconId, attrs: TrayIconAttributes) -> crate::Result { + let icon = attrs.icon.map(|icon| icon.inner.into()); + + let title = title_or_pkg_name(attrs.title.unwrap_or_default()); + + let tooltip = attrs.tooltip.unwrap_or_default(); + + let menu = attrs + .menu + .as_ref() + .map(|menu| menu.items().into_iter().map(Into::into).collect()) + .unwrap_or_default(); + + let tray_service = ksni::TrayService::new(Tray::new(id, icon, title, tooltip, menu)); + let tray_handle = tray_service.handle(); + tray_service.spawn(); + + Ok(Self { tray_handle }) + } + + 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.items().into_iter().map(Into::into).collect()) + .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(); + let title = title_or_pkg_name(title); + + 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 + } +} + +fn title_or_pkg_name(title: String) -> String { + if !title.is_empty() { + title + } else { + env!("CARGO_PKG_NAME").into() + } +} diff --git a/src/platform_impl/linux/tray.rs b/src/platform_impl/linux/tray.rs new file mode 100644 index 00000000..371df275 --- /dev/null +++ b/src/platform_impl/linux/tray.rs @@ -0,0 +1,104 @@ +use crate::{MouseButton, MouseButtonState, TrayIconEvent, TrayIconId}; + +use super::menu::MenuItem; + +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(Into::into).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..ef5e6fdb 100644 --- a/src/platform_impl/mod.rs +++ b/src/platform_impl/mod.rs @@ -6,7 +6,7 @@ #[path = "windows/mod.rs"] mod platform; #[cfg(target_os = "linux")] -#[path = "gtk/mod.rs"] +#[path = "linux/mod.rs"] mod platform; #[cfg(target_os = "macos")] #[path = "macos/mod.rs"] From dce212eb3ac4c15e3b7e56cd2fb52e2a8106d5f7 Mon Sep 17 00:00:00 2001 From: Daniel Faust Date: Sun, 10 Nov 2024 15:22:31 +0100 Subject: [PATCH 02/13] Apply review suggestions --- examples/tao.rs | 45 ++++++++++++++++++--------------- src/platform_impl/linux/menu.rs | 26 ++++++++++--------- src/platform_impl/linux/mod.rs | 13 +--------- 3 files changed, 39 insertions(+), 45 deletions(-) diff --git a/examples/tao.rs b/examples/tao.rs index 41e77c5f..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, CheckMenuItem, IconMenuItem, PredefinedMenuItem, Submenu}, + menu::{ + AboutMetadata, CheckMenuItem, IconMenuItem, Menu, MenuEvent, MenuItem, PredefinedMenuItem, + Submenu, + }, TrayIconBuilder, TrayIconEvent, }; @@ -37,7 +40,12 @@ fn main() { let tray_menu = Menu::new(); - let icon_i = IconMenuItem::new("Icon", true, Some(menu_icon(std::path::Path::new(path))), None); + 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); @@ -69,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 @@ -111,26 +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 menu_icon(path: &std::path::Path) -> muda::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_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/platform_impl/linux/menu.rs b/src/platform_impl/linux/menu.rs index 6e9b7df5..bc66614d 100644 --- a/src/platform_impl/linux/menu.rs +++ b/src/platform_impl/linux/menu.rs @@ -1,4 +1,4 @@ -use muda::{AboutDialog, PredefinedMenuItemType}; +use muda::{AboutDialog, PredefinedMenuItemKind}; use super::tray::Tray; @@ -8,7 +8,7 @@ pub struct StandardItem { label: String, enabled: bool, icon: Option>, - predefined_menu_item_type: Option, + predefined_menu_item_kind: Option, } #[derive(Debug, Clone)] @@ -61,7 +61,7 @@ impl From for MenuItem { label: menu_item.text().replace('&', ""), enabled: menu_item.is_enabled(), icon: None, - predefined_menu_item_type: None, + predefined_menu_item_kind: None, } .into(), muda::MenuItemKind::Submenu(submenu) => SubMenuItem { @@ -71,14 +71,14 @@ impl From for MenuItem { } .into(), muda::MenuItemKind::Predefined(predefined_menu_item) => { - match predefined_menu_item.predefined_item_type() { - Some(PredefinedMenuItemType::Separator) => MenuItem::Separator, - Some(predefined_menu_item_type) => StandardItem { + match predefined_menu_item.predefined_item_kind() { + Some(PredefinedMenuItemKind::Separator) => MenuItem::Separator, + Some(predefined_menu_item_kind) => StandardItem { id: predefined_menu_item.id().0.clone(), label: predefined_menu_item.text().replace('&', ""), enabled: true, icon: None, - predefined_menu_item_type: Some(predefined_menu_item_type), + predefined_menu_item_kind: Some(predefined_menu_item_kind), } .into(), _ => StandardItem { @@ -86,7 +86,7 @@ impl From for MenuItem { label: predefined_menu_item.text().replace('&', ""), enabled: true, icon: None, - predefined_menu_item_type: None, + predefined_menu_item_kind: None, } .into(), } @@ -102,8 +102,10 @@ impl From for MenuItem { id: icon_menu_item.id().0.clone(), label: icon_menu_item.text().replace('&', ""), enabled: icon_menu_item.is_enabled(), - icon: icon_menu_item.icon().map(|icon| icon.to_png()), - predefined_menu_item_type: None, + icon: icon_menu_item + .icon() + .map(|icon| icon.to_pixbuf().save_to_bufferv("png", &[]).unwrap()), + predefined_menu_item_kind: None, } .into(), } @@ -115,8 +117,8 @@ impl From for ksni::MenuItem { match item { MenuItem::Standard(menu_item) => { let id = menu_item.id; - match menu_item.predefined_menu_item_type { - Some(PredefinedMenuItemType::About(Some(metadata))) => { + match menu_item.predefined_menu_item_kind { + Some(PredefinedMenuItemKind::About(Some(metadata))) => { let about_dialog = AboutDialog::new(metadata); ksni::menu::StandardItem { label: menu_item.label, diff --git a/src/platform_impl/linux/mod.rs b/src/platform_impl/linux/mod.rs index 8ea61e76..b3b465ff 100644 --- a/src/platform_impl/linux/mod.rs +++ b/src/platform_impl/linux/mod.rs @@ -18,9 +18,7 @@ pub struct TrayIcon { impl TrayIcon { pub fn new(id: TrayIconId, attrs: TrayIconAttributes) -> crate::Result { let icon = attrs.icon.map(|icon| icon.inner.into()); - - let title = title_or_pkg_name(attrs.title.unwrap_or_default()); - + let title = attrs.title.unwrap_or_default(); let tooltip = attrs.tooltip.unwrap_or_default(); let menu = attrs @@ -77,7 +75,6 @@ impl TrayIcon { .map(AsRef::as_ref) .unwrap_or_default() .to_string(); - let title = title_or_pkg_name(title); self.tray_handle.update(|tray| { tray.set_title(title); @@ -100,11 +97,3 @@ impl TrayIcon { None } } - -fn title_or_pkg_name(title: String) -> String { - if !title.is_empty() { - title - } else { - env!("CARGO_PKG_NAME").into() - } -} From ee3097c37087c289fa64376d21445d6a16405822 Mon Sep 17 00:00:00 2001 From: Daniel Faust Date: Sat, 16 Nov 2024 19:17:31 +0100 Subject: [PATCH 03/13] Add compat menu items and menu update channel --- Cargo.toml | 3 +- examples/counter.rs | 86 ++++++++++++++ src/platform_impl/linux/menu.rs | 194 ++++++++------------------------ src/platform_impl/linux/mod.rs | 13 ++- src/platform_impl/linux/tray.rs | 14 ++- 5 files changed, 154 insertions(+), 156 deletions(-) create mode 100644 examples/counter.rs diff --git a/Cargo.toml b/Cargo.toml index 5c6ca146..8d151629 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ serde = ["muda/serde", "dep:serde"] common-controls-v6 = ["muda/common-controls-v6"] [dependencies] -muda = { version = "0.15", default-features = false } +muda = { version = "0.15", default-features = false, features = ["ksni"] } crossbeam-channel = "0.5" once_cell = "1" thiserror = "2.0" @@ -33,6 +33,7 @@ features = [ ] [target."cfg(target_os = \"linux\")".dependencies] +arc-swap = "1.7.1" ksni = "0.2.2" dirs = "6" 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/src/platform_impl/linux/menu.rs b/src/platform_impl/linux/menu.rs index bc66614d..9139a7ad 100644 --- a/src/platform_impl/linux/menu.rs +++ b/src/platform_impl/linux/menu.rs @@ -1,166 +1,64 @@ +use std::sync::Arc; + +use arc_swap::ArcSwap; use muda::{AboutDialog, PredefinedMenuItemKind}; use super::tray::Tray; -#[derive(Debug, Clone)] -pub struct StandardItem { - id: String, - label: String, - enabled: bool, - icon: Option>, - predefined_menu_item_kind: Option, -} - -#[derive(Debug, Clone)] -pub struct CheckmarkItem { - id: String, - label: String, - enabled: bool, - checked: bool, -} - -#[derive(Debug, Clone)] -pub struct SubMenuItem { - label: String, - enabled: bool, - submenu: Vec, -} - -#[allow(clippy::large_enum_variant)] -#[derive(Debug, Clone)] -pub enum MenuItem { - Standard(StandardItem), - Checkmark(CheckmarkItem), - SubMenu(SubMenuItem), - Separator, -} - -impl From for MenuItem { - fn from(item: StandardItem) -> Self { - Self::Standard(item) - } -} - -impl From for MenuItem { - fn from(item: CheckmarkItem) -> Self { - Self::Checkmark(item) - } -} - -impl From for MenuItem { - fn from(item: SubMenuItem) -> Self { - Self::SubMenu(item) - } -} - -impl From for MenuItem { - fn from(item: muda::MenuItemKind) -> Self { - match item { - muda::MenuItemKind::MenuItem(menu_item) => StandardItem { - id: menu_item.id().0.clone(), - label: menu_item.text().replace('&', ""), - enabled: menu_item.is_enabled(), - icon: None, - predefined_menu_item_kind: None, - } - .into(), - muda::MenuItemKind::Submenu(submenu) => SubMenuItem { - label: submenu.text().replace('&', ""), - enabled: submenu.is_enabled(), - submenu: submenu.items().into_iter().map(Into::into).collect(), - } - .into(), - muda::MenuItemKind::Predefined(predefined_menu_item) => { - match predefined_menu_item.predefined_item_kind() { - Some(PredefinedMenuItemKind::Separator) => MenuItem::Separator, - Some(predefined_menu_item_kind) => StandardItem { - id: predefined_menu_item.id().0.clone(), - label: predefined_menu_item.text().replace('&', ""), - enabled: true, - icon: None, - predefined_menu_item_kind: Some(predefined_menu_item_kind), - } - .into(), - _ => StandardItem { - id: predefined_menu_item.id().0.clone(), - label: predefined_menu_item.text().replace('&', ""), - enabled: true, - icon: None, - predefined_menu_item_kind: None, - } - .into(), - } - } - muda::MenuItemKind::Check(check_menu_item) => CheckmarkItem { - id: check_menu_item.id().0.clone(), - label: check_menu_item.text().replace('&', ""), - enabled: check_menu_item.is_enabled(), - checked: check_menu_item.is_checked(), - } - .into(), - muda::MenuItemKind::Icon(icon_menu_item) => StandardItem { - id: icon_menu_item.id().0.clone(), - label: icon_menu_item.text().replace('&', ""), - enabled: icon_menu_item.is_enabled(), - icon: icon_menu_item - .icon() - .map(|icon| icon.to_pixbuf().save_to_bufferv("png", &[]).unwrap()), - predefined_menu_item_kind: None, - } - .into(), - } - } -} - -impl From for ksni::MenuItem { - fn from(item: MenuItem) -> Self { - match item { - MenuItem::Standard(menu_item) => { - let id = menu_item.id; - match menu_item.predefined_menu_item_kind { - Some(PredefinedMenuItemKind::About(Some(metadata))) => { - let about_dialog = AboutDialog::new(metadata); - ksni::menu::StandardItem { - label: menu_item.label, - enabled: menu_item.enabled, - icon_data: menu_item.icon.unwrap_or_default(), - activate: Box::new(move |_| { - about_dialog.show(); - }), - ..Default::default() - } - .into() - } - _ => ksni::menu::StandardItem { - label: menu_item.label, +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.unwrap_or_default(), - activate: Box::new(move |_| send_menu_event(&id)), + icon_data: menu_item.icon.clone().unwrap_or_default(), + activate: Box::new(move |_| { + about_dialog.show(); + }), ..Default::default() } - .into(), + .into() } - } - MenuItem::Checkmark(check_menu_item) => { - let id = check_menu_item.id; - ksni::menu::CheckmarkItem { - label: check_menu_item.label, - enabled: check_menu_item.enabled, - checked: check_menu_item.checked, + _ => 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() + .into(), } - MenuItem::SubMenu(submenu) => ksni::menu::SubMenu { - label: submenu.label, - enabled: submenu.enabled, - submenu: submenu.submenu.into_iter().map(Into::into).collect(), + } + 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(), - MenuItem::Separator => ksni::menu::MenuItem::Separator, + .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, } } diff --git a/src/platform_impl/linux/mod.rs b/src/platform_impl/linux/mod.rs index b3b465ff..5f7409b7 100644 --- a/src/platform_impl/linux/mod.rs +++ b/src/platform_impl/linux/mod.rs @@ -6,6 +6,8 @@ mod icon; mod menu; mod tray; +use std::thread; + pub(crate) use icon::PlatformIcon; use tray::Tray; @@ -24,13 +26,20 @@ impl TrayIcon { let menu = attrs .menu .as_ref() - .map(|menu| menu.items().into_iter().map(Into::into).collect()) + .map(|menu| menu.compat_items()) .unwrap_or_default(); 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(); + thread::spawn(move || { + while muda::recv_menu_update().is_ok() { + update_tray_handle.update(|_| {}); + } + }); + Ok(Self { tray_handle }) } @@ -47,7 +56,7 @@ impl TrayIcon { pub fn set_menu(&mut self, menu: Option>) { let menu = menu .as_ref() - .map(|menu| menu.items().into_iter().map(Into::into).collect()) + .map(|menu| menu.compat_items()) .unwrap_or_default(); self.tray_handle.update(|tray| { diff --git a/src/platform_impl/linux/tray.rs b/src/platform_impl/linux/tray.rs index 371df275..f3cf20f0 100644 --- a/src/platform_impl/linux/tray.rs +++ b/src/platform_impl/linux/tray.rs @@ -1,6 +1,10 @@ +use std::sync::Arc; + +use arc_swap::ArcSwap; + use crate::{MouseButton, MouseButtonState, TrayIconEvent, TrayIconId}; -use super::menu::MenuItem; +use super::menu::muda_to_ksni_menu_item; pub struct Tray { id: TrayIconId, @@ -8,7 +12,7 @@ pub struct Tray { title: String, status: ksni::Status, tooltip: String, - menu: Vec, + menu: Vec>>, } impl Tray { @@ -17,7 +21,7 @@ impl Tray { icon: Option, title: String, tooltip: String, - menu: Vec, + menu: Vec>>, ) -> Self { Tray { id, @@ -45,7 +49,7 @@ impl Tray { self.tooltip = tooltip; } - pub fn set_menu(&mut self, menu: Vec) { + pub fn set_menu(&mut self, menu: Vec>>) { self.menu = menu; } } @@ -77,7 +81,7 @@ impl ksni::Tray for Tray { } fn menu(&self) -> Vec> { - self.menu.iter().cloned().map(Into::into).collect() + self.menu.iter().cloned().map(muda_to_ksni_menu_item).collect() } fn activate(&mut self, x: i32, y: i32) { From be91405a956b8791929b99e20dffb6efd33fc6c0 Mon Sep 17 00:00:00 2001 From: Daniel Faust Date: Wed, 11 Dec 2024 20:54:21 +0100 Subject: [PATCH 04/13] Shut down ksni menu update thread --- src/platform_impl/linux/mod.rs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/platform_impl/linux/mod.rs b/src/platform_impl/linux/mod.rs index 5f7409b7..58f657c1 100644 --- a/src/platform_impl/linux/mod.rs +++ b/src/platform_impl/linux/mod.rs @@ -6,7 +6,7 @@ mod icon; mod menu; mod tray; -use std::thread; +use std::{ops::ControlFlow, thread}; pub(crate) use icon::PlatformIcon; use tray::Tray; @@ -35,8 +35,15 @@ impl TrayIcon { let update_tray_handle = tray_handle.clone(); thread::spawn(move || { - while muda::recv_menu_update().is_ok() { - update_tray_handle.update(|_| {}); + while let Ok(flow) = muda::recv_menu_update() { + match flow { + ControlFlow::Continue(_) => { + update_tray_handle.update(|_| {}); + } + ControlFlow::Break(_) => { + break; + } + } } }); @@ -106,3 +113,9 @@ impl TrayIcon { None } } + +impl Drop for TrayIcon { + fn drop(&mut self) { + muda::send_menu_shutdown(); + } +} From 8ad32f5ffacc830e19c59866f479ed7e5548ad5f Mon Sep 17 00:00:00 2001 From: Daniel Faust Date: Wed, 11 Dec 2024 20:54:32 +0100 Subject: [PATCH 05/13] Update linux docs --- src/lib.rs | 65 +++++------------------------------------------------- 1 file changed, 5 insertions(+), 60 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index a6fd23d7..f05ade2e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,7 +10,7 @@ //! //! - Windows //! - macOS -//! - Linux (gtk Only) +//! - Linux //! //! # Platform-specific notes: //! @@ -19,18 +19,18 @@ //! //! # 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`, `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. `libdbus-1-dev` is used to communicate with the desktop environment to manage the tray icon. 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 dbus gtk3 xdotool libappindicator-gtk3 #or libayatana-appindicator //! ``` //! //! #### Debian / Ubuntu: //! //! ```sh -//! sudo apt install libgtk-3-dev libxdo-dev libappindicator3-dev #or libayatana-appindicator3-dev +//! sudo apt install libdbus-1-dev libgtk-3-dev libxdo-dev libappindicator3-dev #or libayatana-appindicator3-dev //! ``` //! //! # Examples @@ -147,25 +147,12 @@ static COUNTER: Counter = Counter::new(); /// Attributes to use when creating a tray icon. pub struct TrayIconAttributes { /// Tray icon tooltip - /// - /// ## Platform-specific: - /// - /// - **Linux:** Unsupported. pub tooltip: Option, /// Tray menu - /// - /// ## Platform-specific: - /// - /// - **Linux**: once a menu is set, it cannot be removed. pub menu: Option>, /// Tray 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. pub icon: Option, /// Use the icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only**. @@ -178,11 +165,6 @@ pub struct TrayIconAttributes { /// /// ## 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 title: Option, } @@ -225,31 +207,18 @@ impl TrayIconBuilder { } /// Set the a menu for this tray icon. - /// - /// ## Platform-specific: - /// - /// - **Linux**: once a menu is set, it cannot be removed or replaced but you can change its content. pub fn with_menu(mut self, menu: Box) -> Self { self.attrs.menu = Some(menu); self } /// Set an icon for this tray 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. pub fn with_icon(mut self, icon: Icon) -> Self { self.attrs.icon = Some(icon); self } /// Set a tooltip for this tray icon. - /// - /// ## Platform-specific: - /// - /// - **Linux:** Unsupported. pub fn with_tooltip>(mut self, s: S) -> Self { self.attrs.tooltip = Some(s.as_ref().to_string()); self @@ -259,11 +228,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()); @@ -305,11 +269,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 { @@ -346,19 +305,11 @@ impl TrayIcon { } /// Set new tray menu. - /// - /// ## Platform-specific: - /// - /// - **Linux**: once a menu is set it cannot be removed so `None` has no effect pub fn set_menu(&self, menu: Option>) { self.tray.borrow_mut().set_menu(menu) } /// Sets the tooltip for this tray icon. - /// - /// ## Platform-specific: - /// - /// - **Linux:** Unsupported pub fn set_tooltip>(&self, tooltip: Option) -> Result<()> { self.tray.borrow_mut().set_tooltip(tooltip) } @@ -367,11 +318,6 @@ 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 pub fn set_title>(&self, title: Option) { self.tray.borrow_mut().set_title(title) @@ -430,8 +376,7 @@ impl TrayIcon { /// /// ## Platform-specific: /// -/// - **Linux**: Unsupported. The event is not emmited even though the icon is shown -/// and will still show a context menu on right click. +/// - **Linux**: Only `Click` is supported. #[derive(Debug, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(tag = "type"))] From ddb0ecbe015844ad5822b031ac5cfd6ff8bc3b62 Mon Sep 17 00:00:00 2001 From: Daniel Faust Date: Thu, 12 Dec 2024 19:03:25 +0100 Subject: [PATCH 06/13] Shut down tray menu thread using AtomicBool --- src/platform_impl/linux/mod.rs | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/platform_impl/linux/mod.rs b/src/platform_impl/linux/mod.rs index 58f657c1..c0691e6e 100644 --- a/src/platform_impl/linux/mod.rs +++ b/src/platform_impl/linux/mod.rs @@ -6,7 +6,10 @@ mod icon; mod menu; mod tray; -use std::{ops::ControlFlow, thread}; +use std::{ + sync::{atomic::AtomicBool, Arc}, + thread, +}; pub(crate) use icon::PlatformIcon; use tray::Tray; @@ -15,6 +18,7 @@ use crate::{icon::Icon, TrayIconAttributes, TrayIconId}; pub struct TrayIcon { tray_handle: ksni::Handle, + shutdown: Arc, } impl TrayIcon { @@ -29,25 +33,27 @@ impl TrayIcon { .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 let Ok(flow) = muda::recv_menu_update() { - match flow { - ControlFlow::Continue(_) => { - update_tray_handle.update(|_| {}); - } - ControlFlow::Break(_) => { - break; - } + 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 }) + Ok(Self { + tray_handle, + shutdown, + }) } pub fn set_icon(&mut self, icon: Option) -> crate::Result<()> { @@ -116,6 +122,8 @@ impl TrayIcon { impl Drop for TrayIcon { fn drop(&mut self) { - muda::send_menu_shutdown(); + self.shutdown + .store(true, std::sync::atomic::Ordering::Relaxed); + muda::send_menu_update(); } } From fd95d1c7976582c25e6be1a9118efc072aa513ec Mon Sep 17 00:00:00 2001 From: Daniel Faust Date: Thu, 12 Dec 2024 19:03:49 +0100 Subject: [PATCH 07/13] Remove libxdo feature --- Cargo.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8d151629..7eefc7ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,8 +10,7 @@ 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"] From 35639b48d1601d3a9150c98d6a5070bc2a256453 Mon Sep 17 00:00:00 2001 From: Daniel Faust Date: Mon, 16 Dec 2024 11:58:53 +0100 Subject: [PATCH 08/13] cargo fmt --- src/platform_impl/linux/tray.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/platform_impl/linux/tray.rs b/src/platform_impl/linux/tray.rs index f3cf20f0..151106fc 100644 --- a/src/platform_impl/linux/tray.rs +++ b/src/platform_impl/linux/tray.rs @@ -81,7 +81,11 @@ impl ksni::Tray for Tray { } fn menu(&self) -> Vec> { - self.menu.iter().cloned().map(muda_to_ksni_menu_item).collect() + self.menu + .iter() + .cloned() + .map(muda_to_ksni_menu_item) + .collect() } fn activate(&mut self, x: i32, y: i32) { From 5bdb5db239780f70bb2718fd5f5902e02b79ff71 Mon Sep 17 00:00:00 2001 From: Daniel Faust Date: Tue, 31 Dec 2024 17:33:06 +0100 Subject: [PATCH 09/13] Patch muda dependency --- Cargo.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 7eefc7ad..bcbcb8fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,3 +88,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" } From 3d8748d522f24190767bc1d2a0a96765b32861b1 Mon Sep 17 00:00:00 2001 From: Daniel Faust Date: Wed, 8 Jan 2025 21:21:24 +0100 Subject: [PATCH 10/13] Rename feature ksni to linux-ksni and re-add gtk --- Cargo.toml | 13 ++- README.md | 16 ++-- src/lib.rs | 102 ++++++++++++++++++++-- src/platform_impl/gtk/icon.rs | 38 +++++++++ src/platform_impl/gtk/mod.rs | 156 ++++++++++++++++++++++++++++++++++ src/platform_impl/mod.rs | 5 +- 6 files changed, 315 insertions(+), 15 deletions(-) create mode 100644 src/platform_impl/gtk/icon.rs create mode 100644 src/platform_impl/gtk/mod.rs diff --git a/Cargo.toml b/Cargo.toml index bcbcb8fb..b61976ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,9 +13,10 @@ rust-version = "1.71" default = [] serde = ["muda/serde", "dep:serde"] common-controls-v6 = ["muda/common-controls-v6"] +linux-ksni = ["muda/linux-ksni"] [dependencies] -muda = { version = "0.15", default-features = false, features = ["ksni"] } +muda = { version = "0.15", default-features = false } crossbeam-channel = "0.5" once_cell = "1" thiserror = "2.0" @@ -32,9 +33,14 @@ features = [ ] [target."cfg(target_os = \"linux\")".dependencies] +dirs = "6" + +[target."cfg(all(target_os = \"linux\", not(feature = \"linux-ksni\")))".dependencies] +libappindicator = "0.9" + +[target."cfg(all(target_os = \"linux\", feature = \"linux-ksni\"))".dependencies] arc-swap = "1.7.1" ksni = "0.2.2" -dirs = "6" [target."cfg(target_os = \"linux\")".dev-dependencies] gtk = "0.18" @@ -90,4 +96,5 @@ eframe = "0.30" serde_json = "1" [patch.crates-io] -muda = { git = "https://github.com/dfaust/muda", branch = "ksni" } +# muda = { git = "https://github.com/dfaust/muda", branch = "linux-ksni" } +muda = { path = "../muda" } diff --git a/README.md b/README.md index 8111c91b..5c89d3f4 100644 --- a/README.md +++ b/README.md @@ -4,33 +4,37 @@ 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, so make sure to install them on your system. +On Linux, `gtk` is required. `libappindicator` or `libayatana-appindicator` are used to create the tray icon. Alternatively `libdbus-1-dev` is used to communicate with the desktop environment to manage the tray icon, if the `linux-ksni` feature is enabled. So make sure to install these packages on your system. #### Arch Linux / Manjaro: ```sh -pacman -S gtk3 xdotool +pacman -S gtk3 libappindicator-gtk3 # or libayatana-appindicator +# or +pacman -S gtk3 dbus ``` #### Debian / Ubuntu: ```sh -sudo apt install libgtk-3-dev libxdo-dev +sudo apt install libgtk-3-dev libappindicator3-dev # or libayatana-appindicator3-dev +# or +sudo apt install libgtk-3-dev libdbus-1-dev ``` ## Examples diff --git a/src/lib.rs b/src/lib.rs index f05ade2e..85f58a7a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,20 +17,30 @@ //! - 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. `libdbus-1-dev` is used to communicate with the desktop environment to manage the tray icon. So make sure to install these packages on your system. +//! On Linux, `gtk` is required. `libappindicator` or `libayatana-appindicator` are used to create the tray icon. Alternatively `libdbus-1-dev` is used to communicate with the desktop environment to manage the tray icon, if the `linux-ksni` feature is enabled. So make sure to install these packages on your system. //! //! #### Arch Linux / Manjaro: //! //! ```sh -//! pacman -S dbus gtk3 xdotool libappindicator-gtk3 #or libayatana-appindicator +//! pacman -S gtk3 libappindicator-gtk3 # or libayatana-appindicator +//! # or +//! pacman -S gtk3 dbus //! ``` //! //! #### Debian / Ubuntu: //! //! ```sh -//! sudo apt install libdbus-1-dev libgtk-3-dev libxdo-dev libappindicator3-dev #or libayatana-appindicator3-dev +//! sudo apt install libgtk-3-dev libappindicator3-dev # or libayatana-appindicator3-dev +//! # or +//! sudo apt install libgtk-3-dev libdbus-1-dev //! ``` //! //! # Examples @@ -122,6 +132,9 @@ 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}; use once_cell::sync::{Lazy, OnceCell}; @@ -147,14 +160,32 @@ static COUNTER: Counter = Counter::new(); /// Attributes to use when creating a tray icon. pub struct TrayIconAttributes { /// Tray icon tooltip + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. Works with feature `linux-ksni`. pub tooltip: Option, /// Tray menu + /// + /// ## Platform-specific: + /// + /// - **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 + /// + /// ## 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 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**. pub icon_is_template: bool, @@ -165,6 +196,12 @@ pub struct TrayIconAttributes { /// /// ## 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. + /// Works with feature `linux-ksni`. /// - **Windows:** Unsupported. pub title: Option, } @@ -175,6 +212,8 @@ 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, title: None, @@ -207,18 +246,32 @@ impl TrayIconBuilder { } /// Set the a menu for this tray icon. + /// + /// ## Platform-specific: + /// + /// - **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 } /// Set an icon for this tray 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 with_icon(mut self, icon: Icon) -> Self { self.attrs.icon = Some(icon); self } /// Set a tooltip for this tray icon. + /// + /// ## Platform-specific: + /// + /// - **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 @@ -234,6 +287,18 @@ impl TrayIconBuilder { self } + /// 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 + } + /// Use the icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only**. pub fn with_icon_as_template(mut self, is_template: bool) -> Self { self.attrs.icon_is_template = is_template; @@ -300,16 +365,30 @@ 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) } /// Set new tray menu. + /// + /// ## Platform-specific: + /// + /// - **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) } /// Sets the tooltip for this tray icon. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. Works with feature `linux-ksni`. pub fn set_tooltip>(&self, tooltip: Option) -> Result<()> { self.tray.borrow_mut().set_tooltip(tooltip) } @@ -318,11 +397,22 @@ impl TrayIcon { /// /// ## Platform-specific: /// - /// - **Windows:** Unsupported + /// - **Windows:** Unsupported. pub fn set_title>(&self, title: Option) { self.tray.borrow_mut().set_title(title) } + /// 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

) { + self.tray.borrow_mut().set_temp_dir_path(path); + } + /// Show or hide this tray icon pub fn set_visible(&self, visible: bool) -> Result<()> { self.tray.borrow_mut().set_visible(visible) @@ -376,7 +466,9 @@ impl TrayIcon { /// /// ## Platform-specific: /// -/// - **Linux**: Only `Click` is supported. +/// - **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/gtk/icon.rs b/src/platform_impl/gtk/icon.rs new file mode 100644 index 00000000..9a9c6dcd --- /dev/null +++ b/src/platform_impl/gtk/icon.rs @@ -0,0 +1,38 @@ +// Copyright 2022-2022 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{fs::File, io::BufWriter, path::Path}; + +use crate::icon::BadIcon; + +#[derive(Debug, Clone)] +pub struct PlatformIcon { + rgba: Vec, + width: i32, + height: i32, +} + +impl PlatformIcon { + pub fn from_rgba(rgba: Vec, width: u32, height: u32) -> Result { + Ok(Self { + rgba, + width: width as i32, + height: height as i32, + }) + } + + pub fn write_to_png(&self, path: impl AsRef) -> crate::Result<()> { + let png = File::create(path)?; + let w = &mut BufWriter::new(png); + + let mut encoder = png::Encoder::new(w, self.width as _, self.height as _); + encoder.set_color(png::ColorType::Rgba); + encoder.set_depth(png::BitDepth::Eight); + + let mut writer = encoder.write_header()?; + writer.write_image_data(&self.rgba)?; + + Ok(()) + } +} diff --git a/src/platform_impl/gtk/mod.rs b/src/platform_impl/gtk/mod.rs new file mode 100644 index 00000000..58e82ebe --- /dev/null +++ b/src/platform_impl/gtk/mod.rs @@ -0,0 +1,156 @@ +// Copyright 2022-2022 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +mod icon; +use std::path::{Path, PathBuf}; + +use crate::icon::Icon; +pub(crate) use icon::PlatformIcon; + +use crate::{TrayIconAttributes, TrayIconId, COUNTER}; +use libappindicator::{AppIndicator, AppIndicatorStatus}; + +pub struct TrayIcon { + id: u32, + indicator: AppIndicator, + temp_dir_path: Option, + path: PathBuf, + counter: u32, + menu: Option>, +} + +impl TrayIcon { + pub fn new(_id: TrayIconId, attrs: TrayIconAttributes) -> crate::Result { + let id = COUNTER.next(); + let mut indicator = AppIndicator::new("tray-icon tray app", ""); + indicator.set_status(AppIndicatorStatus::Active); + + let (parent_path, icon_path) = temp_icon_path(attrs.temp_dir_path.as_ref(), id, 0)?; + + if let Some(icon) = attrs.icon { + icon.inner.write_to_png(&icon_path)?; + } + + indicator.set_icon_theme_path(&parent_path.to_string_lossy()); + indicator.set_icon_full(&icon_path.to_string_lossy(), "icon"); + + if let Some(menu) = &attrs.menu { + indicator.set_menu(&mut menu.gtk_context_menu()); + } + + if let Some(title) = attrs.title { + indicator.set_label(title.as_str(), ""); + } + + Ok(Self { + id, + indicator, + path: icon_path, + temp_dir_path: attrs.temp_dir_path, + counter: 0, + menu: attrs.menu, + }) + } + pub fn set_icon(&mut self, icon: Option) -> crate::Result<()> { + let _ = std::fs::remove_file(&self.path); + + self.counter += 1; + + let (parent_path, icon_path) = + temp_icon_path(self.temp_dir_path.as_ref(), self.id, self.counter)?; + + if let Some(icon) = icon { + icon.inner.write_to_png(&icon_path)?; + } + + self.indicator + .set_icon_theme_path(&parent_path.to_string_lossy()); + self.indicator + .set_icon_full(&icon_path.to_string_lossy(), "tray icon"); + self.path = icon_path; + + Ok(()) + } + + pub fn set_menu(&mut self, menu: Option>) { + if let Some(menu) = &menu { + self.indicator.set_menu(&mut menu.gtk_context_menu()); + } + self.menu = menu; + } + + pub fn set_tooltip>(&mut self, _tooltip: Option) -> crate::Result<()> { + Ok(()) + } + + pub fn set_title>(&mut self, title: Option) { + self.indicator + .set_label(title.as_ref().map(|t| t.as_ref()).unwrap_or(""), ""); + } + + pub fn set_visible(&mut self, visible: bool) -> crate::Result<()> { + if visible { + self.indicator.set_status(AppIndicatorStatus::Active); + } else { + self.indicator.set_status(AppIndicatorStatus::Passive); + } + + Ok(()) + } + + pub fn set_temp_dir_path>(&mut self, path: Option

) { + self.temp_dir_path = path.map(|p| p.as_ref().to_path_buf()); + } + + pub fn rect(&self) -> Option { + None + } +} + +impl Drop for TrayIcon { + fn drop(&mut self) { + self.indicator.set_status(AppIndicatorStatus::Passive); + let _ = std::fs::remove_file(&self.path); + } +} + +/// Generates an icon path in one of the following dirs: +/// 1. If `temp_icon_dir` is `Some` use that. +/// 2. `$XDG_RUNTIME_DIR/tray-icon` +/// 3. `/tmp/tray-icon` +fn temp_icon_path( + temp_icon_dir: Option<&PathBuf>, + id: u32, + counter: u32, +) -> std::io::Result<(PathBuf, PathBuf)> { + let parent_path = match temp_icon_dir.as_ref() { + Some(path) => path.to_path_buf(), + None => dirs::runtime_dir() + .unwrap_or_else(std::env::temp_dir) + .join("tray-icon"), + }; + + std::fs::create_dir_all(&parent_path)?; + let icon_path = parent_path.join(format!("tray-icon-{}-{}.png", id, counter)); + Ok((parent_path, icon_path)) +} + +#[test] +fn temp_icon_path_preference_order() { + let runtime_dir = option_env!("XDG_RUNTIME_DIR"); + let override_dir = PathBuf::from("/tmp/tao-tests"); + + let (dir1, _file1) = temp_icon_path(Some(&override_dir), 00, 00).unwrap(); + let (dir2, _file1) = temp_icon_path(None, 00, 00).unwrap(); + std::env::remove_var("XDG_RUNTIME_DIR"); + let (dir3, _file2) = temp_icon_path(None, 00, 00).unwrap(); + + assert_eq!(dir1, override_dir); + if let Some(runtime_dir) = runtime_dir { + std::env::set_var("XDG_RUNTIME_DIR", runtime_dir); + assert_eq!(dir2, PathBuf::from(format!("{}/tray-icon", runtime_dir))); + } + + assert_eq!(dir3, PathBuf::from("/tmp/tray-icon")); +} diff --git a/src/platform_impl/mod.rs b/src/platform_impl/mod.rs index ef5e6fdb..9b6b907b 100644 --- a/src/platform_impl/mod.rs +++ b/src/platform_impl/mod.rs @@ -5,7 +5,10 @@ #[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")] From c6e3a48076db998998e120ddc6b30302a5baa209 Mon Sep 17 00:00:00 2001 From: Daniel Faust Date: Wed, 8 Jan 2025 21:22:27 +0100 Subject: [PATCH 11/13] Remove libxdo-dev and add libdbus-1-dev to github workflow --- .changes/config.json | 2 +- .github/workflows/clippy-fmt.yml | 2 +- .github/workflows/test.yml | 2 +- Cargo.toml | 3 +-- 4 files changed, 4 insertions(+), 5 deletions(-) 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.toml b/Cargo.toml index b61976ff..7c4e6323 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -96,5 +96,4 @@ eframe = "0.30" serde_json = "1" [patch.crates-io] -# muda = { git = "https://github.com/dfaust/muda", branch = "linux-ksni" } -muda = { path = "../muda" } +muda = { git = "https://github.com/dfaust/muda", branch = "ksni" } From 9644c48c305c2b5307ba41327c4e37facec80597 Mon Sep 17 00:00:00 2001 From: Daniel Faust Date: Wed, 8 Jan 2025 22:06:35 +0100 Subject: [PATCH 12/13] Update docs --- Cargo.toml | 12 ++++-------- README.md | 10 +++------- src/lib.rs | 18 +++++++----------- 3 files changed, 14 insertions(+), 26 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7c4e6323..4f13382c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ rust-version = "1.71" default = [] serde = ["muda/serde", "dep:serde"] common-controls-v6 = ["muda/common-controls-v6"] -linux-ksni = ["muda/linux-ksni"] +linux-ksni = ["dep:ksni", "dep:arc-swap", "muda/linux-ksni"] [dependencies] muda = { version = "0.15", default-features = false } @@ -33,14 +33,10 @@ features = [ ] [target."cfg(target_os = \"linux\")".dependencies] -dirs = "6" - -[target."cfg(all(target_os = \"linux\", not(feature = \"linux-ksni\")))".dependencies] libappindicator = "0.9" - -[target."cfg(all(target_os = \"linux\", feature = \"linux-ksni\"))".dependencies] -arc-swap = "1.7.1" -ksni = "0.2.2" +arc-swap = { version = "1.7.1", optional = true } +ksni = { version = "0.2.2", optional = true } +dirs = "6" [target."cfg(target_os = \"linux\")".dev-dependencies] gtk = "0.18" diff --git a/README.md b/README.md index 5c89d3f4..b054ccb2 100644 --- a/README.md +++ b/README.md @@ -19,22 +19,18 @@ tray-icon lets you create tray icons for desktop applications. ## Dependencies (Linux Only) -On Linux, `gtk` is required. `libappindicator` or `libayatana-appindicator` are used to create the tray icon. Alternatively `libdbus-1-dev` is used to communicate with the desktop environment to manage the tray icon, if the `linux-ksni` feature is enabled. So make sure to install these packages 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 libappindicator-gtk3 # or libayatana-appindicator -# or -pacman -S gtk3 dbus +pacman -S gtk3 libappindicator-gtk3 # or `libayatana-appindicator` and optionally `dbus` ``` #### Debian / Ubuntu: ```sh -sudo apt install libgtk-3-dev libappindicator3-dev # or libayatana-appindicator3-dev -# or -sudo apt install libgtk-3-dev libdbus-1-dev +sudo apt install libgtk-3-dev libappindicator3-dev # or `libayatana-appindicator3-dev` and optionally `libdbus-1-dev` ``` ## Examples diff --git a/src/lib.rs b/src/lib.rs index 85f58a7a..3fa4389b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,29 +18,25 @@ //! - 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` is required. `libappindicator` or `libayatana-appindicator` are used to create the tray icon. Alternatively `libdbus-1-dev` is used to communicate with the desktop environment to manage the tray icon, if the `linux-ksni` feature is enabled. So make sure to install these packages 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 libappindicator-gtk3 # or libayatana-appindicator -//! # or -//! pacman -S gtk3 dbus +//! pacman -S gtk3 libappindicator-gtk3 # or `libayatana-appindicator` and optionally `dbus` //! ``` //! //! #### Debian / Ubuntu: //! //! ```sh -//! sudo apt install libgtk-3-dev libappindicator3-dev # or libayatana-appindicator3-dev -//! # or -//! sudo apt install libgtk-3-dev libdbus-1-dev +//! sudo apt install libgtk-3-dev libappindicator3-dev # or `libayatana-appindicator3-dev` and optionally `libdbus-1-dev` //! ``` //! //! # Examples @@ -288,7 +284,7 @@ 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 @@ -403,7 +399,7 @@ impl TrayIcon { } /// 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 From 1a04b3f51111b34b95d26958314dfe7523c0b897 Mon Sep 17 00:00:00 2001 From: Daniel Faust Date: Sat, 8 Feb 2025 14:44:16 +0100 Subject: [PATCH 13/13] cargo fmt --- Cargo.lock | 151 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 128 insertions(+), 23 deletions(-) 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"