presentar_widgets/
data_table.rs

1//! `DataTable` widget for displaying tabular data.
2
3use 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/// Column definition for a data table.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct TableColumn {
13    /// Column key (field name in data)
14    pub key: String,
15    /// Display header
16    pub header: String,
17    /// Column width (None = auto)
18    pub width: Option<f32>,
19    /// Text alignment
20    pub align: TextAlign,
21    /// Whether column is sortable
22    pub sortable: bool,
23}
24
25impl TableColumn {
26    /// Create a new column.
27    #[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    /// Set column width.
39    #[must_use]
40    pub fn width(mut self, width: f32) -> Self {
41        self.width = Some(width.max(20.0));
42        self
43    }
44
45    /// Set text alignment.
46    #[must_use]
47    pub const fn align(mut self, align: TextAlign) -> Self {
48        self.align = align;
49        self
50    }
51
52    /// Make column sortable.
53    #[must_use]
54    pub const fn sortable(mut self) -> Self {
55        self.sortable = true;
56        self
57    }
58}
59
60/// Text alignment within a cell.
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
62pub enum TextAlign {
63    #[default]
64    Left,
65    Center,
66    Right,
67}
68
69/// Sort direction.
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
71pub enum SortDirection {
72    Ascending,
73    Descending,
74}
75
76/// Message emitted when table sorting changes.
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct TableSortChanged {
79    /// Column key being sorted
80    pub column: String,
81    /// Sort direction
82    pub direction: SortDirection,
83}
84
85/// Message emitted when a row is selected.
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct TableRowSelected {
88    /// Index of selected row
89    pub index: usize,
90}
91
92/// A cell value in the table.
93#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
94pub enum CellValue {
95    /// Text value
96    Text(String),
97    /// Numeric value
98    Number(f64),
99    /// Boolean value
100    Bool(bool),
101    /// Empty cell
102    Empty,
103}
104
105impl CellValue {
106    /// Get display text for the cell.
107    #[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/// A row of data in the table.
149#[derive(Debug, Clone, Default, Serialize, Deserialize)]
150pub struct TableRow {
151    /// Cell values by column key
152    pub cells: std::collections::HashMap<String, CellValue>,
153}
154
155impl TableRow {
156    /// Create a new empty row.
157    #[must_use]
158    pub fn new() -> Self {
159        Self::default()
160    }
161
162    /// Add a cell value.
163    #[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    /// Get a cell value.
170    #[must_use]
171    pub fn get(&self, key: &str) -> Option<&CellValue> {
172        self.cells.get(key)
173    }
174}
175
176/// `DataTable` widget for displaying tabular data.
177#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct DataTable {
179    /// Column definitions
180    columns: Vec<TableColumn>,
181    /// Row data
182    rows: Vec<TableRow>,
183    /// Row height
184    row_height: f32,
185    /// Header height
186    header_height: f32,
187    /// Current sort column
188    sort_column: Option<String>,
189    /// Current sort direction
190    sort_direction: SortDirection,
191    /// Selected row index
192    selected_row: Option<usize>,
193    /// Whether rows are selectable
194    selectable: bool,
195    /// Striped rows
196    striped: bool,
197    /// Show borders
198    bordered: bool,
199    /// Header background color
200    header_bg: Color,
201    /// Row background color
202    row_bg: Color,
203    /// Alternate row background color
204    row_alt_bg: Color,
205    /// Selected row background color
206    selected_bg: Color,
207    /// Border color
208    border_color: Color,
209    /// Text color
210    text_color: Color,
211    /// Header text color
212    header_text_color: Color,
213    /// Accessible name
214    accessible_name_value: Option<String>,
215    /// Test ID
216    test_id_value: Option<String>,
217    /// Cached bounds
218    #[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    /// Create a new empty data table.
251    #[must_use]
252    pub fn new() -> Self {
253        Self::default()
254    }
255
256    /// Add a column.
257    #[must_use]
258    pub fn column(mut self, column: TableColumn) -> Self {
259        self.columns.push(column);
260        self
261    }
262
263    /// Add multiple columns.
264    #[must_use]
265    pub fn columns(mut self, columns: impl IntoIterator<Item = TableColumn>) -> Self {
266        self.columns.extend(columns);
267        self
268    }
269
270    /// Add a row.
271    #[must_use]
272    pub fn row(mut self, row: TableRow) -> Self {
273        self.rows.push(row);
274        self
275    }
276
277    /// Add multiple rows.
278    #[must_use]
279    pub fn rows(mut self, rows: impl IntoIterator<Item = TableRow>) -> Self {
280        self.rows.extend(rows);
281        self
282    }
283
284    /// Set row height.
285    #[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    /// Set header height.
292    #[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    /// Enable row selection.
299    #[must_use]
300    pub const fn selectable(mut self, selectable: bool) -> Self {
301        self.selectable = selectable;
302        self
303    }
304
305    /// Enable striped rows.
306    #[must_use]
307    pub const fn striped(mut self, striped: bool) -> Self {
308        self.striped = striped;
309        self
310    }
311
312    /// Enable borders.
313    #[must_use]
314    pub const fn bordered(mut self, bordered: bool) -> Self {
315        self.bordered = bordered;
316        self
317    }
318
319    /// Set header background color.
320    #[must_use]
321    pub const fn header_bg(mut self, color: Color) -> Self {
322        self.header_bg = color;
323        self
324    }
325
326    /// Set row background color.
327    #[must_use]
328    pub const fn row_bg(mut self, color: Color) -> Self {
329        self.row_bg = color;
330        self
331    }
332
333    /// Set alternate row background color.
334    #[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    /// Set selected row background color.
341    #[must_use]
342    pub const fn selected_bg(mut self, color: Color) -> Self {
343        self.selected_bg = color;
344        self
345    }
346
347    /// Set text color.
348    #[must_use]
349    pub const fn text_color(mut self, color: Color) -> Self {
350        self.text_color = color;
351        self
352    }
353
354    /// Set the accessible name.
355    #[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    /// Set the test ID.
362    #[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    /// Get column count.
369    #[must_use]
370    pub fn column_count(&self) -> usize {
371        self.columns.len()
372    }
373
374    /// Get row count.
375    #[must_use]
376    pub fn row_count(&self) -> usize {
377        self.rows.len()
378    }
379
380    /// Get columns.
381    #[must_use]
382    pub fn get_columns(&self) -> &[TableColumn] {
383        &self.columns
384    }
385
386    /// Get rows.
387    #[must_use]
388    pub fn get_rows(&self) -> &[TableRow] {
389        &self.rows
390    }
391
392    /// Get selected row index.
393    #[must_use]
394    pub const fn get_selected_row(&self) -> Option<usize> {
395        self.selected_row
396    }
397
398    /// Get current sort column.
399    #[must_use]
400    pub fn get_sort_column(&self) -> Option<&str> {
401        self.sort_column.as_deref()
402    }
403
404    /// Get current sort direction.
405    #[must_use]
406    pub const fn get_sort_direction(&self) -> SortDirection {
407        self.sort_direction
408    }
409
410    /// Check if table is empty.
411    #[must_use]
412    pub fn is_empty(&self) -> bool {
413        self.rows.is_empty()
414    }
415
416    /// Select a row.
417    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    /// Set sort column and direction.
428    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    /// Clear data.
434    pub fn clear(&mut self) {
435        self.rows.clear();
436        self.selected_row = None;
437    }
438
439    /// Calculate total width.
440    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    /// Calculate total height.
449    fn calculate_height(&self) -> f32 {
450        (self.rows.len() as f32).mul_add(self.row_height, self.header_height)
451    }
452
453    /// Get row Y position.
454    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        // Draw header row
478        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        // Draw header text
487        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        // Draw data rows
505        for (row_idx, row) in self.rows.iter().enumerate() {
506            let row_y = self.row_y(row_idx);
507
508            // Determine background color
509            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            // Draw cell values
521            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        // Draw borders
541        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        // Row selection and sorting would be handled here
554        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    // ===== TableColumn Tests =====
591
592    #[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    // ===== TextAlign Tests =====
619
620    #[test]
621    fn test_text_align_default() {
622        assert_eq!(TextAlign::default(), TextAlign::Left);
623    }
624
625    // ===== CellValue Tests =====
626
627    #[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    // ===== TableRow Tests =====
675
676    #[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    // ===== DataTable Construction Tests =====
701
702    #[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    // ===== Selection Tests =====
751
752    #[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    // ===== Sort Tests =====
774
775    #[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    // ===== Clear Tests =====
790
791    #[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    // ===== Dimension Tests =====
802
803    #[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    // ===== Widget Trait Tests =====
834
835    #[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); // 40 + 30*2
853    }
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    // ===== Message Tests =====
907
908    #[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    // ===== Position Tests =====
925
926    #[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); // 10 + 50
936        assert_eq!(table.row_y(1), 100.0); // 10 + 50 + 40
937    }
938}