egui_material3/
datatable.rs

1use crate::theme::get_global_color;
2use egui::{
3    ecolor::Color32, 
4    epaint::{Stroke, CornerRadius},
5    FontFamily, FontId,
6    Id, Rect, Response, Sense, Ui, Vec2, Widget, WidgetText,
7};
8use std::collections::{HashMap, HashSet};
9
10/// Persistent state for a Material Design data table.
11///
12/// This structure maintains the state of the table including selections,
13/// sorting, and editing state across frames.
14#[derive(Clone, Debug, Default)]
15pub struct DataTableState {
16    /// Selection state for each row (true if selected)
17    pub selected_rows: Vec<bool>,
18    /// State of the header checkbox (for select-all functionality)
19    pub header_checkbox: bool,
20    /// Sort states for each column by column name
21    pub column_sorts: HashMap<String, SortDirection>,
22    /// Index of the currently sorted column (if any)
23    pub sorted_column: Option<usize>,
24    /// Current sort direction for the sorted column
25    pub sort_direction: SortDirection,
26    /// Set of row indices currently being edited
27    pub editing_rows: std::collections::HashSet<usize>,
28    /// Temporary edit data for rows being edited (row_index -> cell_values)
29    pub edit_data: HashMap<usize, Vec<String>>,
30}
31
32/// Response returned by the data table widget.
33///
34/// Contains both the standard egui Response and additional table-specific
35/// information about user interactions.
36#[derive(Debug)]
37pub struct DataTableResponse {
38    /// The standard egui widget response
39    pub response: Response,
40    /// Current selection state for each row
41    pub selected_rows: Vec<bool>,
42    /// Current state of the header checkbox
43    pub header_checkbox: bool,
44    /// Index of column that was clicked for sorting (if any)
45    pub column_clicked: Option<usize>,
46    /// Current sort state (column index, direction)
47    pub sort_state: (Option<usize>, SortDirection),
48    /// List of row actions performed (edit, delete, save)
49    pub row_actions: Vec<RowAction>,
50}
51
52/// Actions that can be performed on data table rows.
53#[derive(Debug, Clone)]
54pub enum RowAction {
55    /// User clicked edit button for the specified row
56    Edit(usize),
57    /// User clicked delete button for the specified row
58    Delete(usize), 
59    /// User clicked save button for the specified row
60    Save(usize),
61    /// User clicked cancel button for the specified row
62    Cancel(usize),
63}
64
65/// Material Design data table component.
66///
67/// Data tables display sets of data across rows and columns.
68/// They organize information in a way that's easy to scan.
69///
70/// ```
71/// # egui::__run_test_ui(|ui| {
72/// // Basic data table
73/// let mut table = MaterialDataTable::new()
74///     .column("Name", 120.0, false)
75///     .column("Age", 80.0, true)
76///     .column("City", 100.0, false);
77///
78/// table.row(|row| {
79///     row.cell("John Doe");
80///     row.cell("25");
81///     row.cell("New York");
82/// });
83///
84/// ui.add(table);
85/// # });
86/// ```
87#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
88pub struct MaterialDataTable<'a> {
89    columns: Vec<DataTableColumn>,
90    rows: Vec<DataTableRow<'a>>,
91    id: Option<Id>,
92    allow_selection: bool,
93    sticky_header: bool,
94    progress_visible: bool,
95    corner_radius: CornerRadius,
96    sorted_column: Option<usize>,
97    sort_direction: SortDirection,
98}
99
100#[derive(Clone, Debug)]
101pub struct DataTableColumn {
102    /// Display title for the column header
103    pub title: String,
104    /// Fixed width of the column in pixels
105    pub width: f32,
106    /// Whether the column contains numeric data (affects alignment and sorting)
107    pub numeric: bool,
108    /// Whether this column can be sorted by clicking the header
109    pub sortable: bool,
110    /// Current sort direction for this column (if sorted)
111    pub sort_direction: Option<SortDirection>,
112}
113
114#[derive(Clone, Debug, PartialEq)]
115pub enum SortDirection {
116    Ascending,
117    Descending,
118}
119
120impl Default for SortDirection {
121    fn default() -> Self {
122        SortDirection::Ascending
123    }
124}
125
126pub struct DataTableRow<'a> {
127    cells: Vec<WidgetText>,
128    selected: bool,
129    readonly: bool,
130    id: Option<String>,
131    _phantom: std::marker::PhantomData<&'a ()>,
132}
133
134impl<'a> DataTableRow<'a> {
135    pub fn new() -> Self {
136        Self {
137            cells: Vec::new(),
138            selected: false,
139            readonly: false,
140            id: None,
141            _phantom: std::marker::PhantomData,
142        }
143    }
144    
145    pub fn cell(mut self, text: impl Into<WidgetText>) -> Self {
146        self.cells.push(text.into());
147        self
148    }
149    
150    pub fn selected(mut self, selected: bool) -> Self {
151        self.selected = selected;
152        self
153    }
154    
155    pub fn readonly(mut self, readonly: bool) -> Self {
156        self.readonly = readonly;
157        self
158    }
159    
160    pub fn id(mut self, id: impl Into<String>) -> Self {
161        self.id = Some(id.into());
162        self
163    }
164}
165
166impl<'a> MaterialDataTable<'a> {
167    /// Create a new data table.
168    pub fn new() -> Self {
169        Self {
170            columns: Vec::new(),
171            rows: Vec::new(),
172            id: None,
173            allow_selection: false,
174            sticky_header: false,
175            progress_visible: false,
176            corner_radius: CornerRadius::from(4.0),
177            sorted_column: None,
178            sort_direction: SortDirection::Ascending,
179        }
180    }
181
182    /// Set the initial sort column and direction
183    pub fn sort_by(mut self, column_index: usize, direction: SortDirection) -> Self {
184        self.sorted_column = Some(column_index);
185        self.sort_direction = direction;
186        self
187    }
188    
189    /// Get current sorting state
190    pub fn get_sort_state(&self) -> (Option<usize>, SortDirection) {
191        (self.sorted_column, self.sort_direction.clone())
192    }
193
194    /// Add a column to the data table.
195    pub fn column(mut self, title: impl Into<String>, width: f32, numeric: bool) -> Self {
196        self.columns.push(DataTableColumn {
197            title: title.into(),
198            width,
199            numeric,
200            sortable: true, // Make all columns sortable by default
201            sort_direction: None,
202        });
203        self
204    }
205
206    /// Add a sortable column to the data table.
207    pub fn sortable_column(mut self, title: impl Into<String>, width: f32, numeric: bool) -> Self {
208        self.columns.push(DataTableColumn {
209            title: title.into(),
210            width,
211            numeric,
212            sortable: true,
213            sort_direction: None,
214        });
215        self
216    }
217
218    /// Add a row using a builder pattern.
219    pub fn row<F>(mut self, f: F) -> Self 
220    where
221        F: FnOnce(DataTableRow<'a>) -> DataTableRow<'a>,
222    {
223        let row = f(DataTableRow::new());
224        self.rows.push(row);
225        self
226    }
227
228    /// Set the ID for state persistence.
229    pub fn id(mut self, id: impl Into<Id>) -> Self {
230        self.id = Some(id.into());
231        self
232    }
233
234    /// Enable row selection.
235    pub fn allow_selection(mut self, allow: bool) -> Self {
236        self.allow_selection = allow;
237        self
238    }
239
240    /// Make the header sticky.
241    pub fn sticky_header(mut self, sticky: bool) -> Self {
242        self.sticky_header = sticky;
243        self
244    }
245
246    /// Show progress indicator.
247    pub fn show_progress(mut self, show: bool) -> Self {
248        self.progress_visible = show;
249        self
250    }
251
252    /// Set corner radius.
253    pub fn corner_radius(mut self, corner_radius: impl Into<CornerRadius>) -> Self {
254        self.corner_radius = corner_radius.into();
255        self
256    }
257
258    fn get_table_style(&self) -> (Color32, Stroke) {
259        let md_surface = get_global_color("surface");
260        let md_outline = get_global_color("outline");
261        (md_surface, Stroke::new(1.0, md_outline))
262    }
263
264    /// Show the data table and return both UI response and selection state
265    pub fn show(self, ui: &mut Ui) -> DataTableResponse {
266        let (background_color, border_stroke) = self.get_table_style();
267        
268        // Generate table ID for state persistence
269        let table_id = self.id.unwrap_or_else(|| {
270            use std::collections::hash_map::DefaultHasher;
271            use std::hash::{Hash, Hasher};
272            let mut hasher = DefaultHasher::new();
273            
274            // Hash based on columns and first few rows for uniqueness
275            for col in &self.columns {
276                col.title.hash(&mut hasher);
277                col.width.to_bits().hash(&mut hasher);
278            }
279            for (i, row) in self.rows.iter().take(3).enumerate() {
280                i.hash(&mut hasher);
281                for cell in &row.cells {
282                    cell.text().hash(&mut hasher);
283                }
284            }
285            self.rows.len().hash(&mut hasher);
286            Id::new(format!("datatable_{}", hasher.finish()))
287        });
288
289        // Get or create persistent state
290        let mut state: DataTableState = ui.data_mut(|d| d.get_persisted(table_id).unwrap_or_default());
291        
292        // Get external editing state from UI memory if available
293        if let Some(external_editing_state) = ui.memory(|mem| {
294            mem.data.get_temp::<(HashSet<usize>, HashMap<usize, Vec<String>>)>(table_id.with("external_edit_state"))
295        }) {
296            state.editing_rows = external_editing_state.0;
297            state.edit_data = external_editing_state.1;
298        }
299        
300        // Initialize sorting state from widget if not set
301        if state.sorted_column.is_none() && self.sorted_column.is_some() {
302            state.sorted_column = self.sorted_column;
303            state.sort_direction = self.sort_direction.clone();
304        }
305        
306        // Ensure state vectors match current row count
307        if state.selected_rows.len() != self.rows.len() {
308            state.selected_rows.resize(self.rows.len(), false);
309        }
310        
311        // Initialize selection from rows if this is the first time or rows changed
312        for (i, row) in self.rows.iter().enumerate() {
313            if i < state.selected_rows.len() && row.selected {
314                state.selected_rows[i] = row.selected;
315            }
316        }
317
318        let MaterialDataTable {
319            columns,
320            mut rows,
321            allow_selection,
322            sticky_header: _,
323            progress_visible,
324            corner_radius,
325            ..
326        } = self;
327        
328        // Sort rows if a column is selected for sorting
329        if let Some(sort_col_idx) = state.sorted_column {
330            if let Some(sort_column) = columns.get(sort_col_idx) {
331                rows.sort_by(|a, b| {
332                    let cell_a = a.cells.get(sort_col_idx).map(|c| c.text()).unwrap_or("");
333                    let cell_b = b.cells.get(sort_col_idx).map(|c| c.text()).unwrap_or("");
334                    
335                    let comparison = if sort_column.numeric {
336                        // Try to parse as numbers for numeric columns
337                        let a_num: f64 = cell_a.trim_start_matches('$').parse().unwrap_or(0.0);
338                        let b_num: f64 = cell_b.trim_start_matches('$').parse().unwrap_or(0.0);
339                        a_num.partial_cmp(&b_num).unwrap_or(std::cmp::Ordering::Equal)
340                    } else {
341                        // Alphabetical comparison for text columns
342                        cell_a.cmp(cell_b)
343                    };
344                    
345                    match state.sort_direction {
346                        SortDirection::Ascending => comparison,
347                        SortDirection::Descending => comparison.reverse(),
348                    }
349                });
350            }
351        }
352
353        // Calculate table dimensions with dynamic row heights
354        let checkbox_width = if allow_selection { 48.0 } else { 0.0 };
355        let total_width = checkbox_width + columns.iter().map(|col| col.width).sum::<f32>();
356        let min_row_height = 52.0;
357        let min_header_height = 56.0;
358        
359        // Calculate header height with text wrapping
360        let mut header_height: f32 = min_header_height;
361        for column in &columns {
362            let available_width = column.width - 48.0; // Account for padding and sort icon
363            let header_font = FontId::new(16.0, FontFamily::Proportional);
364            
365            let galley = ui.fonts(|f| f.layout_job(egui::text::LayoutJob {
366                text: column.title.clone(),
367                sections: vec![egui::text::LayoutSection {
368                    leading_space: 0.0,
369                    byte_range: 0..column.title.len(),
370                    format: egui::TextFormat {
371                        font_id: header_font,
372                        color: get_global_color("onSurface"),
373                        ..Default::default()
374                    },
375                }],
376                wrap: egui::text::TextWrapping {
377                    max_width: available_width,
378                    ..Default::default()
379                },
380                break_on_newline: true,
381                halign: egui::Align::LEFT,
382                justify: false,
383                first_row_min_height: 0.0,
384                round_output_to_gui: true,
385            }));
386            
387            let content_height: f32 = galley.size().y + 16.0; // Add padding
388            header_height = header_height.max(content_height);
389        }
390        
391        // Calculate individual row heights based on content
392        let mut row_heights = Vec::new();
393        for row in &rows {
394            let mut max_height: f32 = min_row_height;
395            for (cell_idx, cell_text) in row.cells.iter().enumerate() {
396                if let Some(column) = columns.get(cell_idx) {
397                    let available_width = column.width - 32.0;
398                    let cell_font = FontId::new(14.0, FontFamily::Proportional);
399                    
400                    let galley = ui.fonts(|f| f.layout_job(egui::text::LayoutJob {
401                        text: cell_text.text().to_string(),
402                        sections: vec![egui::text::LayoutSection {
403                            leading_space: 0.0,
404                            byte_range: 0..cell_text.text().len(),
405                            format: egui::TextFormat {
406                                font_id: cell_font,
407                                color: get_global_color("onSurface"),
408                                ..Default::default()
409                            },
410                        }],
411                        wrap: egui::text::TextWrapping {
412                            max_width: available_width,
413                            ..Default::default()
414                        },
415                        break_on_newline: true,
416                        halign: if column.numeric { egui::Align::RIGHT } else { egui::Align::LEFT },
417                        justify: false,
418                        first_row_min_height: 0.0,
419                        round_output_to_gui: true,
420                    }));
421                    
422                    let content_height: f32 = galley.size().y + 16.0; // Add padding
423                    max_height = max_height.max(content_height);
424                }
425            }
426            row_heights.push(max_height);
427        }
428        
429        let total_height = header_height + row_heights.iter().sum::<f32>();
430        
431        // Collect all row actions from this frame  
432        let mut all_row_actions: Vec<RowAction> = Vec::new();
433
434        let desired_size = Vec2::new(total_width, total_height);
435        let response = ui.allocate_response(desired_size, Sense::click());
436        let rect = response.rect;
437
438        if ui.is_rect_visible(rect) {
439            // Draw table background
440            ui.painter().rect_filled(rect, corner_radius, background_color);
441            ui.painter().rect_stroke(rect, corner_radius, border_stroke, egui::epaint::StrokeKind::Outside);
442
443            let mut current_y = rect.min.y;
444
445            // Draw header
446            let header_rect = Rect::from_min_size(rect.min, Vec2::new(total_width, header_height));
447            let header_bg = get_global_color("surfaceVariant");
448            ui.painter().rect_filled(header_rect, CornerRadius::ZERO, header_bg);
449            
450            let mut current_x = rect.min.x;
451            
452            // Header checkbox
453            if allow_selection {
454                let checkbox_rect = Rect::from_min_size(
455                    egui::pos2(current_x, current_y),
456                    Vec2::new(checkbox_width, header_height)
457                );
458                
459                let checkbox_center = checkbox_rect.center();
460                let checkbox_size = Vec2::splat(18.0);
461                let checkbox_inner_rect = Rect::from_center_size(checkbox_center, checkbox_size);
462                
463                let checkbox_color = if state.header_checkbox {
464                    get_global_color("primary")
465                } else {
466                    Color32::TRANSPARENT
467                };
468                
469                ui.painter().rect_filled(
470                    checkbox_inner_rect,
471                    CornerRadius::from(2.0),
472                    checkbox_color
473                );
474                ui.painter().rect_stroke(
475                    checkbox_inner_rect,
476                    CornerRadius::from(2.0),
477                    Stroke::new(2.0, get_global_color("outline")),
478                    egui::epaint::StrokeKind::Outside
479                );
480                
481                if state.header_checkbox {
482                    // Draw checkmark
483                    let check_points = [
484                        checkbox_inner_rect.min + Vec2::new(4.0, 9.0),
485                        checkbox_inner_rect.min + Vec2::new(8.0, 13.0),
486                        checkbox_inner_rect.min + Vec2::new(14.0, 5.0),
487                    ];
488                    ui.painter().line_segment(
489                        [check_points[0], check_points[1]],
490                        Stroke::new(2.0, Color32::WHITE)
491                    );
492                    ui.painter().line_segment(
493                        [check_points[1], check_points[2]],
494                        Stroke::new(2.0, Color32::WHITE)
495                    );
496                }
497                
498                // Handle header checkbox click
499                let header_checkbox_id = table_id.with("header_checkbox");
500                let checkbox_response = ui.interact(checkbox_inner_rect, header_checkbox_id, Sense::click());
501                if checkbox_response.clicked() {
502                    state.header_checkbox = !state.header_checkbox;
503                    // Only update non-readonly rows
504                    for (idx, selected) in state.selected_rows.iter_mut().enumerate() {
505                        if let Some(row) = rows.get(idx) {
506                            if !row.readonly {
507                                *selected = state.header_checkbox;
508                            }
509                        }
510                    }
511                }
512                
513                current_x += checkbox_width;
514            }
515            
516            // Header columns
517            for (col_idx, column) in columns.iter().enumerate() {
518                let col_rect = Rect::from_min_size(
519                    egui::pos2(current_x, current_y),
520                    Vec2::new(column.width, header_height)
521                );
522                
523                // Render header text with wrapping support
524                let available_width = column.width - 48.0; // Account for padding and sort icon
525                let header_font = FontId::new(16.0, FontFamily::Proportional);
526                
527                let galley = ui.fonts(|f| f.layout_job(egui::text::LayoutJob {
528                    text: column.title.clone(),
529                    sections: vec![egui::text::LayoutSection {
530                        leading_space: 0.0,
531                        byte_range: 0..column.title.len(),
532                        format: egui::TextFormat {
533                            font_id: header_font,
534                            color: get_global_color("onSurface"),
535                            ..Default::default()
536                        },
537                    }],
538                    wrap: egui::text::TextWrapping {
539                        max_width: available_width,
540                        ..Default::default()
541                    },
542                    break_on_newline: true,
543                    halign: egui::Align::LEFT,
544                    justify: false,
545                    first_row_min_height: 0.0,
546                    round_output_to_gui: true,
547                }));
548                
549                let text_pos = egui::pos2(
550                    current_x + 16.0,
551                    current_y + (header_height - galley.size().y) / 2.0
552                );
553                
554                ui.painter().galley(text_pos, galley, get_global_color("onSurface"));
555                
556                // Handle column header clicks for sorting
557                if column.sortable {
558                    let header_click_id = table_id.with(format!("column_header_{}", col_idx));
559                    let header_response = ui.interact(col_rect, header_click_id, Sense::click());
560                    if header_response.clicked() {
561                        // Handle sorting logic
562                        if state.sorted_column == Some(col_idx) {
563                            // Same column clicked, toggle direction
564                            state.sort_direction = match state.sort_direction {
565                                SortDirection::Ascending => SortDirection::Descending,
566                                SortDirection::Descending => SortDirection::Ascending,
567                            };
568                        } else {
569                            // New column clicked
570                            state.sorted_column = Some(col_idx);
571                            state.sort_direction = SortDirection::Ascending;
572                        }
573                        ui.memory_mut(|mem| {
574                            mem.data.insert_temp(table_id.with("column_clicked"), Some(col_idx));
575                        });
576                    }
577                    
578                    let icon_pos = egui::pos2(
579                        current_x + column.width - 32.0,
580                        current_y + (header_height - 24.0) / 2.0
581                    );
582                    let icon_rect = Rect::from_min_size(icon_pos, Vec2::splat(24.0));
583                    
584                    // Determine if this column is currently sorted
585                    let is_sorted = state.sorted_column == Some(col_idx);
586                    let sort_direction = if is_sorted { Some(&state.sort_direction) } else { None };
587                    
588                    // Draw sort arrow with enhanced visual feedback
589                    let arrow_color = if is_sorted {
590                        get_global_color("primary") // Highlight active sort column
591                    } else {
592                        get_global_color("onSurfaceVariant")
593                    };
594                    
595                    let center = icon_rect.center();
596                    
597                    // Draw triangle arrows
598                    match sort_direction {
599                        Some(SortDirection::Ascending) => {
600                            // Up triangle (▲)
601                            let points = [
602                                center + Vec2::new(0.0, -6.0),  // Top point
603                                center + Vec2::new(-5.0, 4.0),  // Bottom left
604                                center + Vec2::new(5.0, 4.0),   // Bottom right
605                            ];
606                            ui.painter().line_segment([points[0], points[1]], Stroke::new(2.0, arrow_color));
607                            ui.painter().line_segment([points[1], points[2]], Stroke::new(2.0, arrow_color));
608                            ui.painter().line_segment([points[2], points[0]], Stroke::new(2.0, arrow_color));
609                        },
610                        Some(SortDirection::Descending) => {
611                            // Down triangle (▼)
612                            let points = [
613                                center + Vec2::new(0.0, 6.0),   // Bottom point
614                                center + Vec2::new(-5.0, -4.0), // Top left
615                                center + Vec2::new(5.0, -4.0),  // Top right
616                            ];
617                            ui.painter().line_segment([points[0], points[1]], Stroke::new(2.0, arrow_color));
618                            ui.painter().line_segment([points[1], points[2]], Stroke::new(2.0, arrow_color));
619                            ui.painter().line_segment([points[2], points[0]], Stroke::new(2.0, arrow_color));
620                        },
621                        None => {
622                            // Neutral state - show both arrows faintly
623                            let light_color = arrow_color.gamma_multiply(0.5);
624                            // Up triangle
625                            let up_points = [
626                                center + Vec2::new(0.0, -8.0),
627                                center + Vec2::new(-3.0, -2.0),
628                                center + Vec2::new(3.0, -2.0),
629                            ];
630                            ui.painter().line_segment([up_points[0], up_points[1]], Stroke::new(1.0, light_color));
631                            ui.painter().line_segment([up_points[1], up_points[2]], Stroke::new(1.0, light_color));
632                            ui.painter().line_segment([up_points[2], up_points[0]], Stroke::new(1.0, light_color));
633                            
634                            // Down triangle
635                            let down_points = [
636                                center + Vec2::new(0.0, 8.0),
637                                center + Vec2::new(-3.0, 2.0),
638                                center + Vec2::new(3.0, 2.0),
639                            ];
640                            ui.painter().line_segment([down_points[0], down_points[1]], Stroke::new(1.0, light_color));
641                            ui.painter().line_segment([down_points[1], down_points[2]], Stroke::new(1.0, light_color));
642                            ui.painter().line_segment([down_points[2], down_points[0]], Stroke::new(1.0, light_color));
643                        }
644                    }
645                }
646                
647                current_x += column.width;
648            }
649
650            current_y += header_height;
651
652            
653            // Draw rows with dynamic heights
654            for (row_idx, row) in rows.iter().enumerate() {
655                let row_height = row_heights.get(row_idx).copied().unwrap_or(min_row_height);
656                let row_rect = Rect::from_min_size(
657                    egui::pos2(rect.min.x, current_y),
658                    Vec2::new(total_width, row_height)
659                );
660                
661                let row_selected = state.selected_rows.get(row_idx).copied().unwrap_or(false);
662                let row_bg = if row_selected {
663                    get_global_color("primaryContainer")
664                } else if row.readonly {
665                    // Subtle background for readonly rows
666                    let surface_variant = get_global_color("surfaceVariant");
667                    Color32::from_rgba_premultiplied(
668                        surface_variant.r(),
669                        surface_variant.g(), 
670                        surface_variant.b(),
671                        (surface_variant.a() as f32 * 0.3) as u8
672                    )
673                } else if row_idx % 2 == 1 {
674                    get_global_color("surfaceVariant")
675                } else {
676                    background_color
677                };
678                
679                ui.painter().rect_filled(row_rect, CornerRadius::ZERO, row_bg);
680                
681                current_x = rect.min.x;
682                
683                // Row checkbox
684                if allow_selection {
685                    let checkbox_rect = Rect::from_min_size(
686                        egui::pos2(current_x, current_y),
687                        Vec2::new(checkbox_width, row_height)
688                    );
689                    
690                    let checkbox_center = checkbox_rect.center();
691                    let checkbox_size = Vec2::splat(18.0);
692                    let checkbox_inner_rect = Rect::from_center_size(checkbox_center, checkbox_size);
693                    
694                    let checkbox_color = if row_selected {
695                        get_global_color("primary")
696                    } else {
697                        Color32::TRANSPARENT
698                    };
699                    
700                    let border_color = if row.readonly {
701                        get_global_color("outline").linear_multiply(0.5) // Dimmed for readonly
702                    } else {
703                        get_global_color("outline")
704                    };
705                    
706                    ui.painter().rect_filled(
707                        checkbox_inner_rect,
708                        CornerRadius::from(2.0),
709                        checkbox_color
710                    );
711                    ui.painter().rect_stroke(
712                        checkbox_inner_rect,
713                        CornerRadius::from(2.0),
714                        Stroke::new(2.0, border_color),
715                        egui::epaint::StrokeKind::Outside
716                    );
717                    
718                    if row_selected {
719                        // Draw checkmark
720                        let check_points = [
721                            checkbox_inner_rect.min + Vec2::new(4.0, 9.0),
722                            checkbox_inner_rect.min + Vec2::new(8.0, 13.0),
723                            checkbox_inner_rect.min + Vec2::new(14.0, 5.0),
724                        ];
725                        ui.painter().line_segment(
726                            [check_points[0], check_points[1]],
727                            Stroke::new(2.0, Color32::WHITE)
728                        );
729                        ui.painter().line_segment(
730                            [check_points[1], check_points[2]],
731                            Stroke::new(2.0, Color32::WHITE)
732                        );
733                    }
734                    
735                    // Handle row checkbox click
736                    let row_checkbox_id = table_id.with(format!("row_checkbox_{}", row_idx));
737                    let checkbox_response = ui.interact(checkbox_inner_rect, row_checkbox_id, Sense::click());
738                    if checkbox_response.clicked() && !row.readonly {
739                        if let Some(selected) = state.selected_rows.get_mut(row_idx) {
740                            *selected = !*selected;
741                        }
742                        
743                        // Update header checkbox state based on row selections
744                        // Only consider non-readonly rows for header checkbox state
745                        let non_readonly_indices: Vec<usize> = rows.iter()
746                            .enumerate()
747                            .filter(|(_, row)| !row.readonly)
748                            .map(|(idx, _)| idx)
749                            .collect();
750                        
751                        if !non_readonly_indices.is_empty() {
752                            let all_non_readonly_selected = non_readonly_indices.iter()
753                                .all(|&idx| state.selected_rows.get(idx).copied().unwrap_or(false));
754                            let none_non_readonly_selected = non_readonly_indices.iter()
755                                .all(|&idx| !state.selected_rows.get(idx).copied().unwrap_or(false));
756                            state.header_checkbox = all_non_readonly_selected && !none_non_readonly_selected;
757                        }
758                    }
759                    
760                    current_x += checkbox_width;
761                }
762                
763                // Track row actions for this specific row
764                let mut row_actions: Vec<RowAction> = Vec::new();
765                
766                // Row cells
767                for (cell_idx, cell_text) in row.cells.iter().enumerate() {
768                    if let Some(column) = columns.get(cell_idx) {
769                        let _cell_rect = Rect::from_min_size(
770                            egui::pos2(current_x, current_y),
771                            Vec2::new(column.width, row_height)
772                        );
773                        
774                        let is_row_editing = state.editing_rows.contains(&row_idx);
775                        let is_actions_column = column.title == "Actions";
776                        
777                        if is_actions_column {
778                            // Render action buttons
779                            let button_rect = Rect::from_min_size(
780                                egui::pos2(current_x + 8.0, current_y + (row_height - 32.0) / 2.0),
781                                Vec2::new(column.width - 16.0, 32.0)
782                            );
783                            
784                            ui.scope_builder(egui::UiBuilder::new().max_rect(button_rect), |ui| {
785                                ui.horizontal(|ui| {
786                                    if is_row_editing {
787                                        if ui.small_button("Save").clicked() {
788                                            row_actions.push(RowAction::Save(row_idx));
789                                        }
790                                        if ui.small_button("Cancel").clicked() {
791                                            row_actions.push(RowAction::Cancel(row_idx));
792                                        }
793                                    } else {
794                                        if ui.small_button("Edit").clicked() {
795                                            row_actions.push(RowAction::Edit(row_idx));
796                                        }
797                                        if ui.small_button("Delete").clicked() {
798                                            row_actions.push(RowAction::Delete(row_idx));
799                                        }
800                                    }
801                                });
802                            });
803                        } else if is_row_editing {
804                            // Render editable text field
805                            let edit_rect = Rect::from_min_size(
806                                egui::pos2(current_x + 8.0, current_y + (row_height - 24.0) / 2.0),
807                                Vec2::new(column.width - 16.0, 24.0)
808                            );
809                            
810                            // Get or initialize edit data
811                            let edit_data = state.edit_data
812                                .entry(row_idx)
813                                .or_insert_with(|| {
814                                    row.cells.iter().map(|c| c.text().to_string()).collect()
815                                });
816                            
817                            // Ensure we have enough entries for this cell
818                            if edit_data.len() <= cell_idx {
819                                edit_data.resize(cell_idx + 1, String::new());
820                            }
821                            
822                            let edit_text = &mut edit_data[cell_idx];
823                            
824                            ui.scope_builder(egui::UiBuilder::new().max_rect(edit_rect), |ui| {
825                                ui.add(egui::TextEdit::singleline(edit_text)
826                                    .desired_width(column.width - 16.0));
827                            });
828                        } else {
829                            // Render normal text
830                            let _text_align = if column.numeric {
831                                egui::Align2::RIGHT_CENTER
832                            } else {
833                                egui::Align2::LEFT_CENTER
834                            };
835                        
836                            // Handle text wrapping for cell content (keeping original logic)
837                            let available_width = column.width - 32.0; // Account for padding
838                            let cell_font = FontId::new(14.0, FontFamily::Proportional);
839                            
840                            let galley = ui.fonts(|f| f.layout_job(egui::text::LayoutJob {
841                                text: cell_text.text().to_string(),
842                                sections: vec![egui::text::LayoutSection {
843                                    leading_space: 0.0,
844                                    byte_range: 0..cell_text.text().len(),
845                                    format: egui::TextFormat {
846                                        font_id: cell_font,
847                                        color: get_global_color("onSurface"),
848                                        ..Default::default()
849                                    },
850                                }],
851                                wrap: egui::text::TextWrapping {
852                                    max_width: available_width,
853                                    ..Default::default()
854                                },
855                                break_on_newline: true,
856                                halign: if column.numeric { egui::Align::RIGHT } else { egui::Align::LEFT },
857                                justify: false,
858                                first_row_min_height: 0.0,
859                                round_output_to_gui: true,
860                            }));
861                            
862                            let text_pos = if column.numeric {
863                                egui::pos2(current_x + column.width - 16.0 - galley.size().x, current_y + (row_height - galley.size().y) / 2.0)
864                            } else {
865                                egui::pos2(current_x + 16.0, current_y + (row_height - galley.size().y) / 2.0)
866                            };
867                            
868                            ui.painter().galley(text_pos, galley, get_global_color("onSurface"));
869                        }
870                        
871                        current_x += column.width;
872                    }
873                }
874                
875                // Add this row's actions to the global collection
876                all_row_actions.extend(row_actions);
877                
878                current_y += row_height;
879            }
880
881            // Draw progress indicator if visible
882            if progress_visible {
883                let scrim_color = Color32::from_rgba_unmultiplied(255, 255, 255, 128);
884                ui.painter().rect_filled(rect, corner_radius, scrim_color);
885                
886                // Draw progress bar
887                let progress_rect = Rect::from_min_size(
888                    egui::pos2(rect.min.x, rect.min.y + header_height),
889                    Vec2::new(total_width, 4.0)
890                );
891                
892                let progress_color = get_global_color("primary");
893                ui.painter().rect_filled(progress_rect, CornerRadius::ZERO, progress_color);
894            }
895        }
896
897        // Persist the state
898        ui.data_mut(|d| d.insert_persisted(table_id, state.clone()));
899        
900        // Store editing state back to memory for external access
901        ui.memory_mut(|mem| {
902            mem.data.insert_temp(table_id.with("external_edit_state"), (state.editing_rows.clone(), state.edit_data.clone()));
903        });
904
905        // Check for column clicks using stored state
906        let column_clicked = ui.memory(|mem| {
907            mem.data.get_temp::<Option<usize>>(table_id.with("column_clicked"))
908        }).flatten();
909        
910        // Clear the stored click state
911        ui.memory_mut(|mem| {
912            mem.data.remove::<Option<usize>>(table_id.with("column_clicked"));
913        });
914        
915        DataTableResponse {
916            response,
917            selected_rows: state.selected_rows,
918            header_checkbox: state.header_checkbox,
919            column_clicked,
920            sort_state: (state.sorted_column, state.sort_direction.clone()),
921            row_actions: all_row_actions,
922        }
923    }
924}
925
926impl<'a> Default for MaterialDataTable<'a> {
927    fn default() -> Self {
928        Self::new()
929    }
930}
931
932impl Widget for MaterialDataTable<'_> {
933    fn ui(self, ui: &mut Ui) -> Response {
934        self.show(ui).response
935    }
936}
937
938/// Convenience function to create a new data table.
939pub fn data_table() -> MaterialDataTable<'static> {
940    MaterialDataTable::new()
941}