unistore-tray 0.1.0

System tray capability for UniStore - cross-platform tray icon, menu, and notifications
Documentation
//! 【图标抽象】- TrayIcon 定义
//!
//! 职责:
//! - 提供跨平台的图标加载接口
//! - 支持多种图标来源

use crate::deps::{Path, PathBuf};
use crate::error::{TrayError, TrayResult};

/// 托盘图标
#[derive(Debug, Clone)]
pub struct TrayIcon {
    /// 图标数据来源
    pub(crate) source: IconSource,
}

/// 图标数据来源
#[derive(Debug, Clone)]
pub(crate) enum IconSource {
    /// 从文件路径加载
    File(PathBuf),
    /// 从内存 RGBA 数据
    Rgba {
        data: Vec<u8>,
        width: u32,
        height: u32,
    },
    /// 使用默认图标
    Default,
}

impl TrayIcon {
    /// 从文件路径创建图标
    ///
    /// 支持的格式: PNG, ICO
    pub fn from_path(path: impl AsRef<Path>) -> TrayResult<Self> {
        let path = path.as_ref();
        if !path.exists() {
            return Err(TrayError::IconLoadFailed(format!(
                "图标文件不存在: {}",
                path.display()
            )));
        }
        Ok(Self {
            source: IconSource::File(path.to_path_buf()),
        })
    }

    /// 从 RGBA 数据创建图标
    ///
    /// # Arguments
    /// * `data` - RGBA 格式的像素数据
    /// * `width` - 图标宽度
    /// * `height` - 图标高度
    pub fn from_rgba(data: Vec<u8>, width: u32, height: u32) -> TrayResult<Self> {
        let expected_len = (width * height * 4) as usize;
        if data.len() != expected_len {
            return Err(TrayError::IconLoadFailed(format!(
                "RGBA 数据长度不匹配: 期望 {}, 实际 {}",
                expected_len,
                data.len()
            )));
        }
        Ok(Self {
            source: IconSource::Rgba {
                data,
                width,
                height,
            },
        })
    }

    /// 使用默认图标
    pub fn default_icon() -> Self {
        Self {
            source: IconSource::Default,
        }
    }

    /// 检查是否为默认图标
    pub fn is_default(&self) -> bool {
        matches!(self.source, IconSource::Default)
    }

    /// 获取图标尺寸(如果可用)
    pub fn dimensions(&self) -> Option<(u32, u32)> {
        match &self.source {
            IconSource::Rgba { width, height, .. } => Some((*width, *height)),
            _ => None,
        }
    }
}

impl Default for TrayIcon {
    fn default() -> Self {
        Self::default_icon()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_default_icon() {
        let icon = TrayIcon::default_icon();
        assert!(icon.is_default());
        assert!(icon.dimensions().is_none());
    }

    #[test]
    fn test_from_rgba() {
        // 2x2 红色图标
        let data = vec![
            255, 0, 0, 255, //            255, 0, 0, 255, //            255, 0, 0, 255, //            255, 0, 0, 255, //        ];
        let icon = TrayIcon::from_rgba(data, 2, 2).unwrap();
        assert!(!icon.is_default());
        assert_eq!(icon.dimensions(), Some((2, 2)));
    }

    #[test]
    fn test_from_rgba_invalid_size() {
        let data = vec![0u8; 10]; // 错误的大小
        let result = TrayIcon::from_rgba(data, 2, 2);
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("长度不匹配"));
    }

    #[test]
    fn test_from_path_not_exists() {
        let result = TrayIcon::from_path("/nonexistent/icon.png");
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("不存在"));
    }
}