unistore-tray 0.1.0

System tray capability for UniStore - cross-platform tray icon, menu, and notifications
Documentation
//! 【Linux 实现】- 基于 tray-icon 的 Linux 托盘实现
//!
//! 职责:
//! - 封装 tray-icon 库的 Linux 实现
//! - 处理不同桌面环境的差异

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};

/// Linux 平台托盘实现
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 = 24u32;
        let height = 24u32;
        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 {}