Skip to main content

agpu/widget/
checkbox.rs

1//! Checkbox and Radio button widgets.
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 toggle checkbox with a label.
11pub struct Checkbox {
12    pub id: String,
13    pub label: String,
14    pub checked: 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 Checkbox {
23    #[must_use]
24    pub fn new(id: impl Into<String>, label: impl Into<String>, checked: bool) -> Self {
25        Self {
26            id: id.into(),
27            label: label.into(),
28            checked,
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 Checkbox {
69    fn draw(&self, painter: &mut dyn Painter, area: Rect) {
70        let box_size = 16.0;
71        let box_rect = Rect::new(
72            area.x,
73            area.y + (area.height - box_size) * 0.5,
74            box_size,
75            box_size,
76        );
77
78        let border = self.fg_color.unwrap_or(Color::rgba(0.5, 0.5, 0.6, 1.0));
79        let radius = self.corner_radius.unwrap_or(2.0);
80        painter.stroke_rect(box_rect, border, 1.0, radius);
81
82        if self.checked {
83            let inner = Rect::new(
84                box_rect.x + 3.0,
85                box_rect.y + 3.0,
86                box_size - 6.0,
87                box_size - 6.0,
88            );
89            let check_color = self.bg_color.unwrap_or(Color::rgba(0.2, 0.6, 1.0, 1.0));
90            painter.fill_rect(inner, check_color, 1.0);
91        }
92
93        let text_color = self.fg_color.unwrap_or(Color::WHITE);
94        let style = TextStyle {
95            font_size: self.font_size.unwrap_or(14.0),
96            color: text_color,
97            ..TextStyle::default()
98        };
99        painter.text(
100            Position::new(
101                area.x + box_size + 8.0,
102                area.y + (area.height - style.font_size) * 0.5,
103            ),
104            &self.label,
105            &style,
106        );
107    }
108
109    fn ui_node(&self) -> UiNode {
110        UiNode::new("Checkbox", SemanticRole::Input).with_id(&self.id)
111    }
112}
113
114impl Discoverable for Checkbox {
115    fn schema(&self) -> WidgetSchema {
116        WidgetSchema::new("Checkbox", "A toggle checkbox", SemanticRole::Input)
117    }
118
119    fn capabilities(&self) -> Vec<AgentCapability> {
120        vec![
121            AgentCapability::Focusable,
122            AgentCapability::Toggleable {
123                state: self.checked,
124            },
125        ]
126    }
127
128    fn actions(&self) -> Vec<AgentAction> {
129        vec![AgentAction::simple("toggle", "Toggle the checkbox", true)]
130    }
131
132    fn semantic_role(&self) -> SemanticRole {
133        SemanticRole::Input
134    }
135
136    fn agent_state(&self) -> serde_json::Value {
137        serde_json::json!({ "checked": self.checked, "label": self.label })
138    }
139
140    fn execute_action(
141        &mut self,
142        action: &str,
143        _params: &serde_json::Value,
144    ) -> Result<serde_json::Value, String> {
145        match action {
146            "toggle" => {
147                self.checked = !self.checked;
148                Ok(serde_json::json!({ "checked": self.checked }))
149            }
150            _ => Err(format!("Unknown action: {action}")),
151        }
152    }
153
154    fn agent_id(&self) -> Option<&str> {
155        Some(&self.id)
156    }
157}
158
159/// An exclusive-choice radio button.
160pub struct Radio {
161    pub id: String,
162    pub label: String,
163    pub selected: bool,
164    bg_color: Option<Color>,
165    fg_color: Option<Color>,
166    font_size: Option<f32>,
167    is_bold: bool,
168}
169
170impl Radio {
171    #[must_use]
172    pub fn new(id: impl Into<String>, label: impl Into<String>, selected: bool) -> Self {
173        Self {
174            id: id.into(),
175            label: label.into(),
176            selected,
177            bg_color: None,
178            fg_color: None,
179            font_size: None,
180            is_bold: false,
181        }
182    }
183
184    #[must_use]
185    pub fn bg(mut self, color: Color) -> Self {
186        self.bg_color = Some(color);
187        self
188    }
189
190    #[must_use]
191    pub fn fg(mut self, color: Color) -> Self {
192        self.fg_color = Some(color);
193        self
194    }
195
196    #[must_use]
197    pub fn text_size(mut self, size: f32) -> Self {
198        self.font_size = Some(size);
199        self
200    }
201
202    #[must_use]
203    pub fn bold(mut self) -> Self {
204        self.is_bold = true;
205        self
206    }
207}
208
209impl Widget for Radio {
210    fn draw(&self, painter: &mut dyn Painter, area: Rect) {
211        let radius = 8.0;
212        let center = Position::new(area.x + radius, area.y + area.height * 0.5);
213
214        let border = self.fg_color.unwrap_or(Color::rgba(0.5, 0.5, 0.6, 1.0));
215        painter.stroke_circle(center, radius, border, 1.0);
216
217        if self.selected {
218            let fill = self.bg_color.unwrap_or(Color::rgba(0.2, 0.6, 1.0, 1.0));
219            painter.fill_circle(center, radius - 3.0, fill);
220        }
221
222        let text_color = self.fg_color.unwrap_or(Color::WHITE);
223        let style = TextStyle {
224            font_size: self.font_size.unwrap_or(14.0),
225            color: text_color,
226            ..TextStyle::default()
227        };
228        painter.text(
229            Position::new(
230                area.x + radius * 2.0 + 8.0,
231                area.y + (area.height - style.font_size) * 0.5,
232            ),
233            &self.label,
234            &style,
235        );
236    }
237
238    fn ui_node(&self) -> UiNode {
239        UiNode::new("Radio", SemanticRole::Input).with_id(&self.id)
240    }
241}
242
243impl Discoverable for Radio {
244    fn schema(&self) -> WidgetSchema {
245        WidgetSchema::new(
246            "Radio",
247            "An exclusive-choice radio button",
248            SemanticRole::Input,
249        )
250    }
251
252    fn capabilities(&self) -> Vec<AgentCapability> {
253        vec![
254            AgentCapability::Focusable,
255            AgentCapability::Selectable {
256                multi_select: false,
257                item_count: 1,
258            },
259        ]
260    }
261
262    fn actions(&self) -> Vec<AgentAction> {
263        vec![AgentAction::simple(
264            "select",
265            "Select this radio option",
266            true,
267        )]
268    }
269
270    fn semantic_role(&self) -> SemanticRole {
271        SemanticRole::Input
272    }
273
274    fn agent_state(&self) -> serde_json::Value {
275        serde_json::json!({ "selected": self.selected, "label": self.label })
276    }
277
278    fn execute_action(
279        &mut self,
280        action: &str,
281        _params: &serde_json::Value,
282    ) -> Result<serde_json::Value, String> {
283        match action {
284            "select" => {
285                self.selected = true;
286                Ok(serde_json::json!({ "selected": true }))
287            }
288            _ => Err(format!("Unknown action: {action}")),
289        }
290    }
291
292    fn agent_id(&self) -> Option<&str> {
293        Some(&self.id)
294    }
295}