unistore-tray 0.1.0

System tray capability for UniStore - cross-platform tray icon, menu, and notifications
Documentation
//! 【Windows 实现】- 基于 tray-icon 的 Windows 托盘实现
//!
//! 职责:
//! - 封装 tray-icon 库
//! - 处理 Windows 特定的托盘行为

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

/// Windows 平台托盘实现
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> {
        // 创建一个 32x32 的蓝色图标
        let width = 32u32;
        let height = 32u32;
        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 < 2 || x >= width - 2 || y < 2 || y >= height - 2;
                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()))
    }

    /// 从 TrayIcon 创建 tray-icon 的 Icon
    fn create_icon(icon: &TrayIcon) -> TrayResult<Icon> {
        match &icon.source {
            IconSource::Default => Self::create_default_icon(),
            IconSource::File(path) => {
                // 使用 image crate 加载图片
                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())),
        }
    }

    /// 构建 tray-icon 菜单
    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);
                    Self::populate_submenu(&submenu, items)?;
                    tray_menu
                        .append(&submenu)
                        .map_err(|e| TrayError::MenuFailed(e.to_string()))?;
                }
            }
        }

        Ok(tray_menu)
    }

    /// 填充子菜单
    fn populate_submenu(
        submenu: &tray_icon::menu::Submenu,
        items: &[MenuItem],
    ) -> TrayResult<()> {
        for item in items {
            match item {
                MenuItem::Item { id, label, enabled } => {
                    let menu_item = TrayMenuItem::with_id(id.as_str(), label, *enabled, None);
                    submenu
                        .append(&menu_item)
                        .map_err(|e| TrayError::MenuFailed(e.to_string()))?;
                }
                MenuItem::Separator => {
                    submenu
                        .append(&PredefinedMenuItem::separator())
                        .map_err(|e| TrayError::MenuFailed(e.to_string()))?;
                }
                _ => {} // 忽略子菜单中的子菜单和复选项(简化处理)
            }
        }
        Ok(())
    }

    /// 启动菜单事件监听
    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 {}