unistore-tray 0.1.0

System tray capability for UniStore - cross-platform tray icon, menu, and notifications
Documentation
//! 【菜单系统】- Menu 和 MenuItem 定义
//!
//! 职责:
//! - 定义菜单数据结构
//! - 提供菜单构建器 API

use crate::event::MenuItemId;

/// 菜单
#[derive(Debug, Clone, Default)]
pub struct Menu {
    items: Vec<MenuItem>,
}

impl Menu {
    /// 创建空菜单
    pub fn new() -> Self {
        Self { items: Vec::new() }
    }

    /// 获取菜单项列表
    pub fn items(&self) -> &[MenuItem] {
        &self.items
    }

    /// 获取菜单项数量
    pub fn len(&self) -> usize {
        self.items.len()
    }

    /// 菜单是否为空
    pub fn is_empty(&self) -> bool {
        self.items.is_empty()
    }

    /// 查找菜单项
    pub fn find_item(&self, id: &MenuItemId) -> Option<&MenuItem> {
        self.items.iter().find(|item| {
            if let MenuItem::Item { id: item_id, .. } = item {
                item_id == id
            } else if let MenuItem::CheckItem { id: item_id, .. } = item {
                item_id == id
            } else {
                false
            }
        })
    }
}

/// 菜单项
#[derive(Debug, Clone)]
pub enum MenuItem {
    /// 普通菜单项
    Item {
        id: MenuItemId,
        label: String,
        enabled: bool,
    },
    /// 复选框菜单项
    CheckItem {
        id: MenuItemId,
        label: String,
        checked: bool,
        enabled: bool,
    },
    /// 分隔线
    Separator,
    /// 子菜单
    SubMenu {
        label: String,
        items: Vec<MenuItem>,
    },
}

impl MenuItem {
    /// 创建普通菜单项
    pub fn new(id: impl Into<MenuItemId>, label: impl Into<String>) -> Self {
        MenuItem::Item {
            id: id.into(),
            label: label.into(),
            enabled: true,
        }
    }

    /// 创建复选框菜单项
    pub fn check(
        id: impl Into<MenuItemId>,
        label: impl Into<String>,
        checked: bool,
    ) -> Self {
        MenuItem::CheckItem {
            id: id.into(),
            label: label.into(),
            checked,
            enabled: true,
        }
    }

    /// 创建分隔线
    pub fn separator() -> Self {
        MenuItem::Separator
    }

    /// 获取菜单项 ID(如果有)
    pub fn id(&self) -> Option<&MenuItemId> {
        match self {
            MenuItem::Item { id, .. } => Some(id),
            MenuItem::CheckItem { id, .. } => Some(id),
            _ => None,
        }
    }

    /// 获取标签(如果有)
    pub fn label(&self) -> Option<&str> {
        match self {
            MenuItem::Item { label, .. } => Some(label),
            MenuItem::CheckItem { label, .. } => Some(label),
            MenuItem::SubMenu { label, .. } => Some(label),
            MenuItem::Separator => None,
        }
    }

    /// 是否启用
    pub fn is_enabled(&self) -> bool {
        match self {
            MenuItem::Item { enabled, .. } => *enabled,
            MenuItem::CheckItem { enabled, .. } => *enabled,
            MenuItem::SubMenu { .. } => true,
            MenuItem::Separator => false,
        }
    }
}

/// 菜单构建器
#[derive(Debug, Default)]
pub struct MenuBuilder {
    items: Vec<MenuItem>,
}

impl MenuBuilder {
    /// 创建新的构建器
    pub fn new() -> Self {
        Self { items: Vec::new() }
    }

    /// 添加普通菜单项
    pub fn item(mut self, id: impl Into<MenuItemId>, label: impl Into<String>) -> Self {
        self.items.push(MenuItem::Item {
            id: id.into(),
            label: label.into(),
            enabled: true,
        });
        self
    }

    /// 添加禁用的菜单项
    pub fn item_disabled(
        mut self,
        id: impl Into<MenuItemId>,
        label: impl Into<String>,
    ) -> Self {
        self.items.push(MenuItem::Item {
            id: id.into(),
            label: label.into(),
            enabled: false,
        });
        self
    }

    /// 添加复选框菜单项
    pub fn check_item(
        mut self,
        id: impl Into<MenuItemId>,
        label: impl Into<String>,
        checked: bool,
    ) -> Self {
        self.items.push(MenuItem::CheckItem {
            id: id.into(),
            label: label.into(),
            checked,
            enabled: true,
        });
        self
    }

    /// 添加分隔线
    pub fn separator(mut self) -> Self {
        self.items.push(MenuItem::Separator);
        self
    }

    /// 添加子菜单
    pub fn submenu(mut self, label: impl Into<String>, builder: MenuBuilder) -> Self {
        self.items.push(MenuItem::SubMenu {
            label: label.into(),
            items: builder.items,
        });
        self
    }

    /// 构建菜单
    pub fn build(self) -> Menu {
        Menu { items: self.items }
    }
}

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

    #[test]
    fn test_empty_menu() {
        let menu = Menu::new();
        assert!(menu.is_empty());
        assert_eq!(menu.len(), 0);
    }

    #[test]
    fn test_menu_builder() {
        let menu = MenuBuilder::new()
            .item("open", "打开")
            .separator()
            .check_item("auto_start", "开机启动", false)
            .item("exit", "退出")
            .build();

        assert_eq!(menu.len(), 4);
        assert!(!menu.is_empty());
    }

    #[test]
    fn test_submenu() {
        let _submenu = MenuBuilder::new()
            .item("sub1", "子项1")
            .item("sub2", "子项2")
            .build();

        let menu = MenuBuilder::new()
            .item("main", "主菜单")
            .submenu("更多", MenuBuilder::new().item("sub1", "子项1"))
            .build();

        assert_eq!(menu.len(), 2);

        if let MenuItem::SubMenu { label, items } = &menu.items()[1] {
            assert_eq!(label, "更多");
            assert_eq!(items.len(), 1);
        } else {
            panic!("Expected SubMenu");
        }
    }

    #[test]
    fn test_find_item() {
        let menu = MenuBuilder::new()
            .item("open", "打开")
            .item("exit", "退出")
            .build();

        let open = menu.find_item(&MenuItemId::new("open"));
        assert!(open.is_some());
        assert_eq!(open.unwrap().label(), Some("打开"));

        let not_found = menu.find_item(&MenuItemId::new("unknown"));
        assert!(not_found.is_none());
    }

    #[test]
    fn test_menu_item_properties() {
        let item = MenuItem::new("test", "Test Label");
        assert_eq!(item.id().map(|id| id.as_str()), Some("test"));
        assert_eq!(item.label(), Some("Test Label"));
        assert!(item.is_enabled());

        let sep = MenuItem::separator();
        assert!(sep.id().is_none());
        assert!(sep.label().is_none());
        assert!(!sep.is_enabled());
    }

    #[test]
    fn test_disabled_item() {
        let menu = MenuBuilder::new()
            .item_disabled("disabled", "禁用项")
            .build();

        if let MenuItem::Item { enabled, .. } = &menu.items()[0] {
            assert!(!enabled);
        } else {
            panic!("Expected Item");
        }
    }
}