1use presentar_core::{
4 widget::{AccessibleRole, LayoutResult, TextStyle},
5 Canvas, Color, Constraints, Event, Rect, Size, TypeId, Widget,
6};
7use serde::{Deserialize, Serialize};
8use std::any::Any;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct TableColumn {
13 pub key: String,
15 pub header: String,
17 pub width: Option<f32>,
19 pub align: TextAlign,
21 pub sortable: bool,
23}
24
25impl TableColumn {
26 #[must_use]
28 pub fn new(key: impl Into<String>, header: impl Into<String>) -> Self {
29 Self {
30 key: key.into(),
31 header: header.into(),
32 width: None,
33 align: TextAlign::Left,
34 sortable: false,
35 }
36 }
37
38 #[must_use]
40 pub fn width(mut self, width: f32) -> Self {
41 self.width = Some(width.max(20.0));
42 self
43 }
44
45 #[must_use]
47 pub const fn align(mut self, align: TextAlign) -> Self {
48 self.align = align;
49 self
50 }
51
52 #[must_use]
54 pub const fn sortable(mut self) -> Self {
55 self.sortable = true;
56 self
57 }
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
62pub enum TextAlign {
63 #[default]
64 Left,
65 Center,
66 Right,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
71pub enum SortDirection {
72 Ascending,
73 Descending,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct TableSortChanged {
79 pub column: String,
81 pub direction: SortDirection,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct TableRowSelected {
88 pub index: usize,
90}
91
92#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
94pub enum CellValue {
95 Text(String),
97 Number(f64),
99 Bool(bool),
101 Empty,
103}
104
105impl CellValue {
106 #[must_use]
108 pub fn display(&self) -> String {
109 match self {
110 Self::Text(s) => s.clone(),
111 Self::Number(n) => format!("{n}"),
112 Self::Bool(b) => if *b { "Yes" } else { "No" }.to_string(),
113 Self::Empty => String::new(),
114 }
115 }
116}
117
118impl From<&str> for CellValue {
119 fn from(s: &str) -> Self {
120 Self::Text(s.to_string())
121 }
122}
123
124impl From<String> for CellValue {
125 fn from(s: String) -> Self {
126 Self::Text(s)
127 }
128}
129
130impl From<f64> for CellValue {
131 fn from(n: f64) -> Self {
132 Self::Number(n)
133 }
134}
135
136impl From<i32> for CellValue {
137 fn from(n: i32) -> Self {
138 Self::Number(f64::from(n))
139 }
140}
141
142impl From<bool> for CellValue {
143 fn from(b: bool) -> Self {
144 Self::Bool(b)
145 }
146}
147
148#[derive(Debug, Clone, Default, Serialize, Deserialize)]
150pub struct TableRow {
151 pub cells: std::collections::HashMap<String, CellValue>,
153}
154
155impl TableRow {
156 #[must_use]
158 pub fn new() -> Self {
159 Self::default()
160 }
161
162 #[must_use]
164 pub fn cell(mut self, key: impl Into<String>, value: impl Into<CellValue>) -> Self {
165 self.cells.insert(key.into(), value.into());
166 self
167 }
168
169 #[must_use]
171 pub fn get(&self, key: &str) -> Option<&CellValue> {
172 self.cells.get(key)
173 }
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct DataTable {
179 columns: Vec<TableColumn>,
181 rows: Vec<TableRow>,
183 row_height: f32,
185 header_height: f32,
187 sort_column: Option<String>,
189 sort_direction: SortDirection,
191 selected_row: Option<usize>,
193 selectable: bool,
195 striped: bool,
197 bordered: bool,
199 header_bg: Color,
201 row_bg: Color,
203 row_alt_bg: Color,
205 selected_bg: Color,
207 border_color: Color,
209 text_color: Color,
211 header_text_color: Color,
213 accessible_name_value: Option<String>,
215 test_id_value: Option<String>,
217 #[serde(skip)]
219 bounds: Rect,
220}
221
222impl Default for DataTable {
223 fn default() -> Self {
224 Self {
225 columns: Vec::new(),
226 rows: Vec::new(),
227 row_height: 40.0,
228 header_height: 44.0,
229 sort_column: None,
230 sort_direction: SortDirection::Ascending,
231 selected_row: None,
232 selectable: false,
233 striped: true,
234 bordered: true,
235 header_bg: Color::new(0.95, 0.95, 0.95, 1.0),
236 row_bg: Color::WHITE,
237 row_alt_bg: Color::new(0.98, 0.98, 0.98, 1.0),
238 selected_bg: Color::new(0.9, 0.95, 1.0, 1.0),
239 border_color: Color::new(0.85, 0.85, 0.85, 1.0),
240 text_color: Color::BLACK,
241 header_text_color: Color::new(0.2, 0.2, 0.2, 1.0),
242 accessible_name_value: None,
243 test_id_value: None,
244 bounds: Rect::default(),
245 }
246 }
247}
248
249impl DataTable {
250 #[must_use]
252 pub fn new() -> Self {
253 Self::default()
254 }
255
256 #[must_use]
258 pub fn column(mut self, column: TableColumn) -> Self {
259 self.columns.push(column);
260 self
261 }
262
263 #[must_use]
265 pub fn columns(mut self, columns: impl IntoIterator<Item = TableColumn>) -> Self {
266 self.columns.extend(columns);
267 self
268 }
269
270 #[must_use]
272 pub fn row(mut self, row: TableRow) -> Self {
273 self.rows.push(row);
274 self
275 }
276
277 #[must_use]
279 pub fn rows(mut self, rows: impl IntoIterator<Item = TableRow>) -> Self {
280 self.rows.extend(rows);
281 self
282 }
283
284 #[must_use]
286 pub fn row_height(mut self, height: f32) -> Self {
287 self.row_height = height.max(20.0);
288 self
289 }
290
291 #[must_use]
293 pub fn header_height(mut self, height: f32) -> Self {
294 self.header_height = height.max(20.0);
295 self
296 }
297
298 #[must_use]
300 pub const fn selectable(mut self, selectable: bool) -> Self {
301 self.selectable = selectable;
302 self
303 }
304
305 #[must_use]
307 pub const fn striped(mut self, striped: bool) -> Self {
308 self.striped = striped;
309 self
310 }
311
312 #[must_use]
314 pub const fn bordered(mut self, bordered: bool) -> Self {
315 self.bordered = bordered;
316 self
317 }
318
319 #[must_use]
321 pub const fn header_bg(mut self, color: Color) -> Self {
322 self.header_bg = color;
323 self
324 }
325
326 #[must_use]
328 pub const fn row_bg(mut self, color: Color) -> Self {
329 self.row_bg = color;
330 self
331 }
332
333 #[must_use]
335 pub const fn row_alt_bg(mut self, color: Color) -> Self {
336 self.row_alt_bg = color;
337 self
338 }
339
340 #[must_use]
342 pub const fn selected_bg(mut self, color: Color) -> Self {
343 self.selected_bg = color;
344 self
345 }
346
347 #[must_use]
349 pub const fn text_color(mut self, color: Color) -> Self {
350 self.text_color = color;
351 self
352 }
353
354 #[must_use]
356 pub fn accessible_name(mut self, name: impl Into<String>) -> Self {
357 self.accessible_name_value = Some(name.into());
358 self
359 }
360
361 #[must_use]
363 pub fn test_id(mut self, id: impl Into<String>) -> Self {
364 self.test_id_value = Some(id.into());
365 self
366 }
367
368 #[must_use]
370 pub fn column_count(&self) -> usize {
371 self.columns.len()
372 }
373
374 #[must_use]
376 pub fn row_count(&self) -> usize {
377 self.rows.len()
378 }
379
380 #[must_use]
382 pub fn get_columns(&self) -> &[TableColumn] {
383 &self.columns
384 }
385
386 #[must_use]
388 pub fn get_rows(&self) -> &[TableRow] {
389 &self.rows
390 }
391
392 #[must_use]
394 pub const fn get_selected_row(&self) -> Option<usize> {
395 self.selected_row
396 }
397
398 #[must_use]
400 pub fn get_sort_column(&self) -> Option<&str> {
401 self.sort_column.as_deref()
402 }
403
404 #[must_use]
406 pub const fn get_sort_direction(&self) -> SortDirection {
407 self.sort_direction
408 }
409
410 #[must_use]
412 pub fn is_empty(&self) -> bool {
413 self.rows.is_empty()
414 }
415
416 pub fn select_row(&mut self, index: Option<usize>) {
418 if let Some(idx) = index {
419 if idx < self.rows.len() {
420 self.selected_row = Some(idx);
421 }
422 } else {
423 self.selected_row = None;
424 }
425 }
426
427 pub fn set_sort(&mut self, column: impl Into<String>, direction: SortDirection) {
429 self.sort_column = Some(column.into());
430 self.sort_direction = direction;
431 }
432
433 pub fn clear(&mut self) {
435 self.rows.clear();
436 self.selected_row = None;
437 }
438
439 fn calculate_width(&self) -> f32 {
441 let mut total = 0.0;
442 for col in &self.columns {
443 total += col.width.unwrap_or(100.0);
444 }
445 total.max(100.0)
446 }
447
448 fn calculate_height(&self) -> f32 {
450 (self.rows.len() as f32).mul_add(self.row_height, self.header_height)
451 }
452
453 fn row_y(&self, index: usize) -> f32 {
455 (index as f32).mul_add(self.row_height, self.bounds.y + self.header_height)
456 }
457}
458
459impl Widget for DataTable {
460 fn type_id(&self) -> TypeId {
461 TypeId::of::<Self>()
462 }
463
464 fn measure(&self, constraints: Constraints) -> Size {
465 let preferred = Size::new(self.calculate_width(), self.calculate_height());
466 constraints.constrain(preferred)
467 }
468
469 fn layout(&mut self, bounds: Rect) -> LayoutResult {
470 self.bounds = bounds;
471 LayoutResult {
472 size: bounds.size(),
473 }
474 }
475
476 fn paint(&self, canvas: &mut dyn Canvas) {
477 let header_rect = Rect::new(
479 self.bounds.x,
480 self.bounds.y,
481 self.bounds.width,
482 self.header_height,
483 );
484 canvas.fill_rect(header_rect, self.header_bg);
485
486 let mut x = self.bounds.x;
488 for col in &self.columns {
489 let col_width = col.width.unwrap_or(100.0);
490 let text_style = TextStyle {
491 size: 14.0,
492 color: self.header_text_color,
493 weight: presentar_core::widget::FontWeight::Bold,
494 ..TextStyle::default()
495 };
496 canvas.draw_text(
497 &col.header,
498 presentar_core::Point::new(x + 8.0, self.bounds.y + self.header_height / 2.0),
499 &text_style,
500 );
501 x += col_width;
502 }
503
504 for (row_idx, row) in self.rows.iter().enumerate() {
506 let row_y = self.row_y(row_idx);
507
508 let bg_color = if self.selected_row == Some(row_idx) {
510 self.selected_bg
511 } else if self.striped && row_idx % 2 == 1 {
512 self.row_alt_bg
513 } else {
514 self.row_bg
515 };
516
517 let row_rect = Rect::new(self.bounds.x, row_y, self.bounds.width, self.row_height);
518 canvas.fill_rect(row_rect, bg_color);
519
520 let mut x = self.bounds.x;
522 for col in &self.columns {
523 let col_width = col.width.unwrap_or(100.0);
524 if let Some(cell) = row.get(&col.key) {
525 let text_style = TextStyle {
526 size: 14.0,
527 color: self.text_color,
528 ..TextStyle::default()
529 };
530 canvas.draw_text(
531 &cell.display(),
532 presentar_core::Point::new(x + 8.0, row_y + self.row_height / 2.0),
533 &text_style,
534 );
535 }
536 x += col_width;
537 }
538 }
539
540 if self.bordered {
542 let border_rect = Rect::new(
543 self.bounds.x,
544 self.bounds.y,
545 self.bounds.width,
546 self.calculate_height().min(self.bounds.height),
547 );
548 canvas.stroke_rect(border_rect, self.border_color, 1.0);
549 }
550 }
551
552 fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
553 None
555 }
556
557 fn children(&self) -> &[Box<dyn Widget>] {
558 &[]
559 }
560
561 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
562 &mut []
563 }
564
565 fn is_interactive(&self) -> bool {
566 self.selectable
567 }
568
569 fn is_focusable(&self) -> bool {
570 self.selectable
571 }
572
573 fn accessible_name(&self) -> Option<&str> {
574 self.accessible_name_value.as_deref()
575 }
576
577 fn accessible_role(&self) -> AccessibleRole {
578 AccessibleRole::Table
579 }
580
581 fn test_id(&self) -> Option<&str> {
582 self.test_id_value.as_deref()
583 }
584}
585
586#[cfg(test)]
587mod tests {
588 use super::*;
589
590 #[test]
593 fn test_table_column_new() {
594 let col = TableColumn::new("name", "Name");
595 assert_eq!(col.key, "name");
596 assert_eq!(col.header, "Name");
597 assert!(col.width.is_none());
598 assert!(!col.sortable);
599 }
600
601 #[test]
602 fn test_table_column_builder() {
603 let col = TableColumn::new("price", "Price")
604 .width(150.0)
605 .align(TextAlign::Right)
606 .sortable();
607 assert_eq!(col.width, Some(150.0));
608 assert_eq!(col.align, TextAlign::Right);
609 assert!(col.sortable);
610 }
611
612 #[test]
613 fn test_table_column_width_min() {
614 let col = TableColumn::new("id", "ID").width(5.0);
615 assert_eq!(col.width, Some(20.0));
616 }
617
618 #[test]
621 fn test_text_align_default() {
622 assert_eq!(TextAlign::default(), TextAlign::Left);
623 }
624
625 #[test]
628 fn test_cell_value_text() {
629 let cell = CellValue::Text("Hello".to_string());
630 assert_eq!(cell.display(), "Hello");
631 }
632
633 #[test]
634 fn test_cell_value_number() {
635 let cell = CellValue::Number(42.5);
636 assert_eq!(cell.display(), "42.5");
637 }
638
639 #[test]
640 fn test_cell_value_bool() {
641 assert_eq!(CellValue::Bool(true).display(), "Yes");
642 assert_eq!(CellValue::Bool(false).display(), "No");
643 }
644
645 #[test]
646 fn test_cell_value_empty() {
647 assert_eq!(CellValue::Empty.display(), "");
648 }
649
650 #[test]
651 fn test_cell_value_from_str() {
652 let cell: CellValue = "test".into();
653 assert_eq!(cell, CellValue::Text("test".to_string()));
654 }
655
656 #[test]
657 fn test_cell_value_from_f64() {
658 let cell: CellValue = 1.5f64.into();
659 assert_eq!(cell, CellValue::Number(1.5));
660 }
661
662 #[test]
663 fn test_cell_value_from_i32() {
664 let cell: CellValue = 42i32.into();
665 assert_eq!(cell, CellValue::Number(42.0));
666 }
667
668 #[test]
669 fn test_cell_value_from_bool() {
670 let cell: CellValue = true.into();
671 assert_eq!(cell, CellValue::Bool(true));
672 }
673
674 #[test]
677 fn test_table_row_new() {
678 let row = TableRow::new();
679 assert!(row.cells.is_empty());
680 }
681
682 #[test]
683 fn test_table_row_builder() {
684 let row = TableRow::new()
685 .cell("name", "Alice")
686 .cell("age", 30)
687 .cell("active", true);
688
689 assert_eq!(row.get("name"), Some(&CellValue::Text("Alice".to_string())));
690 assert_eq!(row.get("age"), Some(&CellValue::Number(30.0)));
691 assert_eq!(row.get("active"), Some(&CellValue::Bool(true)));
692 }
693
694 #[test]
695 fn test_table_row_get_missing() {
696 let row = TableRow::new();
697 assert!(row.get("nonexistent").is_none());
698 }
699
700 #[test]
703 fn test_data_table_new() {
704 let table = DataTable::new();
705 assert_eq!(table.column_count(), 0);
706 assert_eq!(table.row_count(), 0);
707 assert!(table.is_empty());
708 }
709
710 #[test]
711 fn test_data_table_builder() {
712 let table = DataTable::new()
713 .column(TableColumn::new("id", "ID"))
714 .column(TableColumn::new("name", "Name"))
715 .row(TableRow::new().cell("id", 1).cell("name", "Alice"))
716 .row(TableRow::new().cell("id", 2).cell("name", "Bob"))
717 .row_height(50.0)
718 .header_height(60.0)
719 .selectable(true)
720 .striped(true)
721 .bordered(true)
722 .accessible_name("User table")
723 .test_id("users-table");
724
725 assert_eq!(table.column_count(), 2);
726 assert_eq!(table.row_count(), 2);
727 assert!(!table.is_empty());
728 assert_eq!(Widget::accessible_name(&table), Some("User table"));
729 assert_eq!(Widget::test_id(&table), Some("users-table"));
730 }
731
732 #[test]
733 fn test_data_table_columns() {
734 let cols = vec![TableColumn::new("a", "A"), TableColumn::new("b", "B")];
735 let table = DataTable::new().columns(cols);
736 assert_eq!(table.column_count(), 2);
737 }
738
739 #[test]
740 fn test_data_table_rows() {
741 let rows = vec![
742 TableRow::new().cell("x", 1),
743 TableRow::new().cell("x", 2),
744 TableRow::new().cell("x", 3),
745 ];
746 let table = DataTable::new().rows(rows);
747 assert_eq!(table.row_count(), 3);
748 }
749
750 #[test]
753 fn test_data_table_select_row() {
754 let mut table = DataTable::new()
755 .row(TableRow::new())
756 .row(TableRow::new())
757 .selectable(true);
758
759 assert!(table.get_selected_row().is_none());
760 table.select_row(Some(1));
761 assert_eq!(table.get_selected_row(), Some(1));
762 table.select_row(None);
763 assert!(table.get_selected_row().is_none());
764 }
765
766 #[test]
767 fn test_data_table_select_row_out_of_bounds() {
768 let mut table = DataTable::new().row(TableRow::new());
769 table.select_row(Some(10));
770 assert!(table.get_selected_row().is_none());
771 }
772
773 #[test]
776 fn test_data_table_set_sort() {
777 let mut table = DataTable::new().column(TableColumn::new("name", "Name").sortable());
778
779 table.set_sort("name", SortDirection::Descending);
780 assert_eq!(table.get_sort_column(), Some("name"));
781 assert_eq!(table.get_sort_direction(), SortDirection::Descending);
782 }
783
784 #[test]
785 fn test_sort_direction() {
786 assert_ne!(SortDirection::Ascending, SortDirection::Descending);
787 }
788
789 #[test]
792 fn test_data_table_clear() {
793 let mut table = DataTable::new().row(TableRow::new()).row(TableRow::new());
794 table.select_row(Some(0));
795
796 table.clear();
797 assert!(table.is_empty());
798 assert!(table.get_selected_row().is_none());
799 }
800
801 #[test]
804 fn test_data_table_row_height_min() {
805 let table = DataTable::new().row_height(10.0);
806 assert_eq!(table.row_height, 20.0);
807 }
808
809 #[test]
810 fn test_data_table_header_height_min() {
811 let table = DataTable::new().header_height(10.0);
812 assert_eq!(table.header_height, 20.0);
813 }
814
815 #[test]
816 fn test_data_table_calculate_width() {
817 let table = DataTable::new()
818 .column(TableColumn::new("a", "A").width(100.0))
819 .column(TableColumn::new("b", "B").width(150.0));
820 assert_eq!(table.calculate_width(), 250.0);
821 }
822
823 #[test]
824 fn test_data_table_calculate_height() {
825 let table = DataTable::new()
826 .header_height(40.0)
827 .row_height(30.0)
828 .row(TableRow::new())
829 .row(TableRow::new());
830 assert_eq!(table.calculate_height(), 40.0 + 60.0);
831 }
832
833 #[test]
836 fn test_data_table_type_id() {
837 let table = DataTable::new();
838 assert_eq!(Widget::type_id(&table), TypeId::of::<DataTable>());
839 }
840
841 #[test]
842 fn test_data_table_measure() {
843 let table = DataTable::new()
844 .column(TableColumn::new("a", "A").width(200.0))
845 .header_height(40.0)
846 .row_height(30.0)
847 .row(TableRow::new())
848 .row(TableRow::new());
849
850 let size = table.measure(Constraints::loose(Size::new(1000.0, 1000.0)));
851 assert_eq!(size.width, 200.0);
852 assert_eq!(size.height, 100.0); }
854
855 #[test]
856 fn test_data_table_layout() {
857 let mut table = DataTable::new();
858 let bounds = Rect::new(10.0, 20.0, 500.0, 300.0);
859 let result = table.layout(bounds);
860 assert_eq!(result.size, Size::new(500.0, 300.0));
861 assert_eq!(table.bounds, bounds);
862 }
863
864 #[test]
865 fn test_data_table_children() {
866 let table = DataTable::new();
867 assert!(table.children().is_empty());
868 }
869
870 #[test]
871 fn test_data_table_is_interactive() {
872 let table = DataTable::new();
873 assert!(!table.is_interactive());
874
875 let table = DataTable::new().selectable(true);
876 assert!(table.is_interactive());
877 }
878
879 #[test]
880 fn test_data_table_is_focusable() {
881 let table = DataTable::new();
882 assert!(!table.is_focusable());
883
884 let table = DataTable::new().selectable(true);
885 assert!(table.is_focusable());
886 }
887
888 #[test]
889 fn test_data_table_accessible_role() {
890 let table = DataTable::new();
891 assert_eq!(table.accessible_role(), AccessibleRole::Table);
892 }
893
894 #[test]
895 fn test_data_table_accessible_name() {
896 let table = DataTable::new().accessible_name("Products table");
897 assert_eq!(Widget::accessible_name(&table), Some("Products table"));
898 }
899
900 #[test]
901 fn test_data_table_test_id() {
902 let table = DataTable::new().test_id("inventory-grid");
903 assert_eq!(Widget::test_id(&table), Some("inventory-grid"));
904 }
905
906 #[test]
909 fn test_table_sort_changed() {
910 let msg = TableSortChanged {
911 column: "price".to_string(),
912 direction: SortDirection::Descending,
913 };
914 assert_eq!(msg.column, "price");
915 assert_eq!(msg.direction, SortDirection::Descending);
916 }
917
918 #[test]
919 fn test_table_row_selected() {
920 let msg = TableRowSelected { index: 5 };
921 assert_eq!(msg.index, 5);
922 }
923
924 #[test]
927 fn test_row_y() {
928 let mut table = DataTable::new()
929 .header_height(50.0)
930 .row_height(40.0)
931 .row(TableRow::new())
932 .row(TableRow::new());
933 table.bounds = Rect::new(0.0, 10.0, 100.0, 200.0);
934
935 assert_eq!(table.row_y(0), 60.0); assert_eq!(table.row_y(1), 100.0); }
938}