Skip to main content

agpu/widget/
modal.rs

1//! Modal dialog widget.
2
3use crate::core::{Color, Position, Rect, TextStyle};
4use crate::ontology::{
5    AgentAction, AgentCapability, Discoverable, SemanticRole, UiNode, WidgetSchema,
6};
7use crate::paint::Painter;
8use crate::widget::Widget;
9
10/// A modal overlay dialog.
11pub struct Modal {
12    pub id: String,
13    pub title: String,
14    pub visible: bool,
15    bg_color: Option<Color>,
16    fg_color: Option<Color>,
17    corner_radius: Option<f32>,
18    font_size: Option<f32>,
19    is_bold: bool,
20}
21
22impl Modal {
23    #[must_use]
24    pub fn new(id: impl Into<String>, title: impl Into<String>, visible: bool) -> Self {
25        Self {
26            id: id.into(),
27            title: title.into(),
28            visible,
29            bg_color: None,
30            fg_color: None,
31            corner_radius: None,
32            font_size: None,
33            is_bold: false,
34        }
35    }
36
37    #[must_use]
38    pub fn bg(mut self, color: Color) -> Self {
39        self.bg_color = Some(color);
40        self
41    }
42
43    #[must_use]
44    pub fn fg(mut self, color: Color) -> Self {
45        self.fg_color = Some(color);
46        self
47    }
48
49    #[must_use]
50    pub fn rounded(mut self, radius: f32) -> Self {
51        self.corner_radius = Some(radius);
52        self
53    }
54
55    #[must_use]
56    pub fn text_size(mut self, size: f32) -> Self {
57        self.font_size = Some(size);
58        self
59    }
60
61    #[must_use]
62    pub fn bold(mut self) -> Self {
63        self.is_bold = true;
64        self
65    }
66}
67
68impl Widget for Modal {
69    fn draw(&self, painter: &mut dyn Painter, area: Rect) {
70        if !self.visible {
71            return;
72        }
73
74        // Overlay backdrop
75        painter.fill_rect(area, Color::rgba(0.0, 0.0, 0.0, 0.5), 0.0);
76
77        // Dialog box centred in the area
78        let dialog_w = (area.width * 0.5).min(400.0);
79        let dialog_h = (area.height * 0.4).min(240.0);
80        let dialog_x = area.x + (area.width - dialog_w) * 0.5;
81        let dialog_y = area.y + (area.height - dialog_h) * 0.5;
82        let dialog = Rect::new(dialog_x, dialog_y, dialog_w, dialog_h);
83
84        let bg = self.bg_color.unwrap_or(Color::rgba(0.18, 0.18, 0.22, 1.0));
85        let radius = self.corner_radius.unwrap_or(6.0);
86        painter.fill_rect(dialog, bg, radius);
87        painter.stroke_rect(dialog, Color::rgba(0.35, 0.35, 0.45, 1.0), 1.0, radius);
88
89        // Title
90        let style = TextStyle {
91            font_size: self.font_size.unwrap_or(16.0),
92            color: self.fg_color.unwrap_or(Color::WHITE),
93            ..TextStyle::default()
94        };
95        let text_size = painter.measure_text(&self.title, &style);
96        painter.text(
97            Position::new(
98                dialog_x + (dialog_w - text_size.width) * 0.5,
99                dialog_y + 16.0,
100            ),
101            &self.title,
102            &style,
103        );
104    }
105
106    fn ui_node(&self) -> UiNode {
107        UiNode::new("Modal", SemanticRole::Modal).with_id(&self.id)
108    }
109}
110
111impl Discoverable for Modal {
112    fn schema(&self) -> WidgetSchema {
113        WidgetSchema::new("Modal", "A modal dialog overlay", SemanticRole::Modal)
114    }
115
116    fn capabilities(&self) -> Vec<AgentCapability> {
117        vec![AgentCapability::Focusable, AgentCapability::Closable]
118    }
119
120    fn actions(&self) -> Vec<AgentAction> {
121        vec![
122            AgentAction::simple("open", "Show the modal", true),
123            AgentAction::simple("close", "Hide the modal", true),
124        ]
125    }
126
127    fn semantic_role(&self) -> SemanticRole {
128        SemanticRole::Modal
129    }
130
131    fn agent_state(&self) -> serde_json::Value {
132        serde_json::json!({
133            "title": self.title,
134            "visible": self.visible,
135        })
136    }
137
138    fn execute_action(
139        &mut self,
140        action: &str,
141        _params: &serde_json::Value,
142    ) -> Result<serde_json::Value, String> {
143        match action {
144            "open" => {
145                self.visible = true;
146                Ok(serde_json::json!({ "visible": true }))
147            }
148            "close" => {
149                self.visible = false;
150                Ok(serde_json::json!({ "visible": false }))
151            }
152            _ => Err(format!("Unknown action: {action}")),
153        }
154    }
155
156    fn agent_id(&self) -> Option<&str> {
157        Some(&self.id)
158    }
159}