#![allow(clippy::uninlined_format_args)]
use std::{
cell::RefCell,
path::{Path, PathBuf},
rc::Rc,
};
use counter::Counter;
use crossbeam_channel::{unbounded, Receiver, Sender};
use once_cell::sync::{Lazy, OnceCell};
mod counter;
mod error;
mod icon;
mod platform_impl;
mod tray_icon_id;
pub use self::error::*;
pub use self::icon::{BadIcon, Icon};
pub use self::tray_icon_id::TrayIconId;
pub mod menu {
pub use muda::*;
}
pub use muda::dpi;
static COUNTER: Counter = Counter::new();
pub struct TrayIconAttributes {
pub tooltip: Option<String>,
pub menu: Option<Box<dyn menu::ContextMenu>>,
pub icon: Option<Icon>,
pub temp_dir_path: Option<PathBuf>,
pub icon_is_template: bool,
pub menu_on_left_click: bool,
pub menu_on_right_click: bool,
pub title: Option<String>,
}
impl Default for TrayIconAttributes {
fn default() -> Self {
Self {
tooltip: None,
menu: None,
icon: None,
temp_dir_path: None,
icon_is_template: false,
menu_on_left_click: true,
menu_on_right_click: true,
title: None,
}
}
}
#[derive(Default)]
pub struct TrayIconBuilder {
id: TrayIconId,
attrs: TrayIconAttributes,
}
impl TrayIconBuilder {
pub fn new() -> Self {
Self {
id: TrayIconId::new_unique(),
attrs: TrayIconAttributes::default(),
}
}
pub fn with_id<I: Into<TrayIconId>>(mut self, id: I) -> Self {
self.id = id.into();
self
}
pub fn with_menu(mut self, menu: Box<dyn menu::ContextMenu>) -> Self {
self.attrs.menu = Some(menu);
self
}
pub fn with_icon(mut self, icon: Icon) -> Self {
self.attrs.icon = Some(icon);
self
}
pub fn with_tooltip<S: AsRef<str>>(mut self, s: S) -> Self {
self.attrs.tooltip = Some(s.as_ref().to_string());
self
}
pub fn with_title<S: AsRef<str>>(mut self, title: S) -> Self {
self.attrs.title.replace(title.as_ref().to_string());
self
}
pub fn with_temp_dir_path<P: AsRef<Path>>(mut self, s: P) -> Self {
self.attrs.temp_dir_path = Some(s.as_ref().to_path_buf());
self
}
pub fn with_icon_as_template(mut self, is_template: bool) -> Self {
self.attrs.icon_is_template = is_template;
self
}
pub fn with_menu_on_left_click(mut self, enable: bool) -> Self {
self.attrs.menu_on_left_click = enable;
self
}
pub fn with_menu_on_right_click(mut self, enable: bool) -> Self {
self.attrs.menu_on_right_click = enable;
self
}
pub fn id(&self) -> &TrayIconId {
&self.id
}
pub fn build(self) -> Result<TrayIcon> {
TrayIcon::with_id(self.id, self.attrs)
}
}
#[derive(Clone)]
pub struct TrayIcon {
id: TrayIconId,
tray: Rc<RefCell<platform_impl::TrayIcon>>,
}
impl TrayIcon {
pub fn new(attrs: TrayIconAttributes) -> Result<Self> {
let id = TrayIconId::new_unique();
Ok(Self {
tray: Rc::new(RefCell::new(platform_impl::TrayIcon::new(
id.clone(),
attrs,
)?)),
id,
})
}
pub fn with_id<I: Into<TrayIconId>>(id: I, attrs: TrayIconAttributes) -> Result<Self> {
let id = id.into();
Ok(Self {
tray: Rc::new(RefCell::new(platform_impl::TrayIcon::new(
id.clone(),
attrs,
)?)),
id,
})
}
pub fn id(&self) -> &TrayIconId {
&self.id
}
pub fn set_icon(&self, icon: Option<Icon>) -> Result<()> {
self.tray.borrow_mut().set_icon(icon)
}
pub fn set_menu(&self, menu: Option<Box<dyn menu::ContextMenu>>) {
self.tray.borrow_mut().set_menu(menu)
}
pub fn set_tooltip<S: AsRef<str>>(&self, tooltip: Option<S>) -> Result<()> {
self.tray.borrow_mut().set_tooltip(tooltip)
}
pub fn set_title<S: AsRef<str>>(&self, title: Option<S>) {
self.tray.borrow_mut().set_title(title)
}
pub fn set_visible(&self, visible: bool) -> Result<()> {
self.tray.borrow_mut().set_visible(visible)
}
pub fn set_temp_dir_path<P: AsRef<Path>>(&self, path: Option<P>) {
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
self.tray.borrow_mut().set_temp_dir_path(path);
#[cfg(not(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
)))]
let _ = path;
}
pub fn set_icon_as_template(&self, is_template: bool) {
#[cfg(target_os = "macos")]
self.tray.borrow_mut().set_icon_as_template(is_template);
#[cfg(not(target_os = "macos"))]
let _ = is_template;
}
pub fn set_icon_with_as_template(&self, icon: Option<Icon>, is_template: bool) -> Result<()> {
#[cfg(target_os = "macos")]
return self
.tray
.borrow_mut()
.set_icon_with_as_template(icon, is_template);
#[cfg(not(target_os = "macos"))]
{
let _ = icon;
let _ = is_template;
Ok(())
}
}
pub fn set_show_menu_on_left_click(&self, enable: bool) {
#[cfg(any(target_os = "macos", target_os = "windows"))]
self.tray.borrow_mut().set_show_menu_on_left_click(enable);
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
let _ = enable;
}
pub fn set_show_menu_on_right_click(&self, enable: bool) {
#[cfg(any(target_os = "macos", target_os = "windows"))]
self.tray.borrow_mut().set_show_menu_on_right_click(enable);
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
let _ = enable;
}
pub fn show_menu(&self) {
#[cfg(any(target_os = "macos", target_os = "windows"))]
self.tray.borrow().show_menu();
}
pub fn rect(&self) -> Option<Rect> {
self.tray.borrow().rect()
}
#[cfg(windows)]
pub fn window_handle(&self) -> windows_sys::Win32::Foundation::HWND {
self.tray.borrow().hwnd()
}
#[cfg(target_os = "macos")]
pub fn ns_status_item(&self) -> Option<objc2::rc::Retained<objc2_app_kit::NSStatusItem>> {
self.tray.borrow().ns_status_item().cloned()
}
#[cfg(all(unix, not(target_os = "macos")))]
pub unsafe fn app_indicator(&self) -> *const libappindicator::AppIndicator {
self.tray.borrow().app_indicator() as *const _
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(tag = "type"))]
#[non_exhaustive]
pub enum TrayIconEvent {
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
Click {
id: TrayIconId,
position: dpi::PhysicalPosition<f64>,
rect: Rect,
button: MouseButton,
button_state: MouseButtonState,
},
DoubleClick {
id: TrayIconId,
position: dpi::PhysicalPosition<f64>,
rect: Rect,
button: MouseButton,
},
Enter {
id: TrayIconId,
position: dpi::PhysicalPosition<f64>,
rect: Rect,
},
Move {
id: TrayIconId,
position: dpi::PhysicalPosition<f64>,
rect: Rect,
},
Leave {
id: TrayIconId,
position: dpi::PhysicalPosition<f64>,
rect: Rect,
},
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Default)]
pub enum MouseButtonState {
#[default]
Up,
Down,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Default)]
pub enum MouseButton {
#[default]
Left,
Right,
Middle,
}
#[derive(Debug, PartialEq, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Rect {
pub size: dpi::PhysicalSize<u32>,
pub position: dpi::PhysicalPosition<f64>,
}
impl Default for Rect {
fn default() -> Self {
Self {
size: dpi::PhysicalSize::new(0, 0),
position: dpi::PhysicalPosition::new(0., 0.),
}
}
}
pub type TrayIconEventReceiver = Receiver<TrayIconEvent>;
type TrayIconEventHandler = Box<dyn Fn(TrayIconEvent) + Send + Sync + 'static>;
static TRAY_CHANNEL: Lazy<(Sender<TrayIconEvent>, TrayIconEventReceiver)> = Lazy::new(unbounded);
static TRAY_EVENT_HANDLER: OnceCell<Option<TrayIconEventHandler>> = OnceCell::new();
impl TrayIconEvent {
pub fn id(&self) -> &TrayIconId {
match self {
TrayIconEvent::Click { id, .. } => id,
TrayIconEvent::DoubleClick { id, .. } => id,
TrayIconEvent::Enter { id, .. } => id,
TrayIconEvent::Move { id, .. } => id,
TrayIconEvent::Leave { id, .. } => id,
}
}
pub fn receiver<'a>() -> &'a TrayIconEventReceiver {
&TRAY_CHANNEL.1
}
pub fn set_event_handler<F: Fn(TrayIconEvent) + Send + Sync + 'static>(f: Option<F>) {
if let Some(f) = f {
let _ = TRAY_EVENT_HANDLER.set(Some(Box::new(f)));
} else {
let _ = TRAY_EVENT_HANDLER.set(None);
}
}
#[allow(unused)]
pub(crate) fn send(event: TrayIconEvent) {
if let Some(handler) = TRAY_EVENT_HANDLER.get_or_init(|| None) {
handler(event);
} else {
let _ = TRAY_CHANNEL.0.send(event);
}
}
}
#[cfg(test)]
mod tests {
#[cfg(feature = "serde")]
#[test]
fn it_serializes() {
use super::*;
let event = TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Down,
id: TrayIconId::new("id"),
position: dpi::PhysicalPosition::default(),
rect: Rect::default(),
};
let value = serde_json::to_value(&event).unwrap();
assert_eq!(
value,
serde_json::json!({
"type": "Click",
"button": "Left",
"buttonState": "Down",
"id": "id",
"position": {
"x": 0.0,
"y": 0.0,
},
"rect": {
"size": {
"width": 0,
"height": 0,
},
"position": {
"x": 0.0,
"y": 0.0,
},
}
})
)
}
}