Skip to main content

agpu/widget/
button.rs

1//! Button 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 clickable button with a text label.
11pub struct Button {
12    pub id: String,
13    pub label: String,
14    pub disabled: bool,
15    pub icon: Option<String>,
16    bg_color: Option<Color>,
17    fg_color: Option<Color>,
18    corner_radius: Option<f32>,
19    font_size: Option<f32>,
20    is_bold: bool,
21}
22
23impl Button {
24    #[must_use]
25    pub fn new(id: impl Into<String>, label: impl Into<String>) -> Self {
26        Self {
27            id: id.into(),
28            label: label.into(),
29            disabled: false,
30            icon: None,
31            bg_color: None,
32            fg_color: None,
33            corner_radius: None,
34            font_size: None,
35            is_bold: false,
36        }
37    }
38
39    #[must_use]
40    pub fn disabled(mut self, disabled: bool) -> Self {
41        self.disabled = disabled;
42        self
43    }
44
45    #[must_use]
46    pub fn icon(mut self, icon: impl Into<String>) -> Self {
47        self.icon = Some(icon.into());
48        self
49    }
50
51    #[must_use]
52    pub fn bg(mut self, color: Color) -> Self {
53        self.bg_color = Some(color);
54        self
55    }
56
57    #[must_use]
58    pub fn fg(mut self, color: Color) -> Self {
59        self.fg_color = Some(color);
60        self
61    }
62
63    #[must_use]
64    pub fn rounded(mut self, radius: f32) -> Self {
65        self.corner_radius = Some(radius);
66        self
67    }
68
69    #[must_use]
70    pub fn text_size(mut self, size: f32) -> Self {
71        self.font_size = Some(size);
72        self
73    }
74
75    #[must_use]
76    pub fn bold(mut self) -> Self {
77        self.is_bold = true;
78        self
79    }
80}
81
82impl Widget for Button {
83    fn draw(&self, painter: &mut dyn Painter, area: Rect) {
84        let bg = self.bg_color.unwrap_or(if self.disabled {
85            Color::rgba(0.3, 0.3, 0.3, 1.0)
86        } else {
87            Color::rgba(0.2, 0.4, 0.8, 1.0)
88        });
89        let fg = self.fg_color.unwrap_or(if self.disabled {
90            Color::rgba(0.6, 0.6, 0.6, 1.0)
91        } else {
92            Color::WHITE
93        });
94        let radius = self.corner_radius.unwrap_or(4.0);
95
96        painter.fill_rect(area, bg, radius);
97        let style = TextStyle {
98            font_size: self.font_size.unwrap_or(14.0),
99            color: fg,
100            ..TextStyle::default()
101        };
102        let text_size = painter.measure_text(&self.label, &style);
103        let tx = area.x + (area.width - text_size.width) * 0.5;
104        let ty = area.y + (area.height - text_size.height) * 0.5;
105        painter.text(Position::new(tx, ty), &self.label, &style);
106    }
107
108    fn ui_node(&self) -> UiNode {
109        UiNode::new("Button", SemanticRole::Action).with_id(&self.id)
110    }
111}
112
113impl Discoverable for Button {
114    fn schema(&self) -> WidgetSchema {
115        WidgetSchema::new("Button", "A clickable button", SemanticRole::Action)
116    }
117
118    fn capabilities(&self) -> Vec<AgentCapability> {
119        if self.disabled {
120            vec![]
121        } else {
122            vec![AgentCapability::Clickable, AgentCapability::Focusable]
123        }
124    }
125
126    fn actions(&self) -> Vec<AgentAction> {
127        if self.disabled {
128            vec![]
129        } else {
130            vec![AgentAction::simple("click", "Click the button", false)]
131        }
132    }
133
134    fn semantic_role(&self) -> SemanticRole {
135        SemanticRole::Action
136    }
137
138    fn agent_state(&self) -> serde_json::Value {
139        serde_json::json!({ "label": self.label, "disabled": self.disabled })
140    }
141
142    fn execute_action(
143        &mut self,
144        action: &str,
145        _params: &serde_json::Value,
146    ) -> Result<serde_json::Value, String> {
147        match action {
148            "click" if !self.disabled => Ok(serde_json::json!({ "clicked": true })),
149            "click" => Err("Button is disabled".into()),
150            _ => Err(format!("Unknown action: {action}")),
151        }
152    }
153
154    fn agent_id(&self) -> Option<&str> {
155        Some(&self.id)
156    }
157}