Skip to main content

agpu/widget/
select.rs

1//! Select / Dropdown 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 dropdown select widget for single-item selection.
11pub 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        // Dropdown arrow
117        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}