1use presentar_core::{
4 widget::{AccessibleRole, LayoutResult, TextStyle},
5 Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event, Rect,
6 Size, TypeId, Widget,
7};
8use serde::{Deserialize, Serialize};
9use std::any::Any;
10use std::time::Duration;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct TableColumn {
15 pub key: String,
17 pub header: String,
19 pub width: Option<f32>,
21 pub align: TextAlign,
23 pub sortable: bool,
25}
26
27impl TableColumn {
28 #[must_use]
30 pub fn new(key: impl Into<String>, header: impl Into<String>) -> Self {
31 Self {
32 key: key.into(),
33 header: header.into(),
34 width: None,
35 align: TextAlign::Left,
36 sortable: false,
37 }
38 }
39
40 #[must_use]
42 pub fn width(mut self, width: f32) -> Self {
43 self.width = Some(width.max(20.0));
44 self
45 }
46
47 #[must_use]
49 pub const fn align(mut self, align: TextAlign) -> Self {
50 self.align = align;
51 self
52 }
53
54 #[must_use]
56 pub const fn sortable(mut self) -> Self {
57 self.sortable = true;
58 self
59 }
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
64pub enum TextAlign {
65 #[default]
66 Left,
67 Center,
68 Right,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
73pub enum SortDirection {
74 Ascending,
75 Descending,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct TableSortChanged {
81 pub column: String,
83 pub direction: SortDirection,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct TableRowSelected {
90 pub index: usize,
92}
93
94#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
96pub enum CellValue {
97 Text(String),
99 Number(f64),
101 Bool(bool),
103 Empty,
105}
106
107impl CellValue {
108 #[must_use]
110 pub fn display(&self) -> String {
111 match self {
112 Self::Text(s) => s.clone(),
113 Self::Number(n) => format!("{n}"),
114 Self::Bool(b) => if *b { "Yes" } else { "No" }.to_string(),
115 Self::Empty => String::new(),
116 }
117 }
118}
119
120impl From<&str> for CellValue {
121 fn from(s: &str) -> Self {
122 Self::Text(s.to_string())
123 }
124}
125
126impl From<String> for CellValue {
127 fn from(s: String) -> Self {
128 Self::Text(s)
129 }
130}
131
132impl From<f64> for CellValue {
133 fn from(n: f64) -> Self {
134 Self::Number(n)
135 }
136}
137
138impl From<i32> for CellValue {
139 fn from(n: i32) -> Self {
140 Self::Number(f64::from(n))
141 }
142}
143
144impl From<bool> for CellValue {
145 fn from(b: bool) -> Self {
146 Self::Bool(b)
147 }
148}
149
150#[derive(Debug, Clone, Default, Serialize, Deserialize)]
152pub struct TableRow {
153 pub cells: std::collections::HashMap<String, CellValue>,
155}
156
157impl TableRow {
158 #[must_use]
160 pub fn new() -> Self {
161 Self::default()
162 }
163
164 #[must_use]
166 pub fn cell(mut self, key: impl Into<String>, value: impl Into<CellValue>) -> Self {
167 self.cells.insert(key.into(), value.into());
168 self
169 }
170
171 #[must_use]
173 pub fn get(&self, key: &str) -> Option<&CellValue> {
174 self.cells.get(key)
175 }
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct DataTable {
181 columns: Vec<TableColumn>,
183 rows: Vec<TableRow>,
185 row_height: f32,
187 header_height: f32,
189 sort_column: Option<String>,
191 sort_direction: SortDirection,
193 selected_row: Option<usize>,
195 selectable: bool,
197 striped: bool,
199 bordered: bool,
201 header_bg: Color,
203 row_bg: Color,
205 row_alt_bg: Color,
207 selected_bg: Color,
209 border_color: Color,
211 text_color: Color,
213 header_text_color: Color,
215 accessible_name_value: Option<String>,
217 test_id_value: Option<String>,
219 #[serde(skip)]
221 bounds: Rect,
222}
223
224impl Default for DataTable {
225 fn default() -> Self {
226 Self {
227 columns: Vec::new(),
228 rows: Vec::new(),
229 row_height: 40.0,
230 header_height: 44.0,
231 sort_column: None,
232 sort_direction: SortDirection::Ascending,
233 selected_row: None,
234 selectable: false,
235 striped: true,
236 bordered: true,
237 header_bg: Color::new(0.95, 0.95, 0.95, 1.0),
238 row_bg: Color::WHITE,
239 row_alt_bg: Color::new(0.98, 0.98, 0.98, 1.0),
240 selected_bg: Color::new(0.9, 0.95, 1.0, 1.0),
241 border_color: Color::new(0.85, 0.85, 0.85, 1.0),
242 text_color: Color::BLACK,
243 header_text_color: Color::new(0.2, 0.2, 0.2, 1.0),
244 accessible_name_value: None,
245 test_id_value: None,
246 bounds: Rect::default(),
247 }
248 }
249}
250
251impl DataTable {
252 #[must_use]
254 pub fn new() -> Self {
255 Self::default()
256 }
257
258 #[must_use]
260 pub fn column(mut self, column: TableColumn) -> Self {
261 self.columns.push(column);
262 self
263 }
264
265 #[must_use]
267 pub fn columns(mut self, columns: impl IntoIterator<Item = TableColumn>) -> Self {
268 self.columns.extend(columns);
269 self
270 }
271
272 #[must_use]
274 pub fn row(mut self, row: TableRow) -> Self {
275 self.rows.push(row);
276 self
277 }
278
279 #[must_use]
281 pub fn rows(mut self, rows: impl IntoIterator<Item = TableRow>) -> Self {
282 self.rows.extend(rows);
283 self
284 }
285
286 #[must_use]
288 pub fn row_height(mut self, height: f32) -> Self {
289 self.row_height = height.max(20.0);
290 self
291 }
292
293 #[must_use]
295 pub fn header_height(mut self, height: f32) -> Self {
296 self.header_height = height.max(20.0);
297 self
298 }
299
300 #[must_use]
302 pub const fn selectable(mut self, selectable: bool) -> Self {
303 self.selectable = selectable;
304 self
305 }
306
307 #[must_use]
309 pub const fn striped(mut self, striped: bool) -> Self {
310 self.striped = striped;
311 self
312 }
313
314 #[must_use]
316 pub const fn bordered(mut self, bordered: bool) -> Self {
317 self.bordered = bordered;
318 self
319 }
320
321 #[must_use]
323 pub const fn header_bg(mut self, color: Color) -> Self {
324 self.header_bg = color;
325 self
326 }
327
328 #[must_use]
330 pub const fn row_bg(mut self, color: Color) -> Self {
331 self.row_bg = color;
332 self
333 }
334
335 #[must_use]
337 pub const fn row_alt_bg(mut self, color: Color) -> Self {
338 self.row_alt_bg = color;
339 self
340 }
341
342 #[must_use]
344 pub const fn selected_bg(mut self, color: Color) -> Self {
345 self.selected_bg = color;
346 self
347 }
348
349 #[must_use]
351 pub const fn text_color(mut self, color: Color) -> Self {
352 self.text_color = color;
353 self
354 }
355
356 #[must_use]
358 pub fn accessible_name(mut self, name: impl Into<String>) -> Self {
359 self.accessible_name_value = Some(name.into());
360 self
361 }
362
363 #[must_use]
365 pub fn test_id(mut self, id: impl Into<String>) -> Self {
366 self.test_id_value = Some(id.into());
367 self
368 }
369
370 #[must_use]
372 pub fn column_count(&self) -> usize {
373 self.columns.len()
374 }
375
376 #[must_use]
378 pub fn row_count(&self) -> usize {
379 self.rows.len()
380 }
381
382 #[must_use]
384 pub fn get_columns(&self) -> &[TableColumn] {
385 &self.columns
386 }
387
388 #[must_use]
390 pub fn get_rows(&self) -> &[TableRow] {
391 &self.rows
392 }
393
394 #[must_use]
396 pub const fn get_selected_row(&self) -> Option<usize> {
397 self.selected_row
398 }
399
400 #[must_use]
402 pub fn get_sort_column(&self) -> Option<&str> {
403 self.sort_column.as_deref()
404 }
405
406 #[must_use]
408 pub const fn get_sort_direction(&self) -> SortDirection {
409 self.sort_direction
410 }
411
412 #[must_use]
414 pub fn is_empty(&self) -> bool {
415 self.rows.is_empty()
416 }
417
418 pub fn select_row(&mut self, index: Option<usize>) {
420 if let Some(idx) = index {
421 if idx < self.rows.len() {
422 self.selected_row = Some(idx);
423 }
424 } else {
425 self.selected_row = None;
426 }
427 }
428
429 pub fn set_sort(&mut self, column: impl Into<String>, direction: SortDirection) {
431 self.sort_column = Some(column.into());
432 self.sort_direction = direction;
433 }
434
435 pub fn clear(&mut self) {
437 self.rows.clear();
438 self.selected_row = None;
439 }
440
441 fn calculate_width(&self) -> f32 {
443 let mut total = 0.0;
444 for col in &self.columns {
445 total += col.width.unwrap_or(100.0);
446 }
447 total.max(100.0)
448 }
449
450 fn calculate_height(&self) -> f32 {
452 (self.rows.len() as f32).mul_add(self.row_height, self.header_height)
453 }
454
455 fn row_y(&self, index: usize) -> f32 {
457 (index as f32).mul_add(self.row_height, self.bounds.y + self.header_height)
458 }
459}
460
461impl Widget for DataTable {
462 fn type_id(&self) -> TypeId {
463 TypeId::of::<Self>()
464 }
465
466 fn measure(&self, constraints: Constraints) -> Size {
467 let preferred = Size::new(self.calculate_width(), self.calculate_height());
468 constraints.constrain(preferred)
469 }
470
471 fn layout(&mut self, bounds: Rect) -> LayoutResult {
472 self.bounds = bounds;
473 LayoutResult {
474 size: bounds.size(),
475 }
476 }
477
478 fn paint(&self, canvas: &mut dyn Canvas) {
479 let header_rect = Rect::new(
481 self.bounds.x,
482 self.bounds.y,
483 self.bounds.width,
484 self.header_height,
485 );
486 canvas.fill_rect(header_rect, self.header_bg);
487
488 let mut x = self.bounds.x;
490 for col in &self.columns {
491 let col_width = col.width.unwrap_or(100.0);
492 let text_style = TextStyle {
493 size: 14.0,
494 color: self.header_text_color,
495 weight: presentar_core::widget::FontWeight::Bold,
496 ..TextStyle::default()
497 };
498 canvas.draw_text(
499 &col.header,
500 presentar_core::Point::new(x + 8.0, self.bounds.y + self.header_height / 2.0),
501 &text_style,
502 );
503 x += col_width;
504 }
505
506 for (row_idx, row) in self.rows.iter().enumerate() {
508 let row_y = self.row_y(row_idx);
509
510 let bg_color = if self.selected_row == Some(row_idx) {
512 self.selected_bg
513 } else if self.striped && row_idx % 2 == 1 {
514 self.row_alt_bg
515 } else {
516 self.row_bg
517 };
518
519 let row_rect = Rect::new(self.bounds.x, row_y, self.bounds.width, self.row_height);
520 canvas.fill_rect(row_rect, bg_color);
521
522 let mut x = self.bounds.x;
524 for col in &self.columns {
525 let col_width = col.width.unwrap_or(100.0);
526 if let Some(cell) = row.get(&col.key) {
527 let text_style = TextStyle {
528 size: 14.0,
529 color: self.text_color,
530 ..TextStyle::default()
531 };
532 canvas.draw_text(
533 &cell.display(),
534 presentar_core::Point::new(x + 8.0, row_y + self.row_height / 2.0),
535 &text_style,
536 );
537 }
538 x += col_width;
539 }
540 }
541
542 if self.bordered {
544 let border_rect = Rect::new(
545 self.bounds.x,
546 self.bounds.y,
547 self.bounds.width,
548 self.calculate_height().min(self.bounds.height),
549 );
550 canvas.stroke_rect(border_rect, self.border_color, 1.0);
551 }
552 }
553
554 fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
555 None
557 }
558
559 fn children(&self) -> &[Box<dyn Widget>] {
560 &[]
561 }
562
563 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
564 &mut []
565 }
566
567 fn is_interactive(&self) -> bool {
568 self.selectable
569 }
570
571 fn is_focusable(&self) -> bool {
572 self.selectable
573 }
574
575 fn accessible_name(&self) -> Option<&str> {
576 self.accessible_name_value.as_deref()
577 }
578
579 fn accessible_role(&self) -> AccessibleRole {
580 AccessibleRole::Table
581 }
582
583 fn test_id(&self) -> Option<&str> {
584 self.test_id_value.as_deref()
585 }
586}
587
588impl Brick for DataTable {
590 fn brick_name(&self) -> &'static str {
591 "DataTable"
592 }
593
594 fn assertions(&self) -> &[BrickAssertion] {
595 &[BrickAssertion::MaxLatencyMs(16)]
596 }
597
598 fn budget(&self) -> BrickBudget {
599 BrickBudget::uniform(16)
600 }
601
602 fn verify(&self) -> BrickVerification {
603 BrickVerification {
604 passed: self.assertions().to_vec(),
605 failed: vec![],
606 verification_time: Duration::from_micros(10),
607 }
608 }
609
610 fn to_html(&self) -> String {
611 let test_id = self.test_id_value.as_deref().unwrap_or("data-table");
612 format!(r#"<table class="brick-data-table" data-testid="{test_id}" role="table"></table>"#)
613 }
614
615 fn to_css(&self) -> String {
616 ".brick-data-table { display: table; width: 100%; }".into()
617 }
618
619 fn test_id(&self) -> Option<&str> {
620 self.test_id_value.as_deref()
621 }
622}
623
624#[cfg(test)]
625mod tests {
626 use super::*;
627
628 #[test]
631 fn test_table_column_new() {
632 let col = TableColumn::new("name", "Name");
633 assert_eq!(col.key, "name");
634 assert_eq!(col.header, "Name");
635 assert!(col.width.is_none());
636 assert!(!col.sortable);
637 }
638
639 #[test]
640 fn test_table_column_builder() {
641 let col = TableColumn::new("price", "Price")
642 .width(150.0)
643 .align(TextAlign::Right)
644 .sortable();
645 assert_eq!(col.width, Some(150.0));
646 assert_eq!(col.align, TextAlign::Right);
647 assert!(col.sortable);
648 }
649
650 #[test]
651 fn test_table_column_width_min() {
652 let col = TableColumn::new("id", "ID").width(5.0);
653 assert_eq!(col.width, Some(20.0));
654 }
655
656 #[test]
659 fn test_text_align_default() {
660 assert_eq!(TextAlign::default(), TextAlign::Left);
661 }
662
663 #[test]
666 fn test_cell_value_text() {
667 let cell = CellValue::Text("Hello".to_string());
668 assert_eq!(cell.display(), "Hello");
669 }
670
671 #[test]
672 fn test_cell_value_number() {
673 let cell = CellValue::Number(42.5);
674 assert_eq!(cell.display(), "42.5");
675 }
676
677 #[test]
678 fn test_cell_value_bool() {
679 assert_eq!(CellValue::Bool(true).display(), "Yes");
680 assert_eq!(CellValue::Bool(false).display(), "No");
681 }
682
683 #[test]
684 fn test_cell_value_empty() {
685 assert_eq!(CellValue::Empty.display(), "");
686 }
687
688 #[test]
689 fn test_cell_value_from_str() {
690 let cell: CellValue = "test".into();
691 assert_eq!(cell, CellValue::Text("test".to_string()));
692 }
693
694 #[test]
695 fn test_cell_value_from_f64() {
696 let cell: CellValue = 1.5f64.into();
697 assert_eq!(cell, CellValue::Number(1.5));
698 }
699
700 #[test]
701 fn test_cell_value_from_i32() {
702 let cell: CellValue = 42i32.into();
703 assert_eq!(cell, CellValue::Number(42.0));
704 }
705
706 #[test]
707 fn test_cell_value_from_bool() {
708 let cell: CellValue = true.into();
709 assert_eq!(cell, CellValue::Bool(true));
710 }
711
712 #[test]
715 fn test_table_row_new() {
716 let row = TableRow::new();
717 assert!(row.cells.is_empty());
718 }
719
720 #[test]
721 fn test_table_row_builder() {
722 let row = TableRow::new()
723 .cell("name", "Alice")
724 .cell("age", 30)
725 .cell("active", true);
726
727 assert_eq!(row.get("name"), Some(&CellValue::Text("Alice".to_string())));
728 assert_eq!(row.get("age"), Some(&CellValue::Number(30.0)));
729 assert_eq!(row.get("active"), Some(&CellValue::Bool(true)));
730 }
731
732 #[test]
733 fn test_table_row_get_missing() {
734 let row = TableRow::new();
735 assert!(row.get("nonexistent").is_none());
736 }
737
738 #[test]
741 fn test_data_table_new() {
742 let table = DataTable::new();
743 assert_eq!(table.column_count(), 0);
744 assert_eq!(table.row_count(), 0);
745 assert!(table.is_empty());
746 }
747
748 #[test]
749 fn test_data_table_builder() {
750 let table = DataTable::new()
751 .column(TableColumn::new("id", "ID"))
752 .column(TableColumn::new("name", "Name"))
753 .row(TableRow::new().cell("id", 1).cell("name", "Alice"))
754 .row(TableRow::new().cell("id", 2).cell("name", "Bob"))
755 .row_height(50.0)
756 .header_height(60.0)
757 .selectable(true)
758 .striped(true)
759 .bordered(true)
760 .accessible_name("User table")
761 .test_id("users-table");
762
763 assert_eq!(table.column_count(), 2);
764 assert_eq!(table.row_count(), 2);
765 assert!(!table.is_empty());
766 assert_eq!(Widget::accessible_name(&table), Some("User table"));
767 assert_eq!(Widget::test_id(&table), Some("users-table"));
768 }
769
770 #[test]
771 fn test_data_table_columns() {
772 let cols = vec![TableColumn::new("a", "A"), TableColumn::new("b", "B")];
773 let table = DataTable::new().columns(cols);
774 assert_eq!(table.column_count(), 2);
775 }
776
777 #[test]
778 fn test_data_table_rows() {
779 let rows = vec![
780 TableRow::new().cell("x", 1),
781 TableRow::new().cell("x", 2),
782 TableRow::new().cell("x", 3),
783 ];
784 let table = DataTable::new().rows(rows);
785 assert_eq!(table.row_count(), 3);
786 }
787
788 #[test]
791 fn test_data_table_select_row() {
792 let mut table = DataTable::new()
793 .row(TableRow::new())
794 .row(TableRow::new())
795 .selectable(true);
796
797 assert!(table.get_selected_row().is_none());
798 table.select_row(Some(1));
799 assert_eq!(table.get_selected_row(), Some(1));
800 table.select_row(None);
801 assert!(table.get_selected_row().is_none());
802 }
803
804 #[test]
805 fn test_data_table_select_row_out_of_bounds() {
806 let mut table = DataTable::new().row(TableRow::new());
807 table.select_row(Some(10));
808 assert!(table.get_selected_row().is_none());
809 }
810
811 #[test]
814 fn test_data_table_set_sort() {
815 let mut table = DataTable::new().column(TableColumn::new("name", "Name").sortable());
816
817 table.set_sort("name", SortDirection::Descending);
818 assert_eq!(table.get_sort_column(), Some("name"));
819 assert_eq!(table.get_sort_direction(), SortDirection::Descending);
820 }
821
822 #[test]
823 fn test_sort_direction() {
824 assert_ne!(SortDirection::Ascending, SortDirection::Descending);
825 }
826
827 #[test]
830 fn test_data_table_clear() {
831 let mut table = DataTable::new().row(TableRow::new()).row(TableRow::new());
832 table.select_row(Some(0));
833
834 table.clear();
835 assert!(table.is_empty());
836 assert!(table.get_selected_row().is_none());
837 }
838
839 #[test]
842 fn test_data_table_row_height_min() {
843 let table = DataTable::new().row_height(10.0);
844 assert_eq!(table.row_height, 20.0);
845 }
846
847 #[test]
848 fn test_data_table_header_height_min() {
849 let table = DataTable::new().header_height(10.0);
850 assert_eq!(table.header_height, 20.0);
851 }
852
853 #[test]
854 fn test_data_table_calculate_width() {
855 let table = DataTable::new()
856 .column(TableColumn::new("a", "A").width(100.0))
857 .column(TableColumn::new("b", "B").width(150.0));
858 assert_eq!(table.calculate_width(), 250.0);
859 }
860
861 #[test]
862 fn test_data_table_calculate_height() {
863 let table = DataTable::new()
864 .header_height(40.0)
865 .row_height(30.0)
866 .row(TableRow::new())
867 .row(TableRow::new());
868 assert_eq!(table.calculate_height(), 40.0 + 60.0);
869 }
870
871 #[test]
874 fn test_data_table_type_id() {
875 let table = DataTable::new();
876 assert_eq!(Widget::type_id(&table), TypeId::of::<DataTable>());
877 }
878
879 #[test]
880 fn test_data_table_measure() {
881 let table = DataTable::new()
882 .column(TableColumn::new("a", "A").width(200.0))
883 .header_height(40.0)
884 .row_height(30.0)
885 .row(TableRow::new())
886 .row(TableRow::new());
887
888 let size = table.measure(Constraints::loose(Size::new(1000.0, 1000.0)));
889 assert_eq!(size.width, 200.0);
890 assert_eq!(size.height, 100.0); }
892
893 #[test]
894 fn test_data_table_layout() {
895 let mut table = DataTable::new();
896 let bounds = Rect::new(10.0, 20.0, 500.0, 300.0);
897 let result = table.layout(bounds);
898 assert_eq!(result.size, Size::new(500.0, 300.0));
899 assert_eq!(table.bounds, bounds);
900 }
901
902 #[test]
903 fn test_data_table_children() {
904 let table = DataTable::new();
905 assert!(table.children().is_empty());
906 }
907
908 #[test]
909 fn test_data_table_is_interactive() {
910 let table = DataTable::new();
911 assert!(!table.is_interactive());
912
913 let table = DataTable::new().selectable(true);
914 assert!(table.is_interactive());
915 }
916
917 #[test]
918 fn test_data_table_is_focusable() {
919 let table = DataTable::new();
920 assert!(!table.is_focusable());
921
922 let table = DataTable::new().selectable(true);
923 assert!(table.is_focusable());
924 }
925
926 #[test]
927 fn test_data_table_accessible_role() {
928 let table = DataTable::new();
929 assert_eq!(table.accessible_role(), AccessibleRole::Table);
930 }
931
932 #[test]
933 fn test_data_table_accessible_name() {
934 let table = DataTable::new().accessible_name("Products table");
935 assert_eq!(Widget::accessible_name(&table), Some("Products table"));
936 }
937
938 #[test]
939 fn test_data_table_test_id() {
940 let table = DataTable::new().test_id("inventory-grid");
941 assert_eq!(Widget::test_id(&table), Some("inventory-grid"));
942 }
943
944 #[test]
947 fn test_table_sort_changed() {
948 let msg = TableSortChanged {
949 column: "price".to_string(),
950 direction: SortDirection::Descending,
951 };
952 assert_eq!(msg.column, "price");
953 assert_eq!(msg.direction, SortDirection::Descending);
954 }
955
956 #[test]
957 fn test_table_row_selected() {
958 let msg = TableRowSelected { index: 5 };
959 assert_eq!(msg.index, 5);
960 }
961
962 #[test]
965 fn test_row_y() {
966 let mut table = DataTable::new()
967 .header_height(50.0)
968 .row_height(40.0)
969 .row(TableRow::new())
970 .row(TableRow::new());
971 table.bounds = Rect::new(0.0, 10.0, 100.0, 200.0);
972
973 assert_eq!(table.row_y(0), 60.0); assert_eq!(table.row_y(1), 100.0); }
976
977 use presentar_core::RecordingCanvas;
980
981 #[test]
982 fn test_data_table_paint_empty() {
983 let mut table = DataTable::new();
984 table.bounds = Rect::new(0.0, 0.0, 400.0, 300.0);
985 let mut canvas = RecordingCanvas::new();
986 table.paint(&mut canvas);
987 }
988
989 #[test]
990 fn test_data_table_paint_with_data() {
991 let mut table = DataTable::new()
992 .column(TableColumn::new("name", "Name").width(100.0))
993 .column(TableColumn::new("value", "Value").width(100.0))
994 .row(TableRow::new().cell("name", "Item 1").cell("value", 100))
995 .row(TableRow::new().cell("name", "Item 2").cell("value", 200));
996 table.bounds = Rect::new(0.0, 0.0, 400.0, 200.0);
997 let mut canvas = RecordingCanvas::new();
998 table.paint(&mut canvas);
999 assert!(canvas.commands().len() > 5);
1000 }
1001
1002 #[test]
1003 fn test_data_table_paint_with_selection() {
1004 let mut table = DataTable::new()
1005 .column(TableColumn::new("x", "X"))
1006 .row(TableRow::new().cell("x", "A"))
1007 .row(TableRow::new().cell("x", "B"))
1008 .selectable(true);
1009 table.bounds = Rect::new(0.0, 0.0, 200.0, 150.0);
1010 table.selected_row = Some(1);
1011 let mut canvas = RecordingCanvas::new();
1012 table.paint(&mut canvas);
1013 assert!(canvas.commands().len() > 5);
1014 }
1015
1016 #[test]
1017 fn test_data_table_paint_striped() {
1018 let mut table = DataTable::new()
1019 .column(TableColumn::new("x", "X"))
1020 .row(TableRow::new())
1021 .row(TableRow::new())
1022 .row(TableRow::new())
1023 .striped(true);
1024 table.bounds = Rect::new(0.0, 0.0, 200.0, 200.0);
1025 let mut canvas = RecordingCanvas::new();
1026 table.paint(&mut canvas);
1027 assert!(canvas.commands().len() > 5);
1028 }
1029
1030 #[test]
1031 fn test_data_table_paint_bordered() {
1032 let mut table = DataTable::new()
1033 .column(TableColumn::new("x", "X"))
1034 .row(TableRow::new())
1035 .bordered(true);
1036 table.bounds = Rect::new(0.0, 0.0, 200.0, 100.0);
1037 let mut canvas = RecordingCanvas::new();
1038 table.paint(&mut canvas);
1040 }
1041
1042 #[test]
1043 fn test_data_table_paint_sortable_columns() {
1044 let mut table = DataTable::new()
1045 .column(TableColumn::new("name", "Name").sortable())
1046 .row(TableRow::new().cell("name", "A"));
1047 table.bounds = Rect::new(0.0, 0.0, 200.0, 100.0);
1048 table.sort_column = Some("name".to_string());
1049 table.sort_direction = SortDirection::Ascending;
1050 let mut canvas = RecordingCanvas::new();
1051 table.paint(&mut canvas);
1052 }
1053
1054 #[test]
1055 fn test_data_table_paint_all_alignments() {
1056 let mut table = DataTable::new()
1057 .column(TableColumn::new("a", "A").align(TextAlign::Left))
1058 .column(TableColumn::new("b", "B").align(TextAlign::Center))
1059 .column(TableColumn::new("c", "C").align(TextAlign::Right))
1060 .row(TableRow::new().cell("a", "L").cell("b", "C").cell("c", "R"));
1061 table.bounds = Rect::new(0.0, 0.0, 300.0, 100.0);
1062 let mut canvas = RecordingCanvas::new();
1063 table.paint(&mut canvas);
1064 }
1065
1066 #[test]
1069 fn test_data_table_event_mouse_down() {
1070 let mut table = DataTable::new()
1071 .column(TableColumn::new("name", "Name").sortable())
1072 .row(TableRow::new());
1073 table.layout(Rect::new(0.0, 0.0, 200.0, 100.0));
1074
1075 let _ = table.event(&Event::MouseDown {
1077 position: presentar_core::Point::new(100.0, 20.0),
1078 button: presentar_core::MouseButton::Left,
1079 });
1080 }
1081
1082 #[test]
1083 fn test_data_table_event_selectable() {
1084 let mut table = DataTable::new()
1085 .column(TableColumn::new("x", "X"))
1086 .row(TableRow::new())
1087 .row(TableRow::new())
1088 .selectable(true);
1089 table.layout(Rect::new(0.0, 0.0, 200.0, 200.0));
1090
1091 let _ = table.event(&Event::MouseDown {
1093 position: presentar_core::Point::new(100.0, 80.0),
1094 button: presentar_core::MouseButton::Left,
1095 });
1096 }
1097
1098 #[test]
1099 fn test_data_table_event_not_selectable() {
1100 let mut table = DataTable::new()
1101 .column(TableColumn::new("x", "X"))
1102 .row(TableRow::new())
1103 .selectable(false);
1104 table.layout(Rect::new(0.0, 0.0, 200.0, 150.0));
1105
1106 let result = table.event(&Event::MouseDown {
1107 position: presentar_core::Point::new(100.0, 80.0),
1108 button: presentar_core::MouseButton::Left,
1109 });
1110 assert!(result.is_none());
1111 }
1112
1113 #[test]
1114 fn test_data_table_event_keydown() {
1115 let mut table = DataTable::new();
1116 table.layout(Rect::new(0.0, 0.0, 200.0, 100.0));
1117
1118 let result = table.event(&Event::KeyDown {
1119 key: presentar_core::Key::Enter,
1120 });
1121 assert!(result.is_none());
1122 }
1123
1124 #[test]
1127 fn test_data_table_color_setters() {
1128 let table = DataTable::new()
1129 .header_bg(Color::RED)
1130 .row_bg(Color::GREEN)
1131 .row_alt_bg(Color::BLUE)
1132 .selected_bg(Color::new(1.0, 1.0, 0.0, 1.0))
1133 .text_color(Color::BLACK);
1134
1135 assert_eq!(table.header_bg, Color::RED);
1136 assert_eq!(table.row_bg, Color::GREEN);
1137 }
1138
1139 #[test]
1140 fn test_data_table_striped_toggle() {
1141 let table = DataTable::new().striped(false);
1142 assert!(!table.striped);
1143 }
1144
1145 #[test]
1146 fn test_data_table_bordered_toggle() {
1147 let table = DataTable::new().bordered(false);
1148 assert!(!table.bordered);
1149 }
1150
1151 #[test]
1154 fn test_cell_value_from_string() {
1155 let s = String::from("hello");
1156 let cell: CellValue = s.into();
1157 assert!(matches!(cell, CellValue::Text(_)));
1158 }
1159
1160 #[test]
1163 fn test_data_table_brick_name() {
1164 let table = DataTable::new();
1165 assert_eq!(table.brick_name(), "DataTable");
1166 }
1167
1168 #[test]
1169 fn test_data_table_brick_assertions() {
1170 let table = DataTable::new();
1171 let assertions = table.assertions();
1172 assert!(!assertions.is_empty());
1173 }
1174
1175 #[test]
1176 fn test_data_table_brick_budget() {
1177 let table = DataTable::new();
1178 let budget = table.budget();
1179 assert!(budget.layout_ms > 0);
1180 }
1181
1182 #[test]
1183 fn test_data_table_brick_verify() {
1184 let table = DataTable::new();
1185 let verification = table.verify();
1186 assert!(!verification.passed.is_empty());
1187 assert!(verification.failed.is_empty());
1188 }
1189
1190 #[test]
1191 fn test_data_table_brick_to_html() {
1192 let table = DataTable::new();
1193 let html = table.to_html();
1194 assert!(html.contains("brick-data-table"));
1195 }
1196
1197 #[test]
1198 fn test_data_table_brick_to_css() {
1199 let table = DataTable::new();
1200 let css = table.to_css();
1201 assert!(css.contains("brick-data-table"));
1202 }
1203
1204 #[test]
1205 fn test_data_table_brick_test_id() {
1206 let table = DataTable::new().test_id("my-table");
1207 assert_eq!(Brick::test_id(&table), Some("my-table"));
1208 }
1209
1210 #[test]
1213 fn test_data_table_children_mut_empty() {
1214 let mut table = DataTable::new();
1215 assert!(table.children_mut().is_empty());
1216 }
1217}