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 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}