presentar_widgets/
data_table.rs

1//! `DataTable` widget for displaying tabular data.
2
3use 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/// Column definition for a data table.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct TableColumn {
15    /// Column key (field name in data)
16    pub key: String,
17    /// Display header
18    pub header: String,
19    /// Column width (None = auto)
20    pub width: Option<f32>,
21    /// Text alignment
22    pub align: TextAlign,
23    /// Whether column is sortable
24    pub sortable: bool,
25}
26
27impl TableColumn {
28    /// Create a new column.
29    #[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    /// Set column width.
41    #[must_use]
42    pub fn width(mut self, width: f32) -> Self {
43        self.width = Some(width.max(20.0));
44        self
45    }
46
47    /// Set text alignment.
48    #[must_use]
49    pub const fn align(mut self, align: TextAlign) -> Self {
50        self.align = align;
51        self
52    }
53
54    /// Make column sortable.
55    #[must_use]
56    pub const fn sortable(mut self) -> Self {
57        self.sortable = true;
58        self
59    }
60}
61
62/// Text alignment within a cell.
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
64pub enum TextAlign {
65    #[default]
66    Left,
67    Center,
68    Right,
69}
70
71/// Sort direction.
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
73pub enum SortDirection {
74    Ascending,
75    Descending,
76}
77
78/// Message emitted when table sorting changes.
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct TableSortChanged {
81    /// Column key being sorted
82    pub column: String,
83    /// Sort direction
84    pub direction: SortDirection,
85}
86
87/// Message emitted when a row is selected.
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct TableRowSelected {
90    /// Index of selected row
91    pub index: usize,
92}
93
94/// A cell value in the table.
95#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
96pub enum CellValue {
97    /// Text value
98    Text(String),
99    /// Numeric value
100    Number(f64),
101    /// Boolean value
102    Bool(bool),
103    /// Empty cell
104    Empty,
105}
106
107impl CellValue {
108    /// Get display text for the cell.
109    #[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/// A row of data in the table.
151#[derive(Debug, Clone, Default, Serialize, Deserialize)]
152pub struct TableRow {
153    /// Cell values by column key
154    pub cells: std::collections::HashMap<String, CellValue>,
155}
156
157impl TableRow {
158    /// Create a new empty row.
159    #[must_use]
160    pub fn new() -> Self {
161        Self::default()
162    }
163
164    /// Add a cell value.
165    #[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    /// Get a cell value.
172    #[must_use]
173    pub fn get(&self, key: &str) -> Option<&CellValue> {
174        self.cells.get(key)
175    }
176}
177
178/// `DataTable` widget for displaying tabular data.
179#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct DataTable {
181    /// Column definitions
182    columns: Vec<TableColumn>,
183    /// Row data
184    rows: Vec<TableRow>,
185    /// Row height
186    row_height: f32,
187    /// Header height
188    header_height: f32,
189    /// Current sort column
190    sort_column: Option<String>,
191    /// Current sort direction
192    sort_direction: SortDirection,
193    /// Selected row index
194    selected_row: Option<usize>,
195    /// Whether rows are selectable
196    selectable: bool,
197    /// Striped rows
198    striped: bool,
199    /// Show borders
200    bordered: bool,
201    /// Header background color
202    header_bg: Color,
203    /// Row background color
204    row_bg: Color,
205    /// Alternate row background color
206    row_alt_bg: Color,
207    /// Selected row background color
208    selected_bg: Color,
209    /// Border color
210    border_color: Color,
211    /// Text color
212    text_color: Color,
213    /// Header text color
214    header_text_color: Color,
215    /// Accessible name
216    accessible_name_value: Option<String>,
217    /// Test ID
218    test_id_value: Option<String>,
219    /// Cached bounds
220    #[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    /// Create a new empty data table.
253    #[must_use]
254    pub fn new() -> Self {
255        Self::default()
256    }
257
258    /// Add a column.
259    #[must_use]
260    pub fn column(mut self, column: TableColumn) -> Self {
261        self.columns.push(column);
262        self
263    }
264
265    /// Add multiple columns.
266    #[must_use]
267    pub fn columns(mut self, columns: impl IntoIterator<Item = TableColumn>) -> Self {
268        self.columns.extend(columns);
269        self
270    }
271
272    /// Add a row.
273    #[must_use]
274    pub fn row(mut self, row: TableRow) -> Self {
275        self.rows.push(row);
276        self
277    }
278
279    /// Add multiple rows.
280    #[must_use]
281    pub fn rows(mut self, rows: impl IntoIterator<Item = TableRow>) -> Self {
282        self.rows.extend(rows);
283        self
284    }
285
286    /// Set row height.
287    #[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    /// Set header height.
294    #[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    /// Enable row selection.
301    #[must_use]
302    pub const fn selectable(mut self, selectable: bool) -> Self {
303        self.selectable = selectable;
304        self
305    }
306
307    /// Enable striped rows.
308    #[must_use]
309    pub const fn striped(mut self, striped: bool) -> Self {
310        self.striped = striped;
311        self
312    }
313
314    /// Enable borders.
315    #[must_use]
316    pub const fn bordered(mut self, bordered: bool) -> Self {
317        self.bordered = bordered;
318        self
319    }
320
321    /// Set header background color.
322    #[must_use]
323    pub const fn header_bg(mut self, color: Color) -> Self {
324        self.header_bg = color;
325        self
326    }
327
328    /// Set row background color.
329    #[must_use]
330    pub const fn row_bg(mut self, color: Color) -> Self {
331        self.row_bg = color;
332        self
333    }
334
335    /// Set alternate row background color.
336    #[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    /// Set selected row background color.
343    #[must_use]
344    pub const fn selected_bg(mut self, color: Color) -> Self {
345        self.selected_bg = color;
346        self
347    }
348
349    /// Set text color.
350    #[must_use]
351    pub const fn text_color(mut self, color: Color) -> Self {
352        self.text_color = color;
353        self
354    }
355
356    /// Set the accessible name.
357    #[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    /// Set the test ID.
364    #[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    /// Get column count.
371    #[must_use]
372    pub fn column_count(&self) -> usize {
373        self.columns.len()
374    }
375
376    /// Get row count.
377    #[must_use]
378    pub fn row_count(&self) -> usize {
379        self.rows.len()
380    }
381
382    /// Get columns.
383    #[must_use]
384    pub fn get_columns(&self) -> &[TableColumn] {
385        &self.columns
386    }
387
388    /// Get rows.
389    #[must_use]
390    pub fn get_rows(&self) -> &[TableRow] {
391        &self.rows
392    }
393
394    /// Get selected row index.
395    #[must_use]
396    pub const fn get_selected_row(&self) -> Option<usize> {
397        self.selected_row
398    }
399
400    /// Get current sort column.
401    #[must_use]
402    pub fn get_sort_column(&self) -> Option<&str> {
403        self.sort_column.as_deref()
404    }
405
406    /// Get current sort direction.
407    #[must_use]
408    pub const fn get_sort_direction(&self) -> SortDirection {
409        self.sort_direction
410    }
411
412    /// Check if table is empty.
413    #[must_use]
414    pub fn is_empty(&self) -> bool {
415        self.rows.is_empty()
416    }
417
418    /// Select a row.
419    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    /// Set sort column and direction.
430    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    /// Clear data.
436    pub fn clear(&mut self) {
437        self.rows.clear();
438        self.selected_row = None;
439    }
440
441    /// Calculate total width.
442    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    /// Calculate total height.
451    fn calculate_height(&self) -> f32 {
452        (self.rows.len() as f32).mul_add(self.row_height, self.header_height)
453    }
454
455    /// Get row Y position.
456    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        // Draw header row
480        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        // Draw header text
489        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        // Draw data rows
507        for (row_idx, row) in self.rows.iter().enumerate() {
508            let row_y = self.row_y(row_idx);
509
510            // Determine background color
511            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            // Draw cell values
523            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        // Draw borders
543        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        // Row selection and sorting would be handled here
556        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
588// PROBAR-SPEC-009: Brick Architecture - Tests define interface
589impl 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    // ===== TableColumn Tests =====
629
630    #[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    // ===== TextAlign Tests =====
657
658    #[test]
659    fn test_text_align_default() {
660        assert_eq!(TextAlign::default(), TextAlign::Left);
661    }
662
663    // ===== CellValue Tests =====
664
665    #[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    // ===== TableRow Tests =====
713
714    #[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    // ===== DataTable Construction Tests =====
739
740    #[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    // ===== Selection Tests =====
789
790    #[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    // ===== Sort Tests =====
812
813    #[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    // ===== Clear Tests =====
828
829    #[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    // ===== Dimension Tests =====
840
841    #[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    // ===== Widget Trait Tests =====
872
873    #[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); // 40 + 30*2
891    }
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    // ===== Message Tests =====
945
946    #[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    // ===== Position Tests =====
963
964    #[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); // 10 + 50
974        assert_eq!(table.row_y(1), 100.0); // 10 + 50 + 40
975    }
976
977    // ===== Paint Tests =====
978
979    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        // Should not panic when painting bordered table
1039        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    // ===== Event Tests =====
1067
1068    #[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        // Should not panic
1076        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        // Click on row area - should not panic
1092        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    // ===== Builder Tests =====
1125
1126    #[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    // ===== Additional CellValue Tests =====
1152
1153    #[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    // ===== Brick Trait Tests =====
1161
1162    #[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    // ===== Widget Trait Tests =====
1211
1212    #[test]
1213    fn test_data_table_children_mut_empty() {
1214        let mut table = DataTable::new();
1215        assert!(table.children_mut().is_empty());
1216    }
1217}