1use 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
10pub 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 painter.fill_rect(area, Color::rgba(0.0, 0.0, 0.0, 0.5), 0.0);
76
77 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 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}