deweygui 1.0.0

An agentic-first GUI framework with pluggable rendering backends and complete ontology for AI agent discoverability
//! Menu widget — menus and menu items.

use crate::core::{Color, Position, Rect, Style};
use crate::ontology::*;
use crate::runtime::Frame;
use crate::widget::Widget;

/// A single menu item.
#[derive(Debug, Clone)]
pub struct MenuItem {
    pub label: String,
    pub shortcut: Option<String>,
    pub enabled: bool,
}

impl MenuItem {
    #[must_use]
    pub fn new(label: impl Into<String>) -> Self {
        Self {
            label: label.into(),
            shortcut: None,
            enabled: true,
        }
    }

    pub fn shortcut(mut self, shortcut: impl Into<String>) -> Self {
        self.shortcut = Some(shortcut.into());
        self
    }

    pub fn enabled(mut self, enabled: bool) -> Self {
        self.enabled = enabled;
        self
    }
}

/// A menu containing items.
pub struct Menu {
    title: String,
    items: Vec<MenuItem>,
    style: Style,
    agent_id: String,
}

impl Menu {
    #[must_use]
    pub fn new(title: impl Into<String>, items: Vec<MenuItem>) -> Self {
        Self {
            title: title.into(),
            items,
            style: Style::default(),
            agent_id: String::new(),
        }
    }

    pub fn style(mut self, style: Style) -> Self {
        self.style = style;
        self
    }

    pub fn bg(mut self, color: Color) -> Self {
        self.style.background = Some(color);
        self
    }

    pub fn fg(mut self, color: Color) -> Self {
        self.style.foreground = Some(color);
        self
    }

    pub fn agent_id(mut self, id: impl Into<String>) -> Self {
        self.agent_id = id.into();
        self
    }
}

impl Discoverable for Menu {
    fn schema(&self) -> WidgetSchema {
        let mut schema = WidgetSchema::new("Menu", "A dropdown menu", SemanticRole::Menu);
        schema.usage_hint = Some("Menu::new(\"File\", vec![MenuItem::new(\"Open\")])".into());
        schema.tags = vec!["menu".into(), "dropdown".into(), "navigation".into()];
        schema
    }

    fn capabilities(&self) -> Vec<AgentCapability> {
        vec![
            AgentCapability::Selectable {
                multi_select: false,
                item_count: self.items.len(),
            },
            AgentCapability::Focusable,
        ]
    }

    fn actions(&self) -> Vec<AgentAction> {
        vec![AgentAction::with_params(
            "select_item",
            "Select a menu item",
            vec![ActionParam::required(
                "label",
                "Menu item label",
                ActionParamType::String,
            )],
            true,
        )]
    }

    fn semantic_role(&self) -> SemanticRole {
        SemanticRole::Menu
    }

    fn agent_state(&self) -> serde_json::Value {
        let items: Vec<_> = self.items.iter().map(|i| &i.label).collect();
        serde_json::json!({ "title": self.title, "items": items })
    }

    fn execute_action(
        &mut self,
        _action: &str,
        _params: &serde_json::Value,
    ) -> Result<serde_json::Value, String> {
        Err("Menu actions should be handled via messages".to_string())
    }

    fn agent_id(&self) -> Option<&str> {
        if self.agent_id.is_empty() {
            None
        } else {
            Some(&self.agent_id)
        }
    }

    fn accessibility_label(&self) -> Option<String> {
        Some(self.title.clone())
    }
}

impl Widget for Menu {
    fn render(self, area: Rect, frame: &mut Frame<'_>) {
        if !self.agent_id.is_empty() {
            let node = UiNode::new("Menu", SemanticRole::Menu)
                .with_id(&self.agent_id)
                .with_bounds(area.into())
                .with_property("title", serde_json::json!(self.title));
            frame.register_widget(node);
        }

        // Menu bar background
        let bar_h = 28.0;
        let bar = Rect::new(area.x, area.y, area.width, bar_h);
        let bar_bg = self.style.background.unwrap_or(Color::DARK_GRAY);
        frame.painter().fill_rect(bar, bar_bg, 0.0);
        let ts = self.style.resolved_text();
        frame
            .painter()
            .text(Position::new(area.x + 8.0, area.y + 6.0), &self.title, &ts);
    }
}