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 List {
12 pub id: String,
13 pub items: Vec<String>,
14 pub selected: Option<usize>,
15 pub scroll_offset: f32,
16 pub item_height: f32,
17 bg_color: Option<Color>,
18 fg_color: Option<Color>,
19 corner_radius: Option<f32>,
20 font_size: Option<f32>,
21 is_bold: bool,
22}
23
24impl List {
25 #[must_use]
26 pub fn new(id: impl Into<String>, items: Vec<String>) -> Self {
27 Self {
28 id: id.into(),
29 items,
30 selected: None,
31 scroll_offset: 0.0,
32 item_height: 28.0,
33 bg_color: None,
34 fg_color: None,
35 corner_radius: None,
36 font_size: None,
37 is_bold: false,
38 }
39 }
40
41 #[must_use]
42 pub fn selected(mut self, index: usize) -> Self {
43 self.selected = Some(index);
44 self
45 }
46
47 #[must_use]
48 pub fn bg(mut self, color: Color) -> Self {
49 self.bg_color = Some(color);
50 self
51 }
52
53 #[must_use]
54 pub fn fg(mut self, color: Color) -> Self {
55 self.fg_color = Some(color);
56 self
57 }
58
59 #[must_use]
60 pub fn rounded(mut self, radius: f32) -> Self {
61 self.corner_radius = Some(radius);
62 self
63 }
64
65 #[must_use]
66 pub fn text_size(mut self, size: f32) -> Self {
67 self.font_size = Some(size);
68 self
69 }
70
71 #[must_use]
72 pub fn bold(mut self) -> Self {
73 self.is_bold = true;
74 self
75 }
76}
77
78impl Widget for List {
79 fn draw(&self, painter: &mut dyn Painter, area: Rect) {
80 let bg = self.bg_color.unwrap_or(Color::rgba(0.12, 0.12, 0.15, 1.0));
81 let radius = self.corner_radius.unwrap_or(3.0);
82 painter.fill_rect(area, bg, radius);
83
84 let text_color = self.fg_color.unwrap_or(Color::WHITE);
85 let style = TextStyle {
86 font_size: self.font_size.unwrap_or(14.0),
87 color: text_color,
88 ..TextStyle::default()
89 };
90
91 let padding = 6.0;
92 let visible_start = (self.scroll_offset / self.item_height).floor() as usize;
93 let visible_count = (area.height / self.item_height).ceil() as usize + 1;
94
95 for i in visible_start..self.items.len().min(visible_start + visible_count) {
96 let y = area.y + i as f32 * self.item_height - self.scroll_offset;
97 if y + self.item_height < area.y || y > area.y + area.height {
98 continue;
99 }
100
101 let item_rect = Rect::new(area.x, y, area.width, self.item_height);
102
103 if self.selected == Some(i) {
104 painter.fill_rect(item_rect, Color::rgba(0.2, 0.4, 0.7, 0.5), 0.0);
105 }
106
107 let text_color = if self.selected == Some(i) {
108 Color::WHITE
109 } else {
110 style.color
111 };
112
113 let text_style = TextStyle {
114 color: text_color,
115 ..style.clone()
116 };
117
118 painter.text(
119 Position::new(
120 area.x + padding,
121 y + (self.item_height - style.font_size) * 0.5,
122 ),
123 &self.items[i],
124 &text_style,
125 );
126 }
127 }
128
129 fn ui_node(&self) -> UiNode {
130 UiNode::new("List", SemanticRole::Selection).with_id(&self.id)
131 }
132}
133
134impl Discoverable for List {
135 fn schema(&self) -> WidgetSchema {
136 WidgetSchema::new(
137 "List",
138 "A scrollable list of items",
139 SemanticRole::Selection,
140 )
141 }
142
143 fn capabilities(&self) -> Vec<AgentCapability> {
144 vec![
145 AgentCapability::Focusable,
146 AgentCapability::Scrollable {
147 vertical: true,
148 horizontal: false,
149 },
150 AgentCapability::Selectable {
151 multi_select: false,
152 item_count: self.items.len(),
153 },
154 ]
155 }
156
157 fn actions(&self) -> Vec<AgentAction> {
158 vec![
159 AgentAction::simple("select", "Select an item by index", true),
160 AgentAction::simple("scroll", "Scroll the list", true),
161 ]
162 }
163
164 fn semantic_role(&self) -> SemanticRole {
165 SemanticRole::Selection
166 }
167
168 fn agent_state(&self) -> serde_json::Value {
169 serde_json::json!({
170 "item_count": self.items.len(),
171 "selected": self.selected,
172 })
173 }
174
175 fn execute_action(
176 &mut self,
177 action: &str,
178 params: &serde_json::Value,
179 ) -> Result<serde_json::Value, String> {
180 match action {
181 "select" => {
182 if let Some(idx) = params.get("index").and_then(|v| v.as_u64()) {
183 let idx = idx as usize;
184 if idx < self.items.len() {
185 self.selected = Some(idx);
186 Ok(serde_json::json!({ "selected": idx }))
187 } else {
188 Err("Index out of range".into())
189 }
190 } else {
191 Err("Missing 'index' parameter".into())
192 }
193 }
194 _ => Err(format!("Unknown action: {action}")),
195 }
196 }
197
198 fn agent_id(&self) -> Option<&str> {
199 Some(&self.id)
200 }
201}
202
203pub struct Table {
205 pub id: String,
206 pub columns: Vec<String>,
207 pub rows: Vec<Vec<String>>,
208 pub selected_row: Option<usize>,
209 pub scroll_offset: f32,
210 pub row_height: f32,
211 pub header_height: f32,
212 bg_color: Option<Color>,
213 fg_color: Option<Color>,
214 corner_radius: Option<f32>,
215 font_size: Option<f32>,
216 is_bold: bool,
217}
218
219impl Table {
220 #[must_use]
221 pub fn new(id: impl Into<String>, columns: Vec<String>, rows: Vec<Vec<String>>) -> Self {
222 Self {
223 id: id.into(),
224 columns,
225 rows,
226 selected_row: None,
227 scroll_offset: 0.0,
228 row_height: 28.0,
229 header_height: 32.0,
230 bg_color: None,
231 fg_color: None,
232 corner_radius: None,
233 font_size: None,
234 is_bold: false,
235 }
236 }
237
238 #[must_use]
239 pub fn bg(mut self, color: Color) -> Self {
240 self.bg_color = Some(color);
241 self
242 }
243
244 #[must_use]
245 pub fn fg(mut self, color: Color) -> Self {
246 self.fg_color = Some(color);
247 self
248 }
249
250 #[must_use]
251 pub fn rounded(mut self, radius: f32) -> Self {
252 self.corner_radius = Some(radius);
253 self
254 }
255
256 #[must_use]
257 pub fn text_size(mut self, size: f32) -> Self {
258 self.font_size = Some(size);
259 self
260 }
261
262 #[must_use]
263 pub fn bold(mut self) -> Self {
264 self.is_bold = true;
265 self
266 }
267}
268
269impl Widget for Table {
270 fn draw(&self, painter: &mut dyn Painter, area: Rect) {
271 let bg = self.bg_color.unwrap_or(Color::rgba(0.1, 0.1, 0.13, 1.0));
272 let radius = self.corner_radius.unwrap_or(3.0);
273 painter.fill_rect(area, bg, radius);
274
275 let col_count = self.columns.len().max(1);
276 let col_width = area.width / col_count as f32;
277 let fs = self.font_size.unwrap_or(13.0);
278
279 let header_style = TextStyle {
280 font_size: fs,
281 color: self.fg_color.unwrap_or(Color::rgba(0.7, 0.7, 0.8, 1.0)),
282 ..TextStyle::default()
283 };
284
285 let header_rect = Rect::new(area.x, area.y, area.width, self.header_height);
287 painter.fill_rect(header_rect, Color::rgba(0.15, 0.15, 0.2, 1.0), 0.0);
288
289 for (i, col) in self.columns.iter().enumerate() {
290 painter.text(
291 Position::new(
292 area.x + i as f32 * col_width + 6.0,
293 area.y + (self.header_height - header_style.font_size) * 0.5,
294 ),
295 col,
296 &header_style,
297 );
298 }
299
300 let row_style = TextStyle {
302 font_size: fs,
303 color: self.fg_color.unwrap_or(Color::WHITE),
304 ..TextStyle::default()
305 };
306
307 let body_y = area.y + self.header_height;
308 for (ri, row) in self.rows.iter().enumerate() {
309 let y = body_y + ri as f32 * self.row_height - self.scroll_offset;
310 if y + self.row_height < body_y || y > area.y + area.height {
311 continue;
312 }
313
314 if self.selected_row == Some(ri) {
315 let row_rect = Rect::new(area.x, y, area.width, self.row_height);
316 painter.fill_rect(row_rect, Color::rgba(0.2, 0.4, 0.7, 0.4), 0.0);
317 }
318
319 for (ci, cell) in row.iter().enumerate().take(col_count) {
320 painter.text(
321 Position::new(
322 area.x + ci as f32 * col_width + 6.0,
323 y + (self.row_height - row_style.font_size) * 0.5,
324 ),
325 cell,
326 &row_style,
327 );
328 }
329 }
330 }
331
332 fn ui_node(&self) -> UiNode {
333 UiNode::new("Table", SemanticRole::DataVisualization).with_id(&self.id)
334 }
335}
336
337impl Discoverable for Table {
338 fn schema(&self) -> WidgetSchema {
339 WidgetSchema::new(
340 "Table",
341 "A data table with headers and rows",
342 SemanticRole::DataVisualization,
343 )
344 }
345
346 fn capabilities(&self) -> Vec<AgentCapability> {
347 vec![
348 AgentCapability::Focusable,
349 AgentCapability::Scrollable {
350 vertical: true,
351 horizontal: false,
352 },
353 AgentCapability::Selectable {
354 multi_select: false,
355 item_count: self.rows.len(),
356 },
357 AgentCapability::Sortable {
358 columns: self.columns.clone(),
359 },
360 ]
361 }
362
363 fn actions(&self) -> Vec<AgentAction> {
364 vec![
365 AgentAction::simple("select_row", "Select a table row", true),
366 AgentAction::simple("scroll", "Scroll the table", true),
367 AgentAction::simple("sort", "Sort by column", false),
368 ]
369 }
370
371 fn semantic_role(&self) -> SemanticRole {
372 SemanticRole::DataVisualization
373 }
374
375 fn agent_state(&self) -> serde_json::Value {
376 serde_json::json!({
377 "columns": self.columns,
378 "row_count": self.rows.len(),
379 "selected_row": self.selected_row,
380 })
381 }
382
383 fn execute_action(
384 &mut self,
385 action: &str,
386 params: &serde_json::Value,
387 ) -> Result<serde_json::Value, String> {
388 match action {
389 "select_row" => {
390 if let Some(idx) = params.get("index").and_then(|v| v.as_u64()) {
391 let idx = idx as usize;
392 if idx < self.rows.len() {
393 self.selected_row = Some(idx);
394 Ok(serde_json::json!({ "selected_row": idx }))
395 } else {
396 Err("Index out of range".into())
397 }
398 } else {
399 Err("Missing 'index' parameter".into())
400 }
401 }
402 _ => Err(format!("Unknown action: {action}")),
403 }
404 }
405
406 fn agent_id(&self) -> Option<&str> {
407 Some(&self.id)
408 }
409}