use crate::backend::PlatformTrayOps;
use crate::config::TrayConfig;
use crate::error::{TrayError, TrayResult};
use crate::event::{MenuItemId, TrayEvent};
use crate::icon::{IconSource, TrayIcon};
use crate::menu::{Menu, MenuItem};
use parking_lot::RwLock;
use std::sync::Arc;
use tokio::sync::broadcast;
use tray_icon::menu::{Menu as TrayMenu, MenuEvent, MenuItem as TrayMenuItem, PredefinedMenuItem};
use tray_icon::{Icon, TrayIconBuilder};
pub struct PlatformTray {
inner: Arc<RwLock<Option<tray_icon::TrayIcon>>>,
event_tx: broadcast::Sender<TrayEvent>,
#[allow(dead_code)]
config: TrayConfig,
}
impl PlatformTray {
fn create_default_icon() -> TrayResult<Icon> {
let width = 22u32; let height = 22u32;
let mut rgba = Vec::with_capacity((width * height * 4) as usize);
for y in 0..height {
for x in 0..width {
let is_border = x < 1 || x >= width - 1 || y < 1 || y >= height - 1;
if is_border {
rgba.extend_from_slice(&[0, 51, 102, 255]);
} else {
rgba.extend_from_slice(&[51, 153, 255, 255]);
}
}
}
Icon::from_rgba(rgba, width, height)
.map_err(|e| TrayError::IconLoadFailed(e.to_string()))
}
fn create_icon(icon: &TrayIcon) -> TrayResult<Icon> {
match &icon.source {
IconSource::Default => Self::create_default_icon(),
IconSource::File(path) => {
let img = image::open(path)
.map_err(|e| TrayError::IconLoadFailed(e.to_string()))?;
let rgba = img.to_rgba8();
let (width, height) = rgba.dimensions();
Icon::from_rgba(rgba.into_raw(), width, height)
.map_err(|e| TrayError::IconLoadFailed(e.to_string()))
}
IconSource::Rgba { data, width, height } => {
Icon::from_rgba(data.clone(), *width, *height)
.map_err(|e| TrayError::IconLoadFailed(e.to_string()))
}
}
}
fn build_tray_menu(menu: &Menu) -> TrayResult<TrayMenu> {
let tray_menu = TrayMenu::new();
for item in menu.items() {
match item {
MenuItem::Item { id, label, enabled } => {
let menu_item = TrayMenuItem::with_id(id.as_str(), label, *enabled, None);
tray_menu.append(&menu_item)
.map_err(|e| TrayError::MenuFailed(e.to_string()))?;
}
MenuItem::CheckItem { id, label, checked, enabled } => {
let check_item = tray_icon::menu::CheckMenuItem::with_id(
id.as_str(), label, *enabled, *checked, None
);
tray_menu.append(&check_item)
.map_err(|e| TrayError::MenuFailed(e.to_string()))?;
}
MenuItem::Separator => {
tray_menu.append(&PredefinedMenuItem::separator())
.map_err(|e| TrayError::MenuFailed(e.to_string()))?;
}
MenuItem::SubMenu { label, items } => {
let submenu = tray_icon::menu::Submenu::new(label, true);
for sub_item in items {
if let MenuItem::Item { id, label, enabled } = sub_item {
let menu_item = TrayMenuItem::with_id(id.as_str(), label, *enabled, None);
submenu.append(&menu_item)
.map_err(|e| TrayError::MenuFailed(e.to_string()))?;
}
}
tray_menu.append(&submenu)
.map_err(|e| TrayError::MenuFailed(e.to_string()))?;
}
}
}
Ok(tray_menu)
}
fn start_menu_event_listener(event_tx: broadcast::Sender<TrayEvent>) {
let receiver = MenuEvent::receiver();
std::thread::spawn(move || {
while let Ok(event) = receiver.recv() {
let id = MenuItemId::new(event.id.0);
let _ = event_tx.send(TrayEvent::MenuItemClicked(id));
}
});
}
}
impl PlatformTrayOps for PlatformTray {
fn new(config: &TrayConfig, event_tx: broadcast::Sender<TrayEvent>) -> TrayResult<Self> {
let icon = if let Some(path) = &config.icon_path {
Self::create_icon(&TrayIcon::from_path(path)?)?
} else {
Self::create_default_icon()?
};
let tray = TrayIconBuilder::new()
.with_tooltip(&config.tooltip)
.with_icon(icon)
.build()
.map_err(|e| TrayError::InitFailed(e.to_string()))?;
Self::start_menu_event_listener(event_tx.clone());
let _ = event_tx.send(TrayEvent::Ready);
Ok(Self {
inner: Arc::new(RwLock::new(Some(tray))),
event_tx,
config: config.clone(),
})
}
fn set_icon(&self, icon: &TrayIcon) -> TrayResult<()> {
let tray_icon = Self::create_icon(icon)?;
let guard = self.inner.read();
if let Some(tray) = guard.as_ref() {
tray.set_icon(Some(tray_icon))
.map_err(|e| TrayError::IconLoadFailed(e.to_string()))?;
}
Ok(())
}
fn set_tooltip(&self, tooltip: &str) -> TrayResult<()> {
let guard = self.inner.read();
if let Some(tray) = guard.as_ref() {
tray.set_tooltip(Some(tooltip))
.map_err(|e| TrayError::SystemError(e.to_string()))?;
}
Ok(())
}
fn set_menu(&self, menu: &Menu) -> TrayResult<()> {
let tray_menu = Self::build_tray_menu(menu)?;
let guard = self.inner.read();
if let Some(tray) = guard.as_ref() {
tray.set_menu(Some(Box::new(tray_menu)));
}
Ok(())
}
fn destroy(&self) -> TrayResult<()> {
let mut guard = self.inner.write();
if guard.take().is_some() {
let _ = self.event_tx.send(TrayEvent::Destroyed);
}
Ok(())
}
}
unsafe impl Send for PlatformTray {}
unsafe impl Sync for PlatformTray {}