use std::{
convert::Infallible,
path::{Path, PathBuf},
str::FromStr,
sync::{
atomic::{AtomicU32, Ordering},
Arc, Mutex,
},
};
use crossbeam_channel::{unbounded, Receiver, Sender};
use once_cell::sync::{Lazy, OnceCell};
use crate::{error::Result, icon::Icon, TrayIconImpl};
static COUNTER: AtomicU32 = AtomicU32::new(1);
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TrayIconId(pub String);
impl TrayIconId {
pub fn new<S: AsRef<str>>(id: S) -> Self {
Self(id.as_ref().to_string())
}
pub(crate) fn new_unique() -> Self {
Self(format!("{}-{}", std::process::id(), COUNTER.fetch_add(1, Ordering::Relaxed)))
}
}
impl AsRef<str> for TrayIconId {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
impl<T: ToString> From<T> for TrayIconId {
fn from(value: T) -> Self {
Self::new(value.to_string())
}
}
impl FromStr for TrayIconId {
type Err = Infallible;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
Ok(Self::new(s))
}
}
impl PartialEq<&str> for TrayIconId {
fn eq(&self, other: &&str) -> bool {
self.0 == *other
}
}
impl PartialEq<&str> for &TrayIconId {
fn eq(&self, other: &&str) -> bool {
self.0 == *other
}
}
impl PartialEq<String> for TrayIconId {
fn eq(&self, other: &String) -> bool {
self.0 == *other
}
}
impl PartialEq<String> for &TrayIconId {
fn eq(&self, other: &String) -> bool {
self.0 == *other
}
}
impl PartialEq<&String> for TrayIconId {
fn eq(&self, other: &&String) -> bool {
self.0 == **other
}
}
impl PartialEq<&TrayIconId> for TrayIconId {
fn eq(&self, other: &&TrayIconId) -> bool {
other.0 == self.0
}
}
#[derive(Default)]
pub struct TrayIconAttributes {
pub tooltip: Option<String>,
pub icon: Option<Icon>,
pub temp_dir_path: Option<PathBuf>,
pub icon_is_template: bool,
pub title: Option<String>,
}
#[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_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 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: Arc<Mutex<TrayIconImpl>>,
}
impl TrayIcon {
pub fn new(attrs: TrayIconAttributes) -> Result<Self> {
let id = TrayIconId::new_unique();
Ok(Self {
tray: Arc::new(Mutex::new(TrayIconImpl::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: Arc::new(Mutex::new(TrayIconImpl::new(id.clone(), attrs)?)),
id,
})
}
pub fn id(&self) -> &TrayIconId {
&self.id
}
pub fn set_icon(&self, icon: Option<Icon>) -> Result<()> {
self.tray.lock().unwrap().set_icon(icon)
}
pub fn set_tooltip<S: AsRef<str>>(&self, tooltip: Option<S>) -> Result<()> {
self.tray.lock().unwrap().set_tooltip(tooltip)
}
pub fn set_title<S: AsRef<str>>(&self, title: Option<S>) {
self.tray.lock().unwrap().set_title(title)
}
pub fn set_visible(&self, visible: bool) -> Result<()> {
self.tray.lock().unwrap().set_visible(visible)
}
pub fn set_temp_dir_path<P: AsRef<Path>>(&self, path: Option<P>) {
#[cfg(target_os = "linux")]
self.tray.lock().unwrap().set_temp_dir_path(path);
#[cfg(not(target_os = "linux"))]
let _ = path;
}
pub fn set_icon_as_template(&self, is_template: bool) {
#[cfg(target_os = "macos")]
self.tray.lock().unwrap().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
.lock()
.unwrap()
.set_icon_with_as_template(icon, is_template);
#[cfg(not(target_os = "macos"))]
{
let _ = icon;
let _ = is_template;
Ok(())
}
}
pub fn rect(&self) -> Option<Rect> {
self.tray.lock().unwrap().rect()
}
}
#[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, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum MouseButtonState {
#[default]
Up,
Down,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
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);
}
}
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 {
use super::*;
#[test]
fn tray_icon_id_eq() {
assert_eq!(TrayIconId::new("t"), "t");
assert_eq!(TrayIconId::new("t"), String::from("t"));
assert_eq!(TrayIconId::new("t"), &String::from("t"));
assert_eq!(TrayIconId::new("t"), TrayIconId::new("t"));
assert_eq!(TrayIconId::new("t"), &TrayIconId::new("t"));
assert_eq!(&TrayIconId::new("t"), &TrayIconId::new("t"));
assert_eq!(TrayIconId::new("t").as_ref(), "t");
}
}