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 Select {
12 pub id: String,
13 pub options: Vec<String>,
14 pub selected: Option<usize>,
15 pub open: bool,
16 pub searchable: bool,
17 pub search_query: String,
18 bg_color: Option<Color>,
19 fg_color: Option<Color>,
20 corner_radius: Option<f32>,
21 font_size: Option<f32>,
22 is_bold: bool,
23}
24
25impl Select {
26 #[must_use]
27 pub fn new(id: impl Into<String>, options: Vec<String>) -> Self {
28 Self {
29 id: id.into(),
30 options,
31 selected: None,
32 open: false,
33 searchable: false,
34 search_query: String::new(),
35 bg_color: None,
36 fg_color: None,
37 corner_radius: None,
38 font_size: None,
39 is_bold: false,
40 }
41 }
42
43 #[must_use]
44 pub fn selected(mut self, index: usize) -> Self {
45 self.selected = Some(index);
46 self
47 }
48
49 #[must_use]
50 pub fn searchable(mut self, searchable: bool) -> Self {
51 self.searchable = searchable;
52 self
53 }
54
55 pub fn selected_label(&self) -> &str {
56 self.selected
57 .and_then(|i| self.options.get(i))
58 .map(|s| s.as_str())
59 .unwrap_or("Select...")
60 }
61
62 #[must_use]
63 pub fn bg(mut self, color: Color) -> Self {
64 self.bg_color = Some(color);
65 self
66 }
67
68 #[must_use]
69 pub fn fg(mut self, color: Color) -> Self {
70 self.fg_color = Some(color);
71 self
72 }
73
74 #[must_use]
75 pub fn rounded(mut self, radius: f32) -> Self {
76 self.corner_radius = Some(radius);
77 self
78 }
79
80 #[must_use]
81 pub fn text_size(mut self, size: f32) -> Self {
82 self.font_size = Some(size);
83 self
84 }
85
86 #[must_use]
87 pub fn bold(mut self) -> Self {
88 self.is_bold = true;
89 self
90 }
91}
92
93impl Widget for Select {
94 fn draw(&self, painter: &mut dyn Painter, area: Rect) {
95 let bg = self.bg_color.unwrap_or(Color::rgba(0.18, 0.18, 0.22, 1.0));
96 let radius = self.corner_radius.unwrap_or(3.0);
97 painter.fill_rect(area, bg, radius);
98 painter.stroke_rect(area, Color::rgba(0.4, 0.4, 0.5, 1.0), 1.0, radius);
99
100 let style = TextStyle {
101 font_size: self.font_size.unwrap_or(14.0),
102 color: self.fg_color.unwrap_or(Color::WHITE),
103 ..TextStyle::default()
104 };
105
106 let padding = 8.0;
107 painter.text(
108 Position::new(
109 area.x + padding,
110 area.y + (area.height - style.font_size) * 0.5,
111 ),
112 self.selected_label(),
113 &style,
114 );
115
116 let arrow_x = area.x + area.width - 20.0;
118 let arrow_y = area.y + area.height * 0.5;
119 painter.line(
120 Position::new(arrow_x, arrow_y - 3.0),
121 Position::new(arrow_x + 6.0, arrow_y + 3.0),
122 Color::WHITE,
123 1.5,
124 );
125 painter.line(
126 Position::new(arrow_x + 6.0, arrow_y + 3.0),
127 Position::new(arrow_x + 12.0, arrow_y - 3.0),
128 Color::WHITE,
129 1.5,
130 );
131 }
132
133 fn ui_node(&self) -> UiNode {
134 UiNode::new("Select", SemanticRole::Selection).with_id(&self.id)
135 }
136}
137
138impl Discoverable for Select {
139 fn schema(&self) -> WidgetSchema {
140 WidgetSchema::new("Select", "A dropdown selection", SemanticRole::Selection)
141 }
142
143 fn capabilities(&self) -> Vec<AgentCapability> {
144 vec![
145 AgentCapability::Focusable,
146 AgentCapability::Selectable {
147 multi_select: false,
148 item_count: self.options.len(),
149 },
150 ]
151 }
152
153 fn actions(&self) -> Vec<AgentAction> {
154 vec![
155 AgentAction::simple("open", "Open the dropdown", true),
156 AgentAction::simple("close", "Close the dropdown", true),
157 AgentAction::simple("select", "Select an option by index", true),
158 ]
159 }
160
161 fn semantic_role(&self) -> SemanticRole {
162 SemanticRole::Selection
163 }
164
165 fn agent_state(&self) -> serde_json::Value {
166 serde_json::json!({
167 "selected": self.selected,
168 "selected_label": self.selected_label(),
169 "option_count": self.options.len(),
170 "open": self.open,
171 })
172 }
173
174 fn execute_action(
175 &mut self,
176 action: &str,
177 params: &serde_json::Value,
178 ) -> Result<serde_json::Value, String> {
179 match action {
180 "open" => {
181 self.open = true;
182 Ok(serde_json::json!({ "open": true }))
183 }
184 "close" => {
185 self.open = false;
186 Ok(serde_json::json!({ "open": false }))
187 }
188 "select" => {
189 if let Some(idx) = params.get("index").and_then(|v| v.as_u64()) {
190 let idx = idx as usize;
191 if idx < self.options.len() {
192 self.selected = Some(idx);
193 self.open = false;
194 Ok(serde_json::json!({ "selected": idx, "label": self.options[idx] }))
195 } else {
196 Err("Index out of range".into())
197 }
198 } else {
199 Err("Missing 'index' parameter".into())
200 }
201 }
202 _ => Err(format!("Unknown action: {action}")),
203 }
204 }
205
206 fn agent_id(&self) -> Option<&str> {
207 Some(&self.id)
208 }
209}