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 Tabs {
12 pub id: String,
13 pub labels: Vec<String>,
14 pub active: usize,
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 Tabs {
23 #[must_use]
24 pub fn new(id: impl Into<String>, labels: Vec<String>, active: usize) -> Self {
25 Self {
26 id: id.into(),
27 labels,
28 active,
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 Tabs {
69 fn draw(&self, painter: &mut dyn Painter, area: Rect) {
70 let tab_height = 32.0;
71 let tab_width = if self.labels.is_empty() {
72 area.width
73 } else {
74 area.width / self.labels.len() as f32
75 };
76
77 let tab_bar = Rect::new(area.x, area.y, area.width, tab_height);
79 let bar_bg = self.bg_color.unwrap_or(Color::rgba(0.15, 0.15, 0.18, 1.0));
80 painter.fill_rect(tab_bar, bar_bg, 0.0);
81
82 let style = TextStyle {
83 font_size: self.font_size.unwrap_or(13.0),
84 color: self.fg_color.unwrap_or(Color::WHITE),
85 ..TextStyle::default()
86 };
87
88 for (i, label) in self.labels.iter().enumerate() {
89 let tab_rect = Rect::new(area.x + i as f32 * tab_width, area.y, tab_width, tab_height);
90
91 if i == self.active {
92 painter.fill_rect(tab_rect, Color::rgba(0.2, 0.2, 0.25, 1.0), 0.0);
93 let indicator =
95 Rect::new(tab_rect.x, tab_rect.y + tab_height - 2.0, tab_width, 2.0);
96 painter.fill_rect(indicator, Color::rgba(0.3, 0.6, 1.0, 1.0), 0.0);
97 }
98
99 let text_color = if i == self.active {
100 Color::WHITE
101 } else {
102 Color::rgba(0.6, 0.6, 0.7, 1.0)
103 };
104
105 let tab_style = TextStyle {
106 color: text_color,
107 ..style.clone()
108 };
109
110 let text_size = painter.measure_text(label, &tab_style);
111 painter.text(
112 Position::new(
113 tab_rect.x + (tab_width - text_size.width) * 0.5,
114 tab_rect.y + (tab_height - text_size.height) * 0.5,
115 ),
116 label,
117 &tab_style,
118 );
119 }
120
121 let content = Rect::new(
123 area.x,
124 area.y + tab_height,
125 area.width,
126 area.height - tab_height,
127 );
128 painter.fill_rect(content, Color::rgba(0.1, 0.1, 0.13, 1.0), 0.0);
129 }
130
131 fn ui_node(&self) -> UiNode {
132 UiNode::new("Tabs", SemanticRole::Tab).with_id(&self.id)
133 }
134}
135
136impl Discoverable for Tabs {
137 fn schema(&self) -> WidgetSchema {
138 WidgetSchema::new("Tabs", "A tabbed container", SemanticRole::Tab)
139 }
140
141 fn capabilities(&self) -> Vec<AgentCapability> {
142 vec![
143 AgentCapability::Focusable,
144 AgentCapability::Selectable {
145 multi_select: false,
146 item_count: self.labels.len(),
147 },
148 ]
149 }
150
151 fn actions(&self) -> Vec<AgentAction> {
152 vec![AgentAction::simple(
153 "select_tab",
154 "Switch to a tab by index",
155 true,
156 )]
157 }
158
159 fn semantic_role(&self) -> SemanticRole {
160 SemanticRole::Tab
161 }
162
163 fn agent_state(&self) -> serde_json::Value {
164 serde_json::json!({
165 "active": self.active,
166 "tab_count": self.labels.len(),
167 "labels": self.labels,
168 })
169 }
170
171 fn execute_action(
172 &mut self,
173 action: &str,
174 params: &serde_json::Value,
175 ) -> Result<serde_json::Value, String> {
176 match action {
177 "select_tab" => {
178 if let Some(idx) = params.get("index").and_then(|v| v.as_u64()) {
179 let idx = idx as usize;
180 if idx < self.labels.len() {
181 self.active = idx;
182 Ok(serde_json::json!({ "active": idx }))
183 } else {
184 Err("Tab index out of range".into())
185 }
186 } else {
187 Err("Missing 'index' parameter".into())
188 }
189 }
190 _ => Err(format!("Unknown action: {action}")),
191 }
192 }
193
194 fn agent_id(&self) -> Option<&str> {
195 Some(&self.id)
196 }
197}
198
199pub struct Panel {
201 pub id: String,
202 pub title: String,
203 pub collapsed: bool,
204 bg_color: Option<Color>,
205 fg_color: Option<Color>,
206 corner_radius: Option<f32>,
207 font_size: Option<f32>,
208 is_bold: bool,
209}
210
211impl Panel {
212 #[must_use]
213 pub fn new(id: impl Into<String>, title: impl Into<String>) -> Self {
214 Self {
215 id: id.into(),
216 title: title.into(),
217 collapsed: false,
218 bg_color: None,
219 fg_color: None,
220 corner_radius: None,
221 font_size: None,
222 is_bold: false,
223 }
224 }
225
226 #[must_use]
227 pub fn collapsed(mut self, collapsed: bool) -> Self {
228 self.collapsed = collapsed;
229 self
230 }
231
232 #[must_use]
233 pub fn bg(mut self, color: Color) -> Self {
234 self.bg_color = Some(color);
235 self
236 }
237
238 #[must_use]
239 pub fn fg(mut self, color: Color) -> Self {
240 self.fg_color = Some(color);
241 self
242 }
243
244 #[must_use]
245 pub fn rounded(mut self, radius: f32) -> Self {
246 self.corner_radius = Some(radius);
247 self
248 }
249
250 #[must_use]
251 pub fn text_size(mut self, size: f32) -> Self {
252 self.font_size = Some(size);
253 self
254 }
255
256 #[must_use]
257 pub fn bold(mut self) -> Self {
258 self.is_bold = true;
259 self
260 }
261}
262
263impl Widget for Panel {
264 fn draw(&self, painter: &mut dyn Painter, area: Rect) {
265 let header_height = 28.0;
266 let header = Rect::new(area.x, area.y, area.width, header_height);
267 let bg = self.bg_color.unwrap_or(Color::rgba(0.18, 0.18, 0.22, 1.0));
268 let radius = self.corner_radius.unwrap_or(3.0);
269 painter.fill_rect(header, bg, radius);
270
271 let text_color = self.fg_color.unwrap_or(Color::WHITE);
272 let style = TextStyle {
273 font_size: self.font_size.unwrap_or(13.0),
274 color: text_color,
275 ..TextStyle::default()
276 };
277
278 let arrow = if self.collapsed { "▸" } else { "▾" };
280 painter.text(
281 Position::new(
282 area.x + 8.0,
283 area.y + (header_height - style.font_size) * 0.5,
284 ),
285 arrow,
286 &style,
287 );
288
289 painter.text(
290 Position::new(
291 area.x + 24.0,
292 area.y + (header_height - style.font_size) * 0.5,
293 ),
294 &self.title,
295 &style,
296 );
297
298 if !self.collapsed {
299 let content = Rect::new(
300 area.x,
301 area.y + header_height,
302 area.width,
303 area.height - header_height,
304 );
305 painter.fill_rect(content, Color::rgba(0.1, 0.1, 0.13, 1.0), 0.0);
306 }
307 }
308
309 fn ui_node(&self) -> UiNode {
310 UiNode::new("Panel", SemanticRole::Container).with_id(&self.id)
311 }
312}
313
314impl Discoverable for Panel {
315 fn schema(&self) -> WidgetSchema {
316 WidgetSchema::new(
317 "Panel",
318 "A collapsible panel container",
319 SemanticRole::Container,
320 )
321 }
322
323 fn capabilities(&self) -> Vec<AgentCapability> {
324 vec![
325 AgentCapability::Focusable,
326 AgentCapability::Expandable {
327 expanded: !self.collapsed,
328 },
329 ]
330 }
331
332 fn actions(&self) -> Vec<AgentAction> {
333 vec![
334 AgentAction::simple("toggle", "Toggle panel collapse", true),
335 AgentAction::simple("expand", "Expand the panel", true),
336 AgentAction::simple("collapse", "Collapse the panel", true),
337 ]
338 }
339
340 fn semantic_role(&self) -> SemanticRole {
341 SemanticRole::Container
342 }
343
344 fn agent_state(&self) -> serde_json::Value {
345 serde_json::json!({
346 "title": self.title,
347 "collapsed": self.collapsed,
348 })
349 }
350
351 fn execute_action(
352 &mut self,
353 action: &str,
354 _params: &serde_json::Value,
355 ) -> Result<serde_json::Value, String> {
356 match action {
357 "toggle" => {
358 self.collapsed = !self.collapsed;
359 Ok(serde_json::json!({ "collapsed": self.collapsed }))
360 }
361 "expand" => {
362 self.collapsed = false;
363 Ok(serde_json::json!({ "collapsed": false }))
364 }
365 "collapse" => {
366 self.collapsed = true;
367 Ok(serde_json::json!({ "collapsed": true }))
368 }
369 _ => Err(format!("Unknown action: {action}")),
370 }
371 }
372
373 fn agent_id(&self) -> Option<&str> {
374 Some(&self.id)
375 }
376}