Skip to main content

egui_material3/
datatable.rs

1use crate::button::MaterialButton;
2use crate::theme::get_global_color;
3use egui::{
4    ecolor::Color32,
5    epaint::{CornerRadius, Stroke},
6    FontFamily, FontId, Id, Rect, Response, Sense, Ui, Vec2, Widget, WidgetText,
7};
8use std::collections::{HashMap, HashSet};
9
10/// Theme/styling configuration for MaterialDataTable
11#[derive(Clone, Debug)]
12pub struct DataTableTheme {
13    pub decoration: Option<Color32>,
14    pub heading_row_color: Option<Color32>,
15    pub heading_row_height: Option<f32>,
16    pub heading_text_style: Option<(FontId, Color32)>,
17    pub data_row_color: Option<Color32>,
18    pub data_row_min_height: Option<f32>,
19    pub data_row_max_height: Option<f32>,
20    pub data_text_style: Option<(FontId, Color32)>,
21    pub horizontal_margin: Option<f32>,
22    pub column_spacing: Option<f32>,
23    pub divider_thickness: Option<f32>,
24    pub divider_color: Option<Color32>,
25    pub checkbox_horizontal_margin: Option<f32>,
26    pub border_stroke: Option<Stroke>,
27    pub sort_active_color: Option<Color32>,
28    pub sort_inactive_color: Option<Color32>,
29    pub selected_row_color: Option<Color32>,
30    pub show_bottom_border: bool,
31    pub show_checkbox_column: bool,
32}
33
34impl Default for DataTableTheme {
35    fn default() -> Self {
36        Self {
37            decoration: None,
38            heading_row_color: None,
39            heading_row_height: Some(56.0),
40            heading_text_style: None,
41            data_row_color: None,
42            data_row_min_height: Some(52.0),
43            data_row_max_height: None,
44            data_text_style: None,
45            horizontal_margin: Some(24.0),
46            column_spacing: Some(56.0),
47            divider_thickness: Some(1.0),
48            divider_color: None,
49            checkbox_horizontal_margin: Some(16.0),
50            border_stroke: None,
51            sort_active_color: None,
52            sort_inactive_color: None,
53            selected_row_color: None,
54            show_bottom_border: true,
55            show_checkbox_column: true,
56        }
57    }
58}
59
60/// Column width specification
61#[derive(Clone, Debug, PartialEq)]
62pub enum ColumnWidth {
63    Fixed(f32),
64    Flex(f32),
65}
66
67impl Default for ColumnWidth {
68    fn default() -> Self {
69        ColumnWidth::Fixed(100.0)
70    }
71}
72
73/// Persistent state for a Material Design data table.
74///
75/// This structure maintains the state of the table including selections,
76/// sorting, and editing state across frames.
77#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
78pub struct DataTableState {
79    /// Selection state for each row (true if selected)
80    pub selected_rows: Vec<bool>,
81    /// State of the header checkbox (for select-all functionality)
82    pub header_checkbox: bool,
83    /// Sort states for each column by column name
84    pub column_sorts: HashMap<String, SortDirection>,
85    /// Index of the currently sorted column (if any)
86    pub sorted_column: Option<usize>,
87    /// Current sort direction for the sorted column
88    pub sort_direction: SortDirection,
89    /// Set of row indices currently being edited
90    pub editing_rows: std::collections::HashSet<usize>,
91    /// Temporary edit data for rows being edited (row_index -> cell_values)
92    pub edit_data: HashMap<usize, Vec<String>>,
93}
94
95/// Response returned by the data table widget.
96///
97/// Contains both the standard egui Response and additional table-specific
98/// information about user interactions.
99#[derive(Debug)]
100pub struct DataTableResponse {
101    /// The standard egui widget response
102    pub response: Response,
103    /// Current selection state for each row
104    pub selected_rows: Vec<bool>,
105    /// Current state of the header checkbox
106    pub header_checkbox: bool,
107    /// Index of column that was clicked for sorting (if any)
108    pub column_clicked: Option<usize>,
109    /// Current sort state (column index, direction)
110    pub sort_state: (Option<usize>, SortDirection),
111    /// List of row actions performed (edit, delete, save)
112    pub row_actions: Vec<RowAction>,
113}
114
115/// Actions that can be performed on data table rows.
116#[derive(Debug, Clone)]
117pub enum RowAction {
118    /// User clicked edit button for the specified row
119    Edit(usize),
120    /// User clicked delete button for the specified row
121    Delete(usize),
122    /// User clicked save button for the specified row
123    Save(usize),
124    /// User clicked cancel button for the specified row
125    Cancel(usize),
126}
127
128/// Trait for providing data to a table lazily
129pub trait DataTableSource {
130    fn row_count(&self) -> usize;
131    fn get_row(&self, index: usize) -> Option<DataTableRow<'_>>;
132    fn is_row_count_approximate(&self) -> bool {
133        false
134    }
135    fn selected_row_count(&self) -> usize {
136        0
137    }
138}
139
140/// Material Design data table component.
141///
142/// Data tables display sets of data across rows and columns.
143/// They organize information in a way that's easy to scan.
144///
145/// ```
146/// # egui::__run_test_ui(|ui| {
147/// // Basic data table
148/// let mut table = MaterialDataTable::new()
149///     .column("Name", 120.0, false)
150///     .column("Age", 80.0, true)
151///     .column("City", 100.0, false);
152///
153/// table.row(|row| {
154///     row.cell("John Doe");
155///     row.cell("25");
156///     row.cell("New York");
157/// });
158///
159/// ui.add(table);
160/// # });
161/// ```
162#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
163pub struct MaterialDataTable<'a> {
164    columns: Vec<DataTableColumn>,
165    rows: Vec<DataTableRow<'a>>,
166    id: Option<Id>,
167    allow_selection: bool,
168    sticky_header: bool,
169    progress_visible: bool,
170    corner_radius: CornerRadius,
171    sorted_column: Option<usize>,
172    sort_direction: SortDirection,
173    default_row_height: f32,
174    theme: DataTableTheme,
175    row_hover_states: HashMap<usize, bool>,
176}
177
178#[derive(Clone, Debug, PartialEq)]
179pub enum VAlign {
180    Top,
181    Center,
182    Bottom,
183}
184
185#[derive(Clone, Debug, PartialEq)]
186pub enum HAlign {
187    Left,
188    Center,
189    Right,
190}
191
192impl Default for VAlign {
193    fn default() -> Self {
194        VAlign::Center
195    }
196}
197
198impl Default for HAlign {
199    fn default() -> Self {
200        HAlign::Left
201    }
202}
203
204#[derive(Clone)]
205pub struct DataTableColumn {
206    /// Display title for the column header (can be text or widget closure)
207    pub title: String,
208    /// Optional widget builder for custom header content
209    pub header_widget: Option<std::sync::Arc<dyn Fn(&mut Ui) + Send + Sync>>,
210    /// Fixed width of the column in pixels
211    pub width: f32,
212    /// Whether the column contains numeric data (affects alignment and sorting)
213    pub numeric: bool,
214    /// Whether this column can be sorted by clicking the header
215    pub sortable: bool,
216    /// Current sort direction for this column (if sorted)
217    pub sort_direction: Option<SortDirection>,
218    /// Horizontal alignment for column cells
219    pub h_align: HAlign,
220    /// Vertical alignment for column cells
221    pub v_align: VAlign,
222    /// Tooltip text for column header
223    pub tooltip: Option<String>,
224    /// Heading text alignment (separate from cell alignment)
225    pub heading_alignment: Option<HAlign>,
226    /// Column width specification
227    pub column_width: ColumnWidth,
228}
229
230#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
231pub enum SortDirection {
232    Ascending,
233    Descending,
234}
235
236impl Default for SortDirection {
237    fn default() -> Self {
238        SortDirection::Ascending
239    }
240}
241
242pub enum CellContent {
243    Text(WidgetText),
244    Widget(std::sync::Arc<dyn Fn(&mut Ui) + Send + Sync>),
245}
246
247pub struct DataTableCell {
248    pub content: CellContent,
249    pub h_align: Option<HAlign>,
250    pub v_align: Option<VAlign>,
251    pub placeholder: bool,
252    pub show_edit_icon: bool,
253}
254
255impl DataTableCell {
256    pub fn text(text: impl Into<WidgetText>) -> Self {
257        Self {
258            content: CellContent::Text(text.into()),
259            h_align: None,
260            v_align: None,
261            placeholder: false,
262            show_edit_icon: false,
263        }
264    }
265
266    pub fn widget<F>(f: F) -> Self
267    where
268        F: Fn(&mut Ui) + Send + Sync + 'static,
269    {
270        Self {
271            content: CellContent::Widget(std::sync::Arc::new(f)),
272            h_align: None,
273            v_align: None,
274            placeholder: false,
275            show_edit_icon: false,
276        }
277    }
278
279    pub fn h_align(mut self, align: HAlign) -> Self {
280        self.h_align = Some(align);
281        self
282    }
283
284    pub fn v_align(mut self, align: VAlign) -> Self {
285        self.v_align = Some(align);
286        self
287    }
288
289    pub fn placeholder(mut self, is_placeholder: bool) -> Self {
290        self.placeholder = is_placeholder;
291        self
292    }
293
294    pub fn show_edit_icon(mut self, show: bool) -> Self {
295        self.show_edit_icon = show;
296        self
297    }
298}
299
300pub struct DataTableRow<'a> {
301    cells: Vec<DataTableCell>,
302    selected: bool,
303    readonly: bool,
304    id: Option<String>,
305    color: Option<Color32>,
306    on_hover: bool,
307    _phantom: std::marker::PhantomData<&'a ()>,
308}
309
310impl<'a> DataTableRow<'a> {
311    pub fn new() -> Self {
312        Self {
313            cells: Vec::new(),
314            selected: false,
315            readonly: false,
316            id: None,
317            color: None,
318            on_hover: true,
319            _phantom: std::marker::PhantomData,
320        }
321    }
322
323    /// Add a text cell
324    pub fn cell(mut self, text: impl Into<WidgetText>) -> Self {
325        self.cells.push(DataTableCell::text(text));
326        self
327    }
328
329    /// Add a custom cell with full control
330    pub fn custom_cell(mut self, cell: DataTableCell) -> Self {
331        self.cells.push(cell);
332        self
333    }
334
335    /// Add a widget cell
336    pub fn widget_cell<F>(mut self, f: F) -> Self
337    where
338        F: Fn(&mut Ui) + Send + Sync + 'static,
339    {
340        self.cells.push(DataTableCell::widget(f));
341        self
342    }
343
344    pub fn selected(mut self, selected: bool) -> Self {
345        self.selected = selected;
346        self
347    }
348
349    pub fn readonly(mut self, readonly: bool) -> Self {
350        self.readonly = readonly;
351        self
352    }
353
354    pub fn id(mut self, id: impl Into<String>) -> Self {
355        self.id = Some(id.into());
356        self
357    }
358
359    pub fn color(mut self, color: Color32) -> Self {
360        self.color = Some(color);
361        self
362    }
363
364    pub fn on_hover(mut self, hover: bool) -> Self {
365        self.on_hover = hover;
366        self
367    }
368}
369
370impl<'a> MaterialDataTable<'a> {
371    /// Create a new data table.
372    pub fn new() -> Self {
373        Self {
374            columns: Vec::new(),
375            rows: Vec::new(),
376            id: None,
377            allow_selection: false,
378            sticky_header: false,
379            progress_visible: false,
380            corner_radius: CornerRadius::from(4.0),
381            sorted_column: None,
382            sort_direction: SortDirection::Ascending,
383            default_row_height: 52.0,
384            theme: DataTableTheme::default(),
385            row_hover_states: HashMap::new(),
386        }
387    }
388
389    /// Set the initial sort column and direction
390    pub fn sort_by(mut self, column_index: usize, direction: SortDirection) -> Self {
391        self.sorted_column = Some(column_index);
392        self.sort_direction = direction;
393        self
394    }
395
396    /// Get current sorting state
397    pub fn get_sort_state(&self) -> (Option<usize>, SortDirection) {
398        (self.sorted_column, self.sort_direction.clone())
399    }
400
401    /// Add a column to the data table.
402    pub fn column(mut self, title: impl Into<String>, width: f32, numeric: bool) -> Self {
403        self.columns.push(DataTableColumn {
404            title: title.into(),
405            header_widget: None,
406            width,
407            numeric,
408            sortable: true, // Make all columns sortable by default
409            sort_direction: None,
410            h_align: if numeric { HAlign::Right } else { HAlign::Left },
411            v_align: VAlign::Center,
412            tooltip: None,
413            heading_alignment: None,
414            column_width: ColumnWidth::Fixed(width),
415        });
416        self
417    }
418
419    /// Add a sortable column to the data table.
420    pub fn sortable_column(mut self, title: impl Into<String>, width: f32, numeric: bool) -> Self {
421        self.columns.push(DataTableColumn {
422            title: title.into(),
423            header_widget: None,
424            width,
425            numeric,
426            sortable: true,
427            sort_direction: None,
428            h_align: if numeric { HAlign::Right } else { HAlign::Left },
429            v_align: VAlign::Center,
430            tooltip: None,
431            heading_alignment: None,
432            column_width: ColumnWidth::Fixed(width),
433        });
434        self
435    }
436
437    pub fn sortable_column_with_align(
438        mut self,
439        title: impl Into<String>,
440        width: f32,
441        numeric: bool,
442        h_align: HAlign,
443        v_align: VAlign,
444    ) -> Self {
445        self.columns.push(DataTableColumn {
446            title: title.into(),
447            header_widget: None,
448            width,
449            numeric,
450            sortable: true,
451            sort_direction: None,
452            h_align,
453            v_align,
454            tooltip: None,
455            heading_alignment: None,
456            column_width: ColumnWidth::Fixed(width),
457        });
458        self
459    }
460
461    /// Add a column with custom alignment
462    pub fn column_with_align(
463        mut self,
464        title: impl Into<String>,
465        width: f32,
466        numeric: bool,
467        h_align: HAlign,
468        v_align: VAlign,
469    ) -> Self {
470        self.columns.push(DataTableColumn {
471            title: title.into(),
472            header_widget: None,
473            width,
474            numeric,
475            sortable: true,
476            sort_direction: None,
477            h_align,
478            v_align,
479            tooltip: None,
480            heading_alignment: None,
481            column_width: ColumnWidth::Fixed(width),
482        });
483        self
484    }
485
486    /// Add a row using a builder pattern.
487    pub fn row<F>(mut self, f: F) -> Self
488    where
489        F: FnOnce(DataTableRow<'a>) -> DataTableRow<'a>,
490    {
491        let row = f(DataTableRow::new());
492        self.rows.push(row);
493        self
494    }
495
496    /// Set the ID for state persistence.
497    pub fn id(mut self, id: impl Into<Id>) -> Self {
498        self.id = Some(id.into());
499        self
500    }
501
502    /// Enable row selection.
503    pub fn allow_selection(mut self, allow: bool) -> Self {
504        self.allow_selection = allow;
505        self
506    }
507
508    /// Make the header sticky.
509    pub fn sticky_header(mut self, sticky: bool) -> Self {
510        self.sticky_header = sticky;
511        self
512    }
513
514    /// Show progress indicator.
515    pub fn show_progress(mut self, show: bool) -> Self {
516        self.progress_visible = show;
517        self
518    }
519
520    /// Set corner radius.
521    pub fn corner_radius(mut self, corner_radius: impl Into<CornerRadius>) -> Self {
522        self.corner_radius = corner_radius.into();
523        self
524    }
525
526    /// Set default row height in pixels.
527    pub fn default_row_height(mut self, height: f32) -> Self {
528        self.default_row_height = height;
529        self
530    }
531
532    /// Set custom theme for this table.
533    pub fn theme(mut self, theme: DataTableTheme) -> Self {
534        self.theme = theme;
535        self
536    }
537
538    fn get_table_style(&self) -> (Color32, Stroke) {
539        let md_surface = self.theme.decoration.unwrap_or_else(|| get_global_color("surface"));
540        let md_outline = get_global_color("outline");
541        let border_stroke = self.theme.border_stroke.unwrap_or_else(|| Stroke::new(1.0, md_outline));
542        (md_surface, border_stroke)
543    }
544
545    /// Show the data table and return both UI response and selection state
546    pub fn show(self, ui: &mut Ui) -> DataTableResponse {
547        let (background_color, border_stroke) = self.get_table_style();
548
549        // Generate table ID for state persistence
550        let table_id = self.id.unwrap_or_else(|| {
551            use std::collections::hash_map::DefaultHasher;
552            use std::hash::{Hash, Hasher};
553            let mut hasher = DefaultHasher::new();
554
555            // Hash based on columns and first few rows for uniqueness
556            for col in &self.columns {
557                col.title.hash(&mut hasher);
558                col.width.to_bits().hash(&mut hasher);
559            }
560            for (i, row) in self.rows.iter().take(3).enumerate() {
561                i.hash(&mut hasher);
562                for cell in &row.cells {
563                    match &cell.content {
564                        CellContent::Text(t) => t.text().hash(&mut hasher),
565                        CellContent::Widget(_) => "widget".hash(&mut hasher),
566                    }
567                }
568            }
569            self.rows.len().hash(&mut hasher);
570            Id::new(format!("datatable_{}", hasher.finish()))
571        });
572
573        // Get or create persistent state
574        let mut state: DataTableState =
575            ui.data_mut(|d| d.get_persisted(table_id).unwrap_or_default());
576
577        // Get external editing state from UI memory if available
578        if let Some(external_editing_state) = ui.memory(|mem| {
579            mem.data
580                .get_temp::<(HashSet<usize>, HashMap<usize, Vec<String>>)>(
581                    table_id.with("external_edit_state"),
582                )
583        }) {
584            state.editing_rows = external_editing_state.0;
585            state.edit_data = external_editing_state.1;
586        }
587
588        // Initialize sorting state from widget if not set
589        if state.sorted_column.is_none() && self.sorted_column.is_some() {
590            state.sorted_column = self.sorted_column;
591            state.sort_direction = self.sort_direction.clone();
592        }
593
594        // Ensure state vectors match current row count
595        if state.selected_rows.len() != self.rows.len() {
596            state.selected_rows.resize(self.rows.len(), false);
597        }
598
599        // Sync selection state from rows - always update to match external state
600        for (i, row) in self.rows.iter().enumerate() {
601            if i < state.selected_rows.len() {
602                state.selected_rows[i] = row.selected;
603            }
604        }
605
606        let MaterialDataTable {
607            columns,
608            mut rows,
609            allow_selection,
610            sticky_header: _,
611            progress_visible,
612            corner_radius,
613            default_row_height,
614            theme,
615            ..
616        } = self;
617
618        // Sort rows if a column is selected for sorting
619        if let Some(sort_col_idx) = state.sorted_column {
620            if let Some(sort_column) = columns.get(sort_col_idx) {
621                rows.sort_by(|a, b| {
622                    let cell_a_text = a
623                        .cells
624                        .get(sort_col_idx)
625                        .and_then(|c| match &c.content {
626                            CellContent::Text(t) => Some(t.text()),
627                            CellContent::Widget(_) => None,
628                        })
629                        .unwrap_or("");
630                    let cell_b_text = b
631                        .cells
632                        .get(sort_col_idx)
633                        .and_then(|c| match &c.content {
634                            CellContent::Text(t) => Some(t.text()),
635                            CellContent::Widget(_) => None,
636                        })
637                        .unwrap_or("");
638
639                    let comparison = if sort_column.numeric {
640                        // Try to parse as numbers for numeric columns
641                        let a_num: f64 = cell_a_text.trim_start_matches('$').parse().unwrap_or(0.0);
642                        let b_num: f64 = cell_b_text.trim_start_matches('$').parse().unwrap_or(0.0);
643                        a_num
644                            .partial_cmp(&b_num)
645                            .unwrap_or(std::cmp::Ordering::Equal)
646                    } else {
647                        // Alphabetical comparison for text columns
648                        cell_a_text.cmp(cell_b_text)
649                    };
650
651                    match state.sort_direction {
652                        SortDirection::Ascending => comparison,
653                        SortDirection::Descending => comparison.reverse(),
654                    }
655                });
656            }
657        }
658
659        // Calculate table dimensions with dynamic row heights
660        let checkbox_width = if allow_selection && theme.show_checkbox_column { 48.0 } else { 0.0 };
661        let total_width = checkbox_width + columns.iter().map(|col| col.width).sum::<f32>();
662        let min_row_height = theme.data_row_min_height.unwrap_or(default_row_height);
663        let min_header_height = theme.heading_row_height.unwrap_or(56.0);
664
665        // Calculate header height with text wrapping
666        let mut header_height: f32 = min_header_height;
667        for column in &columns {
668            let available_width = column.width - 48.0; // Account for padding and sort icon
669            let header_font = FontId::new(16.0, FontFamily::Proportional);
670
671            let galley = ui.fonts(|f| {
672                f.layout_job(egui::text::LayoutJob {
673                    text: column.title.clone(),
674                    sections: vec![egui::text::LayoutSection {
675                        leading_space: 0.0,
676                        byte_range: 0..column.title.len(),
677                        format: egui::TextFormat {
678                            font_id: header_font,
679                            color: get_global_color("onSurface"),
680                            ..Default::default()
681                        },
682                    }],
683                    wrap: egui::text::TextWrapping {
684                        max_width: available_width,
685                        ..Default::default()
686                    },
687                    break_on_newline: true,
688                    halign: egui::Align::LEFT,
689                    justify: false,
690                    first_row_min_height: 0.0,
691                    round_output_to_gui: true,
692                })
693            });
694
695            let content_height: f32 = galley.size().y + 16.0; // Add padding
696            header_height = header_height.max(content_height);
697        }
698
699        // Calculate individual row heights based on content
700        let mut row_heights = Vec::new();
701        for row in &rows {
702            let mut max_height: f32 = min_row_height;
703            for (cell_idx, cell) in row.cells.iter().enumerate() {
704                if let Some(column) = columns.get(cell_idx) {
705                    match &cell.content {
706                        CellContent::Text(cell_text) => {
707                            let available_width = column.width - 32.0;
708                            let cell_font = FontId::new(14.0, FontFamily::Proportional);
709
710                            let galley = ui.fonts(|f| {
711                                f.layout_job(egui::text::LayoutJob {
712                                    text: cell_text.text().to_string(),
713                                    sections: vec![egui::text::LayoutSection {
714                                        leading_space: 0.0,
715                                        byte_range: 0..cell_text.text().len(),
716                                        format: egui::TextFormat {
717                                            font_id: cell_font,
718                                            color: get_global_color("onSurface"),
719                                            ..Default::default()
720                                        },
721                                    }],
722                                    wrap: egui::text::TextWrapping {
723                                        max_width: available_width,
724                                        ..Default::default()
725                                    },
726                                    break_on_newline: true,
727                                    halign: egui::Align::LEFT, // Always left-align within galley; positioning handles cell alignment
728                                    justify: false,
729                                    first_row_min_height: 0.0,
730                                    round_output_to_gui: true,
731                                })
732                            });
733
734                            let content_height: f32 = galley.size().y + 16.0; // Add padding
735                            max_height = max_height.max(content_height);
736                        }
737                        CellContent::Widget(_) => {
738                            // For widgets, use minimum height - they will size themselves
739                            // We could make this configurable in the future
740                        }
741                    }
742                }
743            }
744            row_heights.push(max_height);
745        }
746
747        let total_height = header_height + row_heights.iter().sum::<f32>();
748
749        // Collect all row actions from this frame
750        let mut all_row_actions: Vec<RowAction> = Vec::new();
751
752        // Apply Material theme styling
753        let surface = get_global_color("surface");
754        let on_surface = get_global_color("onSurface");
755        let primary = get_global_color("primary");
756
757        let mut style = (*ui.ctx().style()).clone();
758        style.visuals.widgets.noninteractive.bg_fill = surface;
759        style.visuals.widgets.inactive.bg_fill = surface;
760        style.visuals.widgets.hovered.bg_fill =
761            egui::Color32::from_rgba_premultiplied(primary.r(), primary.g(), primary.b(), 20);
762        style.visuals.widgets.active.bg_fill =
763            egui::Color32::from_rgba_premultiplied(primary.r(), primary.g(), primary.b(), 40);
764        style.visuals.selection.bg_fill = primary;
765        style.visuals.widgets.noninteractive.fg_stroke.color = on_surface;
766        style.visuals.widgets.inactive.fg_stroke.color = on_surface;
767        style.visuals.widgets.hovered.fg_stroke.color = on_surface;
768        style.visuals.widgets.active.fg_stroke.color = on_surface;
769        style.visuals.striped = true;
770        style.visuals.faint_bg_color = egui::Color32::from_rgba_premultiplied(
771            on_surface.r(),
772            on_surface.g(),
773            on_surface.b(),
774            10,
775        );
776        ui.ctx().set_style(style);
777
778        let desired_size = Vec2::new(total_width, total_height);
779        let response = ui.allocate_response(desired_size, Sense::click());
780        let rect = response.rect;
781
782        if ui.is_rect_visible(rect) {
783            // Draw table background
784            ui.painter()
785                .rect_filled(rect, corner_radius, background_color);
786            ui.painter().rect_stroke(
787                rect,
788                corner_radius,
789                border_stroke,
790                egui::epaint::StrokeKind::Outside,
791            );
792
793            let mut current_y = rect.min.y;
794
795            // Draw header
796            let header_rect = Rect::from_min_size(rect.min, Vec2::new(total_width, header_height));
797            let header_bg = theme.heading_row_color.unwrap_or_else(|| get_global_color("surfaceVariant"));
798            ui.painter()
799                .rect_filled(header_rect, CornerRadius::ZERO, header_bg);
800
801            let mut current_x = rect.min.x;
802
803            // Header checkbox
804            if allow_selection && theme.show_checkbox_column {
805                let checkbox_rect = Rect::from_min_size(
806                    egui::pos2(current_x, current_y),
807                    Vec2::new(checkbox_width, header_height),
808                );
809
810                let checkbox_center = checkbox_rect.center();
811                let checkbox_size = Vec2::splat(18.0);
812                let checkbox_inner_rect = Rect::from_center_size(checkbox_center, checkbox_size);
813
814                let checkbox_color = if state.header_checkbox {
815                    get_global_color("primary")
816                } else {
817                    Color32::TRANSPARENT
818                };
819
820                ui.painter().rect_filled(
821                    checkbox_inner_rect,
822                    CornerRadius::from(2.0),
823                    checkbox_color,
824                );
825                ui.painter().rect_stroke(
826                    checkbox_inner_rect,
827                    CornerRadius::from(2.0),
828                    Stroke::new(2.0, get_global_color("outline")),
829                    egui::epaint::StrokeKind::Outside,
830                );
831
832                if state.header_checkbox {
833                    // Draw checkmark
834                    let check_points = [
835                        checkbox_inner_rect.min + Vec2::new(4.0, 9.0),
836                        checkbox_inner_rect.min + Vec2::new(8.0, 13.0),
837                        checkbox_inner_rect.min + Vec2::new(14.0, 5.0),
838                    ];
839                    ui.painter().line_segment(
840                        [check_points[0], check_points[1]],
841                        Stroke::new(2.0, Color32::WHITE),
842                    );
843                    ui.painter().line_segment(
844                        [check_points[1], check_points[2]],
845                        Stroke::new(2.0, Color32::WHITE),
846                    );
847                }
848
849                // Handle header checkbox click
850                let header_checkbox_id = table_id.with("header_checkbox");
851                let checkbox_response =
852                    ui.interact(checkbox_inner_rect, header_checkbox_id, Sense::click());
853                if checkbox_response.clicked() {
854                    state.header_checkbox = !state.header_checkbox;
855                    // Only update non-readonly rows
856                    for (idx, selected) in state.selected_rows.iter_mut().enumerate() {
857                        if let Some(row) = rows.get(idx) {
858                            if !row.readonly {
859                                *selected = state.header_checkbox;
860                            }
861                        }
862                    }
863                }
864
865                current_x += checkbox_width;
866            }
867
868            // Header columns
869            for (col_idx, column) in columns.iter().enumerate() {
870                let col_rect = Rect::from_min_size(
871                    egui::pos2(current_x, current_y),
872                    Vec2::new(column.width, header_height),
873                );
874
875                // Render header text with wrapping support
876                let available_width = column.width - 48.0; // Account for padding and sort icon
877                let header_font = FontId::new(16.0, FontFamily::Proportional);
878
879                let galley = ui.fonts(|f| {
880                    f.layout_job(egui::text::LayoutJob {
881                        text: column.title.clone(),
882                        sections: vec![egui::text::LayoutSection {
883                            leading_space: 0.0,
884                            byte_range: 0..column.title.len(),
885                            format: egui::TextFormat {
886                                font_id: header_font,
887                                color: get_global_color("onSurface"),
888                                ..Default::default()
889                            },
890                        }],
891                        wrap: egui::text::TextWrapping {
892                            max_width: available_width,
893                            ..Default::default()
894                        },
895                        break_on_newline: true,
896                        halign: egui::Align::LEFT,
897                        justify: false,
898                        first_row_min_height: 0.0,
899                        round_output_to_gui: true,
900                    })
901                });
902
903                let text_pos = egui::pos2(
904                    current_x + 16.0,
905                    current_y + (header_height - galley.size().y) / 2.0,
906                );
907
908                ui.painter()
909                    .galley(text_pos, galley, get_global_color("onSurface"));
910
911                // Handle column header clicks for sorting
912                if column.sortable {
913                    let header_click_id = table_id.with(format!("column_header_{}", col_idx));
914                    let mut header_response = ui.interact(col_rect, header_click_id, Sense::click());
915                    
916                    // Show tooltip if available
917                    if let Some(ref tooltip) = column.tooltip {
918                        header_response = header_response.on_hover_text(tooltip);
919                    }
920                    
921                    if header_response.clicked() {
922                        // Handle sorting logic
923                        if state.sorted_column == Some(col_idx) {
924                            // Same column clicked, toggle direction
925                            state.sort_direction = match state.sort_direction {
926                                SortDirection::Ascending => SortDirection::Descending,
927                                SortDirection::Descending => SortDirection::Ascending,
928                            };
929                        } else {
930                            // New column clicked
931                            state.sorted_column = Some(col_idx);
932                            state.sort_direction = SortDirection::Ascending;
933                        }
934                        ui.memory_mut(|mem| {
935                            mem.data
936                                .insert_temp(table_id.with("column_clicked"), Some(col_idx));
937                        });
938                    }
939
940                    let icon_pos = egui::pos2(
941                        current_x + column.width - 32.0,
942                        current_y + (header_height - 24.0) / 2.0,
943                    );
944                    let icon_rect = Rect::from_min_size(icon_pos, Vec2::splat(24.0));
945
946                    // Determine if this column is currently sorted
947                    let is_sorted = state.sorted_column == Some(col_idx);
948                    let sort_direction = if is_sorted {
949                        Some(&state.sort_direction)
950                    } else {
951                        None
952                    };
953
954                    // Draw sort arrow with enhanced visual feedback
955                    let arrow_color = if is_sorted {
956                        theme.sort_active_color.unwrap_or_else(|| get_global_color("primary")) // Highlight active sort column
957                    } else {
958                        theme.sort_inactive_color.unwrap_or_else(|| get_global_color("onSurfaceVariant"))
959                    };
960
961                    let center = icon_rect.center();
962
963                    // Draw triangle arrows
964                    match sort_direction {
965                        Some(SortDirection::Ascending) => {
966                            // Up triangle (▲)
967                            let points = [
968                                center + Vec2::new(0.0, -6.0), // Top point
969                                center + Vec2::new(-5.0, 4.0), // Bottom left
970                                center + Vec2::new(5.0, 4.0),  // Bottom right
971                            ];
972                            ui.painter().line_segment(
973                                [points[0], points[1]],
974                                Stroke::new(2.0, arrow_color),
975                            );
976                            ui.painter().line_segment(
977                                [points[1], points[2]],
978                                Stroke::new(2.0, arrow_color),
979                            );
980                            ui.painter().line_segment(
981                                [points[2], points[0]],
982                                Stroke::new(2.0, arrow_color),
983                            );
984                        }
985                        Some(SortDirection::Descending) => {
986                            // Down triangle (▼)
987                            let points = [
988                                center + Vec2::new(0.0, 6.0),   // Bottom point
989                                center + Vec2::new(-5.0, -4.0), // Top left
990                                center + Vec2::new(5.0, -4.0),  // Top right
991                            ];
992                            ui.painter().line_segment(
993                                [points[0], points[1]],
994                                Stroke::new(2.0, arrow_color),
995                            );
996                            ui.painter().line_segment(
997                                [points[1], points[2]],
998                                Stroke::new(2.0, arrow_color),
999                            );
1000                            ui.painter().line_segment(
1001                                [points[2], points[0]],
1002                                Stroke::new(2.0, arrow_color),
1003                            );
1004                        }
1005                        None => {
1006                            // Neutral state - show both arrows faintly
1007                            let light_color = arrow_color.gamma_multiply(0.5);
1008                            // Up triangle
1009                            let up_points = [
1010                                center + Vec2::new(0.0, -8.0),
1011                                center + Vec2::new(-3.0, -2.0),
1012                                center + Vec2::new(3.0, -2.0),
1013                            ];
1014                            ui.painter().line_segment(
1015                                [up_points[0], up_points[1]],
1016                                Stroke::new(1.0, light_color),
1017                            );
1018                            ui.painter().line_segment(
1019                                [up_points[1], up_points[2]],
1020                                Stroke::new(1.0, light_color),
1021                            );
1022                            ui.painter().line_segment(
1023                                [up_points[2], up_points[0]],
1024                                Stroke::new(1.0, light_color),
1025                            );
1026
1027                            // Down triangle
1028                            let down_points = [
1029                                center + Vec2::new(0.0, 8.0),
1030                                center + Vec2::new(-3.0, 2.0),
1031                                center + Vec2::new(3.0, 2.0),
1032                            ];
1033                            ui.painter().line_segment(
1034                                [down_points[0], down_points[1]],
1035                                Stroke::new(1.0, light_color),
1036                            );
1037                            ui.painter().line_segment(
1038                                [down_points[1], down_points[2]],
1039                                Stroke::new(1.0, light_color),
1040                            );
1041                            ui.painter().line_segment(
1042                                [down_points[2], down_points[0]],
1043                                Stroke::new(1.0, light_color),
1044                            );
1045                        }
1046                    }
1047                }
1048
1049                current_x += column.width;
1050            }
1051
1052            current_y += header_height;
1053
1054            // Draw rows with dynamic heights
1055            for (row_idx, row) in rows.iter().enumerate() {
1056                let row_height = row_heights.get(row_idx).copied().unwrap_or(min_row_height);
1057                let row_rect = Rect::from_min_size(
1058                    egui::pos2(rect.min.x, current_y),
1059                    Vec2::new(total_width, row_height),
1060                );
1061
1062                let row_selected = state.selected_rows.get(row_idx).copied().unwrap_or(false);
1063                
1064                // Determine row background color with priority: custom color > selected > readonly > alternating
1065                let row_bg = if let Some(custom_color) = row.color {
1066                    custom_color
1067                } else if row_selected {
1068                    theme.selected_row_color.unwrap_or_else(|| get_global_color("primaryContainer"))
1069                } else if row.readonly {
1070                    // Subtle background for readonly rows
1071                    let surface_variant = get_global_color("surfaceVariant");
1072                    Color32::from_rgba_premultiplied(
1073                        surface_variant.r(),
1074                        surface_variant.g(),
1075                        surface_variant.b(),
1076                        (surface_variant.a() as f32 * 0.3) as u8,
1077                    )
1078                } else if row_idx % 2 == 1 {
1079                    theme.data_row_color.unwrap_or_else(|| get_global_color("surfaceVariant"))
1080                } else {
1081                    background_color
1082                };
1083
1084                ui.painter()
1085                    .rect_filled(row_rect, CornerRadius::ZERO, row_bg);
1086                    
1087                // Draw divider below row
1088                if row_idx < rows.len() - 1 || theme.show_bottom_border {
1089                    let divider_y = current_y + row_height;
1090                    let divider_thickness = theme.divider_thickness.unwrap_or(1.0);
1091                    let divider_color = theme.divider_color.unwrap_or_else(|| get_global_color("outlineVariant"));
1092                    ui.painter().line_segment(
1093                        [
1094                            egui::pos2(rect.min.x, divider_y),
1095                            egui::pos2(rect.min.x + total_width, divider_y),
1096                        ],
1097                        Stroke::new(divider_thickness, divider_color),
1098                    );
1099                }
1100
1101                current_x = rect.min.x;
1102
1103                // Row checkbox
1104                if allow_selection && theme.show_checkbox_column {
1105                    let checkbox_rect = Rect::from_min_size(
1106                        egui::pos2(current_x, current_y),
1107                        Vec2::new(checkbox_width, row_height),
1108                    );
1109
1110                    let checkbox_center = checkbox_rect.center();
1111                    let checkbox_size = Vec2::splat(18.0);
1112                    let checkbox_inner_rect =
1113                        Rect::from_center_size(checkbox_center, checkbox_size);
1114
1115                    let checkbox_color = if row_selected {
1116                        get_global_color("primary")
1117                    } else {
1118                        Color32::TRANSPARENT
1119                    };
1120
1121                    let border_color = if row.readonly {
1122                        get_global_color("outline").linear_multiply(0.5) // Dimmed for readonly
1123                    } else {
1124                        get_global_color("outline")
1125                    };
1126
1127                    ui.painter().rect_filled(
1128                        checkbox_inner_rect,
1129                        CornerRadius::from(2.0),
1130                        checkbox_color,
1131                    );
1132                    ui.painter().rect_stroke(
1133                        checkbox_inner_rect,
1134                        CornerRadius::from(2.0),
1135                        Stroke::new(2.0, border_color),
1136                        egui::epaint::StrokeKind::Outside,
1137                    );
1138
1139                    if row_selected {
1140                        // Draw checkmark
1141                        let check_points = [
1142                            checkbox_inner_rect.min + Vec2::new(4.0, 9.0),
1143                            checkbox_inner_rect.min + Vec2::new(8.0, 13.0),
1144                            checkbox_inner_rect.min + Vec2::new(14.0, 5.0),
1145                        ];
1146                        ui.painter().line_segment(
1147                            [check_points[0], check_points[1]],
1148                            Stroke::new(2.0, Color32::WHITE),
1149                        );
1150                        ui.painter().line_segment(
1151                            [check_points[1], check_points[2]],
1152                            Stroke::new(2.0, Color32::WHITE),
1153                        );
1154                    }
1155
1156                    // Handle row checkbox click
1157                    let row_checkbox_id = table_id.with(format!("row_checkbox_{}", row_idx));
1158                    let checkbox_response =
1159                        ui.interact(checkbox_inner_rect, row_checkbox_id, Sense::click());
1160                    if checkbox_response.clicked() && !row.readonly {
1161                        if let Some(selected) = state.selected_rows.get_mut(row_idx) {
1162                            *selected = !*selected;
1163                        }
1164
1165                        // Update header checkbox state based on row selections
1166                        // Only consider non-readonly rows for header checkbox state
1167                        let non_readonly_indices: Vec<usize> = rows
1168                            .iter()
1169                            .enumerate()
1170                            .filter(|(_, row)| !row.readonly)
1171                            .map(|(idx, _)| idx)
1172                            .collect();
1173
1174                        if !non_readonly_indices.is_empty() {
1175                            let all_non_readonly_selected = non_readonly_indices
1176                                .iter()
1177                                .all(|&idx| state.selected_rows.get(idx).copied().unwrap_or(false));
1178                            let none_non_readonly_selected =
1179                                non_readonly_indices.iter().all(|&idx| {
1180                                    !state.selected_rows.get(idx).copied().unwrap_or(false)
1181                                });
1182                            state.header_checkbox =
1183                                all_non_readonly_selected && !none_non_readonly_selected;
1184                        }
1185                    }
1186
1187                    current_x += checkbox_width;
1188                }
1189
1190                // Track row actions for this specific row
1191                let mut row_actions: Vec<RowAction> = Vec::new();
1192
1193                // Row cells
1194                for (cell_idx, cell) in row.cells.iter().enumerate() {
1195                    if let Some(column) = columns.get(cell_idx) {
1196                        let _cell_rect = Rect::from_min_size(
1197                            egui::pos2(current_x, current_y),
1198                            Vec2::new(column.width, row_height),
1199                        );
1200
1201                        let is_row_editing = state.editing_rows.contains(&row_idx);
1202                        let is_actions_column = column.title == "Actions";
1203
1204                        if is_actions_column {
1205                            // Render action buttons
1206                            let button_rect = Rect::from_min_size(
1207                                egui::pos2(current_x + 8.0, current_y + (row_height - 32.0) / 2.0),
1208                                Vec2::new(column.width - 16.0, 32.0),
1209                            );
1210
1211                            ui.scope_builder(egui::UiBuilder::new().max_rect(button_rect), |ui| {
1212                                egui::ScrollArea::horizontal()
1213                                    .id_salt(format!("actions_scroll_{}", row_idx))
1214                                    .auto_shrink([false, true])
1215                                    .show(ui, |ui| {
1216                                    ui.horizontal(|ui| {
1217                                        if is_row_editing {
1218                                            if ui.add(MaterialButton::filled("Save").small()).clicked() {
1219                                                row_actions.push(RowAction::Save(row_idx));
1220                                            }
1221                                            if ui.add(MaterialButton::filled("Cancel").small()).clicked() {
1222                                                row_actions.push(RowAction::Cancel(row_idx));
1223                                            }
1224                                        } else {
1225                                            if ui.add(MaterialButton::filled("Edit").small()).clicked() {
1226                                                row_actions.push(RowAction::Edit(row_idx));
1227                                            }
1228                                            if ui.add(MaterialButton::filled("Delete").small()).clicked() {
1229                                                row_actions.push(RowAction::Delete(row_idx));
1230                                            }
1231                                        }
1232                                    });
1233                                });
1234                            });
1235                        } else if is_row_editing {
1236                            // Render editable text field
1237                            let edit_rect = Rect::from_min_size(
1238                                egui::pos2(current_x + 8.0, current_y + (row_height - 24.0) / 2.0),
1239                                Vec2::new(column.width - 16.0, 24.0),
1240                            );
1241
1242                            // Get or initialize edit data
1243                            let edit_data = state.edit_data.entry(row_idx).or_insert_with(|| {
1244                                row.cells
1245                                    .iter()
1246                                    .map(|c| match &c.content {
1247                                        CellContent::Text(t) => t.text().to_string(),
1248                                        CellContent::Widget(_) => String::new(),
1249                                    })
1250                                    .collect()
1251                            });
1252
1253                            // Ensure we have enough entries for this cell
1254                            if edit_data.len() <= cell_idx {
1255                                edit_data.resize(cell_idx + 1, String::new());
1256                            }
1257
1258                            let edit_text = &mut edit_data[cell_idx];
1259
1260                            ui.scope_builder(egui::UiBuilder::new().max_rect(edit_rect), |ui| {
1261                                ui.add(
1262                                    egui::TextEdit::singleline(edit_text)
1263                                        .desired_width(column.width - 16.0),
1264                                );
1265                            });
1266                        } else {
1267                            // Determine alignment from cell or column
1268                            let h_align = cell.h_align.as_ref().unwrap_or(&column.h_align);
1269                            let v_align = cell.v_align.as_ref().unwrap_or(&column.v_align);
1270
1271                            match &cell.content {
1272                                CellContent::Text(cell_text) => {
1273                                    // Render normal text with alignment
1274                                    let available_width = column.width - 32.0; // Account for padding
1275                                    let cell_font = if let Some((ref font_id, _)) = theme.data_text_style {
1276                                        font_id.clone()
1277                                    } else {
1278                                        FontId::new(14.0, FontFamily::Proportional)
1279                                    };
1280                                    
1281                                    let text_color = if cell.placeholder {
1282                                        let base_color = get_global_color("onSurface");
1283                                        Color32::from_rgba_premultiplied(
1284                                            base_color.r(),
1285                                            base_color.g(),
1286                                            base_color.b(),
1287                                            (base_color.a() as f32 * 0.6) as u8,
1288                                        )
1289                                    } else if let Some((_, ref color)) = theme.data_text_style {
1290                                        *color
1291                                    } else {
1292                                        get_global_color("onSurface")
1293                                    };
1294
1295                                    let galley = ui.fonts(|f| {
1296                                        f.layout_job(egui::text::LayoutJob {
1297                                            text: cell_text.text().to_string(),
1298                                            sections: vec![egui::text::LayoutSection {
1299                                                leading_space: 0.0,
1300                                                byte_range: 0..cell_text.text().len(),
1301                                                format: egui::TextFormat {
1302                                                    font_id: cell_font,
1303                                                    color: text_color,
1304                                                    ..Default::default()
1305                                                },
1306                                            }],
1307                                            wrap: egui::text::TextWrapping {
1308                                                max_width: available_width,
1309                                                ..Default::default()
1310                                            },
1311                                            break_on_newline: true,
1312                                            halign: egui::Align::LEFT, // Always left-align within galley; positioning handles cell alignment
1313                                            justify: false,
1314                                            first_row_min_height: 0.0,
1315                                            round_output_to_gui: true,
1316                                        })
1317                                    });
1318
1319                                    // Calculate horizontal position based on alignment
1320                                    let text_x = match h_align {
1321                                        HAlign::Left => current_x + 16.0,
1322                                        HAlign::Center => {
1323                                            current_x + (column.width - galley.size().x) / 2.0
1324                                        }
1325                                        HAlign::Right => {
1326                                            current_x + column.width - 16.0 - galley.size().x
1327                                        }
1328                                    };
1329
1330                                    // Calculate vertical position based on alignment
1331                                    let text_y = match v_align {
1332                                        VAlign::Top => current_y + 8.0,
1333                                        VAlign::Center => {
1334                                            current_y + (row_height - galley.size().y) / 2.0
1335                                        }
1336                                        VAlign::Bottom => {
1337                                            current_y + row_height - galley.size().y - 8.0
1338                                        }
1339                                    };
1340
1341                                    let text_pos = egui::pos2(text_x, text_y);
1342                                    ui.painter().galley(
1343                                        text_pos,
1344                                        galley,
1345                                        text_color,
1346                                    );
1347                                    
1348                                    // Draw edit icon if requested
1349                                    if cell.show_edit_icon {
1350                                        let icon_size = 16.0;
1351                                        let icon_x = current_x + column.width - icon_size - 8.0;
1352                                        let icon_y = current_y + (row_height - icon_size) / 2.0;
1353                                        let icon_rect = Rect::from_min_size(
1354                                            egui::pos2(icon_x, icon_y),
1355                                            Vec2::splat(icon_size),
1356                                        );
1357                                        // Draw simple pencil icon
1358                                        let icon_color = get_global_color("onSurfaceVariant");
1359                                        ui.painter().line_segment(
1360                                            [
1361                                                icon_rect.left_top() + Vec2::new(4.0, 10.0),
1362                                                icon_rect.left_top() + Vec2::new(10.0, 4.0),
1363                                            ],
1364                                            Stroke::new(1.5, icon_color),
1365                                        );
1366                                        ui.painter().line_segment(
1367                                            [
1368                                                icon_rect.left_top() + Vec2::new(2.0, 12.0),
1369                                                icon_rect.left_top() + Vec2::new(4.0, 10.0),
1370                                            ],
1371                                            Stroke::new(1.5, icon_color),
1372                                        );
1373                                    }
1374                                }
1375                                CellContent::Widget(widget_fn) => {
1376                                    // Render custom widget
1377                                    // Calculate widget rect based on alignment
1378                                    let padding = 8.0;
1379                                    let available_width = column.width - 2.0 * padding;
1380                                    let available_height = row_height - 2.0 * padding;
1381
1382                                    // For now, center the widget area. Alignment can be refined based on widget's actual size
1383                                    let widget_rect = match (h_align, v_align) {
1384                                        (HAlign::Left, VAlign::Top) => Rect::from_min_size(
1385                                            egui::pos2(current_x + padding, current_y + padding),
1386                                            Vec2::new(available_width, available_height),
1387                                        ),
1388                                        (HAlign::Center, VAlign::Center) => Rect::from_min_size(
1389                                            egui::pos2(current_x + padding, current_y + padding),
1390                                            Vec2::new(available_width, available_height),
1391                                        ),
1392                                        (HAlign::Right, VAlign::Center) => Rect::from_min_size(
1393                                            egui::pos2(current_x + padding, current_y + padding),
1394                                            Vec2::new(available_width, available_height),
1395                                        ),
1396                                        _ => Rect::from_min_size(
1397                                            egui::pos2(current_x + padding, current_y + padding),
1398                                            Vec2::new(available_width, available_height),
1399                                        ),
1400                                    };
1401
1402                                    ui.scope_builder(
1403                                        egui::UiBuilder::new().max_rect(widget_rect),
1404                                        |ui| {
1405                                            // Apply alignment to the UI
1406                                            match h_align {
1407                                                HAlign::Left => ui.with_layout(
1408                                                    egui::Layout::left_to_right(egui::Align::Min),
1409                                                    |ui| {
1410                                                        widget_fn(ui);
1411                                                    },
1412                                                ),
1413                                                HAlign::Center => ui.with_layout(
1414                                                    egui::Layout::left_to_right(
1415                                                        egui::Align::Center,
1416                                                    ),
1417                                                    |ui| {
1418                                                        widget_fn(ui);
1419                                                    },
1420                                                ),
1421                                                HAlign::Right => ui.with_layout(
1422                                                    egui::Layout::right_to_left(egui::Align::Min),
1423                                                    |ui| {
1424                                                        widget_fn(ui);
1425                                                    },
1426                                                ),
1427                                            };
1428                                        },
1429                                    );
1430                                }
1431                            }
1432                        }
1433
1434                        current_x += column.width;
1435                    }
1436                }
1437
1438                // Add this row's actions to the global collection
1439                all_row_actions.extend(row_actions);
1440
1441                current_y += row_height;
1442            }
1443
1444            // Draw progress indicator if visible
1445            if progress_visible {
1446                let scrim_color = Color32::from_rgba_unmultiplied(255, 255, 255, 128);
1447                ui.painter().rect_filled(rect, corner_radius, scrim_color);
1448
1449                // Draw progress bar
1450                let progress_rect = Rect::from_min_size(
1451                    egui::pos2(rect.min.x, rect.min.y + header_height),
1452                    Vec2::new(total_width, 4.0),
1453                );
1454
1455                let progress_color = get_global_color("primary");
1456                ui.painter()
1457                    .rect_filled(progress_rect, CornerRadius::ZERO, progress_color);
1458            }
1459        }
1460
1461        // Persist the state
1462        ui.data_mut(|d| d.insert_persisted(table_id, state.clone()));
1463
1464        // Store editing state back to memory for external access
1465        ui.memory_mut(|mem| {
1466            mem.data.insert_temp(
1467                table_id.with("external_edit_state"),
1468                (state.editing_rows.clone(), state.edit_data.clone()),
1469            );
1470        });
1471
1472        // Check for column clicks using stored state
1473        let column_clicked = ui
1474            .memory(|mem| {
1475                mem.data
1476                    .get_temp::<Option<usize>>(table_id.with("column_clicked"))
1477            })
1478            .flatten();
1479
1480        // Clear the stored click state
1481        ui.memory_mut(|mem| {
1482            mem.data
1483                .remove::<Option<usize>>(table_id.with("column_clicked"));
1484        });
1485
1486        DataTableResponse {
1487            response,
1488            selected_rows: state.selected_rows,
1489            header_checkbox: state.header_checkbox,
1490            column_clicked,
1491            sort_state: (state.sorted_column, state.sort_direction.clone()),
1492            row_actions: all_row_actions,
1493        }
1494    }
1495}
1496
1497impl<'a> Default for MaterialDataTable<'a> {
1498    fn default() -> Self {
1499        Self::new()
1500    }
1501}
1502
1503impl Widget for MaterialDataTable<'_> {
1504    fn ui(self, ui: &mut Ui) -> Response {
1505        self.show(ui).response
1506    }
1507}
1508
1509/// Convenience function to create a new data table.
1510pub fn data_table() -> MaterialDataTable<'static> {
1511    MaterialDataTable::new()
1512}