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    /// Set of row indices with their drawer expanded
94    pub drawer_open_rows: HashSet<usize>,
95}
96
97/// Response returned by the data table widget.
98///
99/// Contains both the standard egui Response and additional table-specific
100/// information about user interactions.
101#[derive(Debug)]
102pub struct DataTableResponse {
103    /// The standard egui widget response
104    pub response: Response,
105    /// Current selection state for each row
106    pub selected_rows: Vec<bool>,
107    /// Current state of the header checkbox
108    pub header_checkbox: bool,
109    /// Index of column that was clicked for sorting (if any)
110    pub column_clicked: Option<usize>,
111    /// Current sort state (column index, direction)
112    pub sort_state: (Option<usize>, SortDirection),
113    /// List of row actions performed (edit, delete, save)
114    pub row_actions: Vec<RowAction>,
115}
116
117/// Actions that can be performed on data table rows.
118#[derive(Debug, Clone)]
119pub enum RowAction {
120    /// User clicked edit button for the specified row
121    Edit(usize),
122    /// User clicked delete button for the specified row
123    Delete(usize),
124    /// User clicked save button for the specified row
125    Save(usize),
126    /// User clicked cancel button for the specified row
127    Cancel(usize),
128}
129
130/// Trait for providing data to a table lazily
131pub trait DataTableSource {
132    fn row_count(&self) -> usize;
133    fn get_row(&self, index: usize) -> Option<DataTableRow<'_>>;
134    fn is_row_count_approximate(&self) -> bool {
135        false
136    }
137    fn selected_row_count(&self) -> usize {
138        0
139    }
140}
141
142/// Material Design data table component.
143///
144/// Data tables display sets of data across rows and columns.
145/// They organize information in a way that's easy to scan.
146///
147/// ```
148/// # egui::__run_test_ui(|ui| {
149/// // Basic data table
150/// let mut table = MaterialDataTable::new()
151///     .column("Name", 120.0, false)
152///     .column("Age", 80.0, true)
153///     .column("City", 100.0, false);
154///
155/// table.row(|row| {
156///     row.cell("John Doe");
157///     row.cell("25");
158///     row.cell("New York");
159/// });
160///
161/// ui.add(table);
162/// # });
163/// ```
164#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
165pub struct MaterialDataTable<'a> {
166    columns: Vec<DataTableColumn>,
167    rows: Vec<DataTableRow<'a>>,
168    id: Option<Id>,
169    allow_selection: bool,
170    allow_drawer: bool,
171    drawer_row_height: Option<f32>,
172    sticky_header: bool,
173    progress_visible: bool,
174    corner_radius: CornerRadius,
175    sorted_column: Option<usize>,
176    sort_direction: SortDirection,
177    default_row_height: f32,
178    theme: DataTableTheme,
179    row_hover_states: HashMap<usize, bool>,
180    auto_height: bool,
181}
182
183#[derive(Clone, Debug, PartialEq)]
184pub enum VAlign {
185    Top,
186    Center,
187    Bottom,
188}
189
190#[derive(Clone, Debug, PartialEq)]
191pub enum HAlign {
192    Left,
193    Center,
194    Right,
195}
196
197impl Default for VAlign {
198    fn default() -> Self {
199        VAlign::Center
200    }
201}
202
203impl Default for HAlign {
204    fn default() -> Self {
205        HAlign::Left
206    }
207}
208
209#[derive(Clone)]
210pub struct DataTableColumn {
211    /// Display title for the column header (can be text or widget closure)
212    pub title: String,
213    /// Optional widget builder for custom header content
214    pub header_widget: Option<std::sync::Arc<dyn Fn(&mut Ui) + Send + Sync>>,
215    /// Fixed width of the column in pixels
216    pub width: f32,
217    /// Whether the column contains numeric data (affects alignment and sorting)
218    pub numeric: bool,
219    /// Whether this column can be sorted by clicking the header
220    pub sortable: bool,
221    /// Current sort direction for this column (if sorted)
222    pub sort_direction: Option<SortDirection>,
223    /// Horizontal alignment for column cells
224    pub h_align: HAlign,
225    /// Vertical alignment for column cells
226    pub v_align: VAlign,
227    /// Tooltip text for column header
228    pub tooltip: Option<String>,
229    /// Heading text alignment (separate from cell alignment)
230    pub heading_alignment: Option<HAlign>,
231    /// Column width specification
232    pub column_width: ColumnWidth,
233}
234
235#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
236pub enum SortDirection {
237    Ascending,
238    Descending,
239}
240
241impl Default for SortDirection {
242    fn default() -> Self {
243        SortDirection::Ascending
244    }
245}
246
247pub enum CellContent {
248    Text(WidgetText),
249    Widget(std::sync::Arc<dyn Fn(&mut Ui) + Send + Sync>),
250}
251
252pub struct DataTableCell {
253    pub content: CellContent,
254    pub h_align: Option<HAlign>,
255    pub v_align: Option<VAlign>,
256    pub placeholder: bool,
257    pub show_edit_icon: bool,
258}
259
260impl DataTableCell {
261    pub fn text(text: impl Into<WidgetText>) -> Self {
262        Self {
263            content: CellContent::Text(text.into()),
264            h_align: None,
265            v_align: None,
266            placeholder: false,
267            show_edit_icon: false,
268        }
269    }
270
271    pub fn widget<F>(f: F) -> Self
272    where
273        F: Fn(&mut Ui) + Send + Sync + 'static,
274    {
275        Self {
276            content: CellContent::Widget(std::sync::Arc::new(f)),
277            h_align: None,
278            v_align: None,
279            placeholder: false,
280            show_edit_icon: false,
281        }
282    }
283
284    pub fn h_align(mut self, align: HAlign) -> Self {
285        self.h_align = Some(align);
286        self
287    }
288
289    pub fn v_align(mut self, align: VAlign) -> Self {
290        self.v_align = Some(align);
291        self
292    }
293
294    pub fn placeholder(mut self, is_placeholder: bool) -> Self {
295        self.placeholder = is_placeholder;
296        self
297    }
298
299    pub fn show_edit_icon(mut self, show: bool) -> Self {
300        self.show_edit_icon = show;
301        self
302    }
303}
304
305pub struct DataTableRow<'a> {
306    cells: Vec<DataTableCell>,
307    selected: bool,
308    /// True only when `.selected()` was explicitly called.
309    /// Lets the table distinguish externally-managed rows from rows
310    /// whose selection is managed internally by click state.
311    selection_externally_set: bool,
312    readonly: bool,
313    id: Option<String>,
314    color: Option<Color32>,
315    on_hover: bool,
316    /// Optional drawer widget rendered below the row when expanded
317    drawer: Option<std::sync::Arc<dyn Fn(&mut Ui) + Send + Sync>>,
318    _phantom: std::marker::PhantomData<&'a ()>,
319}
320
321impl<'a> DataTableRow<'a> {
322    pub fn new() -> Self {
323        Self {
324            cells: Vec::new(),
325            selected: false,
326            selection_externally_set: false,
327            readonly: false,
328            id: None,
329            color: None,
330            on_hover: true,
331            drawer: None,
332            _phantom: std::marker::PhantomData,
333        }
334    }
335
336    /// Add a text cell
337    pub fn cell(mut self, text: impl Into<WidgetText>) -> Self {
338        self.cells.push(DataTableCell::text(text));
339        self
340    }
341
342    /// Add a custom cell with full control
343    pub fn custom_cell(mut self, cell: DataTableCell) -> Self {
344        self.cells.push(cell);
345        self
346    }
347
348    /// Add a widget cell
349    pub fn widget_cell<F>(mut self, f: F) -> Self
350    where
351        F: Fn(&mut Ui) + Send + Sync + 'static,
352    {
353        self.cells.push(DataTableCell::widget(f));
354        self
355    }
356
357    pub fn selected(mut self, selected: bool) -> Self {
358        self.selected = selected;
359        self.selection_externally_set = true;
360        self
361    }
362
363    pub fn readonly(mut self, readonly: bool) -> Self {
364        self.readonly = readonly;
365        self
366    }
367
368    pub fn id(mut self, id: impl Into<String>) -> Self {
369        self.id = Some(id.into());
370        self
371    }
372
373    pub fn color(mut self, color: Color32) -> Self {
374        self.color = Some(color);
375        self
376    }
377
378    pub fn on_hover(mut self, hover: bool) -> Self {
379        self.on_hover = hover;
380        self
381    }
382
383    /// Set a drawer widget shown below this row when expanded.
384    /// A clickable arrow (> closed / v open) is displayed in the drawer column.
385    pub fn drawer<F>(mut self, f: F) -> Self
386    where
387        F: Fn(&mut Ui) + Send + Sync + 'static,
388    {
389        self.drawer = Some(std::sync::Arc::new(f));
390        self
391    }
392}
393
394impl<'a> MaterialDataTable<'a> {
395    /// Create a new data table.
396    pub fn new() -> Self {
397        Self {
398            columns: Vec::new(),
399            rows: Vec::new(),
400            id: None,
401            allow_selection: false,
402            allow_drawer: false,
403            drawer_row_height: None,
404            sticky_header: false,
405            progress_visible: false,
406            corner_radius: CornerRadius::from(4.0),
407            sorted_column: None,
408            sort_direction: SortDirection::Ascending,
409            default_row_height: 52.0,
410            theme: DataTableTheme::default(),
411            row_hover_states: HashMap::new(),
412            auto_height: false,
413        }
414    }
415
416    /// Set the initial sort column and direction
417    pub fn sort_by(mut self, column_index: usize, direction: SortDirection) -> Self {
418        self.sorted_column = Some(column_index);
419        self.sort_direction = direction;
420        self
421    }
422
423    /// Get current sorting state
424    pub fn get_sort_state(&self) -> (Option<usize>, SortDirection) {
425        (self.sorted_column, self.sort_direction.clone())
426    }
427
428    /// Add a column to the data table.
429    pub fn column(mut self, title: impl Into<String>, width: f32, numeric: bool) -> Self {
430        self.columns.push(DataTableColumn {
431            title: title.into(),
432            header_widget: None,
433            width,
434            numeric,
435            sortable: true, // Make all columns sortable by default
436            sort_direction: None,
437            h_align: if numeric { HAlign::Right } else { HAlign::Left },
438            v_align: VAlign::Center,
439            tooltip: None,
440            heading_alignment: None,
441            column_width: ColumnWidth::Fixed(width),
442        });
443        self
444    }
445
446    /// Add a sortable column to the data table.
447    pub fn sortable_column(mut self, title: impl Into<String>, width: f32, numeric: bool) -> Self {
448        self.columns.push(DataTableColumn {
449            title: title.into(),
450            header_widget: None,
451            width,
452            numeric,
453            sortable: true,
454            sort_direction: None,
455            h_align: if numeric { HAlign::Right } else { HAlign::Left },
456            v_align: VAlign::Center,
457            tooltip: None,
458            heading_alignment: None,
459            column_width: ColumnWidth::Fixed(width),
460        });
461        self
462    }
463
464    pub fn sortable_column_with_align(
465        mut self,
466        title: impl Into<String>,
467        width: f32,
468        numeric: bool,
469        h_align: HAlign,
470        v_align: VAlign,
471    ) -> Self {
472        self.columns.push(DataTableColumn {
473            title: title.into(),
474            header_widget: None,
475            width,
476            numeric,
477            sortable: true,
478            sort_direction: None,
479            h_align,
480            v_align,
481            tooltip: None,
482            heading_alignment: None,
483            column_width: ColumnWidth::Fixed(width),
484        });
485        self
486    }
487
488    /// Add a column with custom alignment
489    pub fn column_with_align(
490        mut self,
491        title: impl Into<String>,
492        width: f32,
493        numeric: bool,
494        h_align: HAlign,
495        v_align: VAlign,
496    ) -> Self {
497        self.columns.push(DataTableColumn {
498            title: title.into(),
499            header_widget: None,
500            width,
501            numeric,
502            sortable: true,
503            sort_direction: None,
504            h_align,
505            v_align,
506            tooltip: None,
507            heading_alignment: None,
508            column_width: ColumnWidth::Fixed(width),
509        });
510        self
511    }
512
513    /// Add a row using a builder pattern.
514    pub fn row<F>(mut self, f: F) -> Self
515    where
516        F: FnOnce(DataTableRow<'a>) -> DataTableRow<'a>,
517    {
518        let row = f(DataTableRow::new());
519        self.rows.push(row);
520        self
521    }
522
523    /// Set the ID for state persistence.
524    pub fn id(mut self, id: impl Into<Id>) -> Self {
525        self.id = Some(id.into());
526        self
527    }
528
529    /// Enable row selection.
530    pub fn allow_selection(mut self, allow: bool) -> Self {
531        self.allow_selection = allow;
532        self
533    }
534
535    /// Enable row drawers. Rows with a `.drawer()` closure will show a clickable
536    /// arrow (> closed, v open) that expands a panel below the row.
537    pub fn allow_drawer(mut self, allow: bool) -> Self {
538        self.allow_drawer = allow;
539        self
540    }
541
542    /// Set the fixed height of expanded drawer panels (default: automatic sizing).
543    /// If not set, drawer height will automatically adjust to fit its contents.
544    pub fn drawer_row_height(mut self, height: f32) -> Self {
545        self.drawer_row_height = Some(height);
546        self
547    }
548
549    /// Make the header sticky.
550    pub fn sticky_header(mut self, sticky: bool) -> Self {
551        self.sticky_header = sticky;
552        self
553    }
554
555    /// Show progress indicator.
556    pub fn show_progress(mut self, show: bool) -> Self {
557        self.progress_visible = show;
558        self
559    }
560
561    /// Set corner radius.
562    pub fn corner_radius(mut self, corner_radius: impl Into<CornerRadius>) -> Self {
563        self.corner_radius = corner_radius.into();
564        self
565    }
566
567    /// Set default row height in pixels.
568    /// This sets a fixed minimum height for all rows.
569    pub fn default_row_height(mut self, height: f32) -> Self {
570        self.default_row_height = height;
571        self.theme.data_row_min_height = Some(height);
572        self.auto_height = false;
573        self
574    }
575
576    /// Enable automatic row height calculation based on content.
577    /// Each row will size independently to fit its content.
578    /// You can still set a minimum height that will be respected.
579    pub fn auto_row_height(mut self, enabled: bool) -> Self {
580        self.auto_height = enabled;
581        if enabled {
582            // Set a minimal default height to allow content-based sizing
583            self.theme.data_row_min_height = Some(20.0);
584        }
585        self
586    }
587
588    /// Set minimum row height for auto-sizing mode.
589    /// Only effective when auto_row_height is enabled.
590    pub fn min_row_height(mut self, height: f32) -> Self {
591        self.theme.data_row_min_height = Some(height);
592        self
593    }
594
595    /// Set custom theme for this table.
596    pub fn theme(mut self, theme: DataTableTheme) -> Self {
597        self.theme = theme;
598        self
599    }
600
601    fn get_table_style(&self) -> (Color32, Stroke) {
602        let md_surface = self.theme.decoration.unwrap_or_else(|| get_global_color("surface"));
603        let md_outline = get_global_color("outline");
604        let border_stroke = self.theme.border_stroke.unwrap_or_else(|| Stroke::new(1.0, md_outline));
605        (md_surface, border_stroke)
606    }
607
608    /// Show the data table and return both UI response and selection state
609    pub fn show(self, ui: &mut Ui) -> DataTableResponse {
610        let (background_color, border_stroke) = self.get_table_style();
611
612        // Generate table ID for state persistence
613        let table_id = self.id.unwrap_or_else(|| {
614            use std::collections::hash_map::DefaultHasher;
615            use std::hash::{Hash, Hasher};
616            let mut hasher = DefaultHasher::new();
617
618            // Hash based on columns and first few rows for uniqueness
619            for col in &self.columns {
620                col.title.hash(&mut hasher);
621                col.width.to_bits().hash(&mut hasher);
622            }
623            for (i, row) in self.rows.iter().take(3).enumerate() {
624                i.hash(&mut hasher);
625                for cell in &row.cells {
626                    match &cell.content {
627                        CellContent::Text(t) => t.text().hash(&mut hasher),
628                        CellContent::Widget(_) => "widget".hash(&mut hasher),
629                    }
630                }
631            }
632            self.rows.len().hash(&mut hasher);
633            Id::new(format!("datatable_{}", hasher.finish()))
634        });
635
636        // Get or create persistent state
637        let mut state: DataTableState =
638            ui.data_mut(|d| d.get_persisted(table_id).unwrap_or_default());
639
640        // Get external editing state from UI memory if available
641        if let Some(external_editing_state) = ui.memory(|mem| {
642            mem.data
643                .get_temp::<(HashSet<usize>, HashMap<usize, Vec<String>>)>(
644                    table_id.with("external_edit_state"),
645                )
646        }) {
647            state.editing_rows = external_editing_state.0;
648            state.edit_data = external_editing_state.1;
649        }
650
651        // Initialize sorting state from widget if not set
652        if state.sorted_column.is_none() && self.sorted_column.is_some() {
653            state.sorted_column = self.sorted_column;
654            state.sort_direction = self.sort_direction.clone();
655        }
656
657        // Ensure state vectors match current row count
658        if state.selected_rows.len() != self.rows.len() {
659            state.selected_rows.resize(self.rows.len(), false);
660        }
661
662        // Sync selection state only for rows where the caller explicitly set `.selected()`.
663        // Rows without an explicit `.selected()` call preserve their internally-clicked state.
664        for (i, row) in self.rows.iter().enumerate() {
665            if i < state.selected_rows.len() && row.selection_externally_set {
666                state.selected_rows[i] = row.selected;
667            }
668        }
669
670        let MaterialDataTable {
671            columns,
672            mut rows,
673            allow_selection,
674            allow_drawer,
675            drawer_row_height,
676            sticky_header: _,
677            progress_visible,
678            corner_radius,
679            default_row_height,
680            theme,
681            auto_height,
682            ..
683        } = self;
684
685        // Sort rows if a column is selected for sorting
686        if let Some(sort_col_idx) = state.sorted_column {
687            if let Some(sort_column) = columns.get(sort_col_idx) {
688                rows.sort_by(|a, b| {
689                    let cell_a_text = a
690                        .cells
691                        .get(sort_col_idx)
692                        .and_then(|c| match &c.content {
693                            CellContent::Text(t) => Some(t.text()),
694                            CellContent::Widget(_) => None,
695                        })
696                        .unwrap_or("");
697                    let cell_b_text = b
698                        .cells
699                        .get(sort_col_idx)
700                        .and_then(|c| match &c.content {
701                            CellContent::Text(t) => Some(t.text()),
702                            CellContent::Widget(_) => None,
703                        })
704                        .unwrap_or("");
705
706                    let comparison = if sort_column.numeric {
707                        // Try to parse as numbers for numeric columns
708                        let a_num: f64 = cell_a_text.trim_start_matches('$').parse().unwrap_or(0.0);
709                        let b_num: f64 = cell_b_text.trim_start_matches('$').parse().unwrap_or(0.0);
710                        a_num
711                            .partial_cmp(&b_num)
712                            .unwrap_or(std::cmp::Ordering::Equal)
713                    } else {
714                        // Alphabetical comparison for text columns
715                        cell_a_text.cmp(cell_b_text)
716                    };
717
718                    match state.sort_direction {
719                        SortDirection::Ascending => comparison,
720                        SortDirection::Descending => comparison.reverse(),
721                    }
722                });
723            }
724        }
725
726        // Calculate table dimensions with dynamic row heights.
727        // Use the data-columns width to decide whether to use compact special-column widths:
728        // when the total table width would be < 500px, minimize checkbox/arrow padding.
729        let columns_only_width: f32 = columns.iter().map(|col| col.width).sum();
730        let base_checkbox_width = if allow_selection && theme.show_checkbox_column { 48.0 } else { 0.0 };
731        let base_drawer_arrow_width = if allow_drawer { 32.0 } else { 0.0 };
732        let is_narrow = base_checkbox_width + base_drawer_arrow_width + columns_only_width < 500.0;
733        let checkbox_width = if allow_selection && theme.show_checkbox_column {
734            if is_narrow { 32.0 } else { 48.0 }
735        } else {
736            0.0
737        };
738        let drawer_arrow_width = if allow_drawer {
739            if is_narrow { 20.0 } else { 32.0 }
740        } else {
741            0.0
742        };
743        let total_width = checkbox_width + drawer_arrow_width + columns_only_width;
744        let min_row_height = theme.data_row_min_height.unwrap_or(default_row_height);
745        let min_header_height = theme.heading_row_height.unwrap_or(56.0);
746
747        // Calculate header height with text wrapping
748        let mut header_height: f32 = min_header_height;
749        for column in &columns {
750            let available_width = column.width - 48.0; // Account for padding and sort icon
751            let header_font = FontId::new(16.0, FontFamily::Proportional);
752
753            let galley = ui.painter().layout_job(egui::text::LayoutJob {
754                text: column.title.clone(),
755                sections: vec![egui::text::LayoutSection {
756                    leading_space: 0.0,
757                    byte_range: 0..column.title.len(),
758                    format: egui::TextFormat {
759                        font_id: header_font,
760                        color: get_global_color("onSurface"),
761                        ..Default::default()
762                    },
763                }],
764                wrap: egui::text::TextWrapping {
765                    max_width: available_width,
766                    ..Default::default()
767                },
768                break_on_newline: true,
769                halign: egui::Align::LEFT,
770                justify: false,
771                first_row_min_height: 0.0,
772                round_output_to_gui: true,
773            });
774
775            let content_height: f32 = galley.size().y + 16.0; // Add padding
776            header_height = header_height.max(content_height);
777        }
778
779        // Calculate individual row heights based on content
780        let mut row_heights = Vec::new();
781        for row in &rows {
782            // In auto_height mode, start with a minimal height, otherwise use min_row_height
783            let base_height = if auto_height { 20.0 } else { min_row_height };
784            let mut max_height: f32 = base_height;
785            
786            for (cell_idx, cell) in row.cells.iter().enumerate() {
787                if let Some(column) = columns.get(cell_idx) {
788                    match &cell.content {
789                        CellContent::Text(cell_text) => {
790                            let available_width = column.width - 32.0;
791                            let cell_font = if let Some((ref font_id, _)) = theme.data_text_style {
792                                font_id.clone()
793                            } else {
794                                FontId::new(14.0, FontFamily::Proportional)
795                            };
796
797                            let galley = ui.painter().layout_job(egui::text::LayoutJob {
798                                text: cell_text.text().to_string(),
799                                sections: vec![egui::text::LayoutSection {
800                                    leading_space: 0.0,
801                                    byte_range: 0..cell_text.text().len(),
802                                    format: egui::TextFormat {
803                                        font_id: cell_font,
804                                        color: get_global_color("onSurface"),
805                                        ..Default::default()
806                                    },
807                                }],
808                                wrap: egui::text::TextWrapping {
809                                    max_width: available_width,
810                                    ..Default::default()
811                                },
812                                break_on_newline: true,
813                                halign: egui::Align::LEFT, // Always left-align within galley; positioning handles cell alignment
814                                justify: false,
815                                first_row_min_height: 0.0,
816                                round_output_to_gui: true,
817                            });
818
819                            let content_height: f32 = galley.size().y + 16.0; // Add padding
820                            max_height = max_height.max(content_height);
821                        }
822                        CellContent::Widget(_) => {
823                            // For widgets, use minimum height - they will size themselves
824                            // In auto mode, don't force a minimum for widget rows
825                            if !auto_height {
826                                max_height = max_height.max(min_row_height);
827                            }
828                        }
829                    }
830                }
831            }
832            
833            // Apply minimum height constraint
834            let final_height = max_height.max(min_row_height);
835            row_heights.push(final_height);
836        }
837
838        // Calculate drawer heights for open rows (0.0 when closed)
839        let drawer_heights: Vec<f32> = rows
840            .iter()
841            .enumerate()
842            .map(|(row_idx, row)| {
843                if allow_drawer
844                    && row.drawer.is_some()
845                    && state.drawer_open_rows.contains(&row_idx)
846                {
847                    // Use fixed height if specified, otherwise check cached height from previous frame
848                    if let Some(fixed_height) = drawer_row_height {
849                        fixed_height
850                    } else {
851                        // Try to get cached height from previous frame's rendering
852                        let cached_height = ui.data(|data| {
853                            data.get_temp::<f32>(table_id.with(format!("drawer_height_{}", row_idx)))
854                        });
855                        cached_height.unwrap_or(120.0) // Default to 120 if not cached yet
856                    }
857                } else {
858                    0.0
859                }
860            })
861            .collect();
862
863        let total_height = header_height
864            + row_heights.iter().sum::<f32>()
865            + drawer_heights.iter().sum::<f32>();
866
867        // Collect all row actions from this frame
868        let mut all_row_actions: Vec<RowAction> = Vec::new();
869
870        // Apply Material theme styling
871        let surface = get_global_color("surface");
872        let on_surface = get_global_color("onSurface");
873        let primary = get_global_color("primary");
874
875        let mut style = (*ui.ctx().style()).clone();
876        style.visuals.widgets.noninteractive.bg_fill = surface;
877        style.visuals.widgets.inactive.bg_fill = surface;
878        style.visuals.widgets.hovered.bg_fill =
879            egui::Color32::from_rgba_premultiplied(primary.r(), primary.g(), primary.b(), 20);
880        style.visuals.widgets.active.bg_fill =
881            egui::Color32::from_rgba_premultiplied(primary.r(), primary.g(), primary.b(), 40);
882        style.visuals.selection.bg_fill = primary;
883        style.visuals.widgets.noninteractive.fg_stroke.color = on_surface;
884        style.visuals.widgets.inactive.fg_stroke.color = on_surface;
885        style.visuals.widgets.hovered.fg_stroke.color = on_surface;
886        style.visuals.widgets.active.fg_stroke.color = on_surface;
887        style.visuals.striped = true;
888        style.visuals.faint_bg_color = egui::Color32::from_rgba_premultiplied(
889            on_surface.r(),
890            on_surface.g(),
891            on_surface.b(),
892            10,
893        );
894        ui.ctx().set_style(style);
895
896        let desired_size = Vec2::new(total_width, total_height);
897        let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click());
898        // Ensure the allocated rect is marked as used to advance the cursor properly
899        ui.advance_cursor_after_rect(rect);
900
901        if ui.is_rect_visible(rect) {
902            // Draw table background
903            ui.painter()
904                .rect_filled(rect, corner_radius, background_color);
905            ui.painter().rect_stroke(
906                rect,
907                corner_radius,
908                border_stroke,
909                egui::epaint::StrokeKind::Outside,
910            );
911
912            let mut current_y = rect.min.y;
913
914            // Draw header
915            let header_rect = Rect::from_min_size(rect.min, Vec2::new(total_width, header_height));
916            let header_bg = theme.heading_row_color.unwrap_or_else(|| get_global_color("surfaceVariant"));
917            ui.painter()
918                .rect_filled(header_rect, CornerRadius::ZERO, header_bg);
919
920            let mut current_x = rect.min.x;
921
922            // Header checkbox
923            if allow_selection && theme.show_checkbox_column {
924                let checkbox_rect = Rect::from_min_size(
925                    egui::pos2(current_x, current_y),
926                    Vec2::new(checkbox_width, header_height),
927                );
928
929                let checkbox_center = checkbox_rect.center();
930                let checkbox_size = Vec2::splat(18.0);
931                let checkbox_inner_rect = Rect::from_center_size(checkbox_center, checkbox_size);
932
933                let checkbox_color = if state.header_checkbox {
934                    get_global_color("primary")
935                } else {
936                    Color32::TRANSPARENT
937                };
938
939                ui.painter().rect_filled(
940                    checkbox_inner_rect,
941                    CornerRadius::from(2.0),
942                    checkbox_color,
943                );
944                ui.painter().rect_stroke(
945                    checkbox_inner_rect,
946                    CornerRadius::from(2.0),
947                    Stroke::new(2.0, get_global_color("outline")),
948                    egui::epaint::StrokeKind::Outside,
949                );
950
951                if state.header_checkbox {
952                    // Draw checkmark
953                    let check_points = [
954                        checkbox_inner_rect.min + Vec2::new(4.0, 9.0),
955                        checkbox_inner_rect.min + Vec2::new(8.0, 13.0),
956                        checkbox_inner_rect.min + Vec2::new(14.0, 5.0),
957                    ];
958                    ui.painter().line_segment(
959                        [check_points[0], check_points[1]],
960                        Stroke::new(2.0, Color32::WHITE),
961                    );
962                    ui.painter().line_segment(
963                        [check_points[1], check_points[2]],
964                        Stroke::new(2.0, Color32::WHITE),
965                    );
966                }
967
968                // Handle header checkbox click
969                let header_checkbox_id = table_id.with("header_checkbox");
970                let checkbox_response =
971                    ui.interact(checkbox_inner_rect, header_checkbox_id, Sense::click());
972                if checkbox_response.clicked() {
973                    state.header_checkbox = !state.header_checkbox;
974                    // Only update non-readonly rows
975                    for (idx, selected) in state.selected_rows.iter_mut().enumerate() {
976                        if let Some(row) = rows.get(idx) {
977                            if !row.readonly {
978                                *selected = state.header_checkbox;
979                            }
980                        }
981                    }
982                }
983
984                current_x += checkbox_width;
985            }
986
987            // Drawer arrow column spacer in header (no header label)
988            if allow_drawer {
989                current_x += drawer_arrow_width;
990            }
991
992            // Header columns
993            for (col_idx, column) in columns.iter().enumerate() {
994                let col_rect = Rect::from_min_size(
995                    egui::pos2(current_x, current_y),
996                    Vec2::new(column.width, header_height),
997                );
998
999                // Render header text with wrapping support
1000                let available_width = column.width - 48.0; // Account for padding and sort icon
1001                let header_font = FontId::new(16.0, FontFamily::Proportional);
1002
1003                let galley = ui.painter().layout_job(egui::text::LayoutJob {
1004                    text: column.title.clone(),
1005                    sections: vec![egui::text::LayoutSection {
1006                        leading_space: 0.0,
1007                        byte_range: 0..column.title.len(),
1008                        format: egui::TextFormat {
1009                            font_id: header_font,
1010                            color: get_global_color("onSurface"),
1011                            ..Default::default()
1012                        },
1013                    }],
1014                    wrap: egui::text::TextWrapping {
1015                        max_width: available_width,
1016                        ..Default::default()
1017                    },
1018                    break_on_newline: true,
1019                    halign: egui::Align::LEFT,
1020                    justify: false,
1021                    first_row_min_height: 0.0,
1022                    round_output_to_gui: true,
1023                });
1024
1025                let text_pos = egui::pos2(
1026                    current_x + 16.0,
1027                    current_y + (header_height - galley.size().y) / 2.0,
1028                );
1029
1030                ui.painter()
1031                    .galley(text_pos, galley, get_global_color("onSurface"));
1032
1033                // Handle column header clicks for sorting
1034                if column.sortable {
1035                    let header_click_id = table_id.with(format!("column_header_{}", col_idx));
1036                    let mut header_response = ui.interact(col_rect, header_click_id, Sense::click());
1037                    
1038                    // Show tooltip if available
1039                    if let Some(ref tooltip) = column.tooltip {
1040                        header_response = header_response.on_hover_text(tooltip);
1041                    }
1042                    
1043                    if header_response.clicked() {
1044                        // Handle sorting logic
1045                        if state.sorted_column == Some(col_idx) {
1046                            // Same column clicked, toggle direction
1047                            state.sort_direction = match state.sort_direction {
1048                                SortDirection::Ascending => SortDirection::Descending,
1049                                SortDirection::Descending => SortDirection::Ascending,
1050                            };
1051                        } else {
1052                            // New column clicked
1053                            state.sorted_column = Some(col_idx);
1054                            state.sort_direction = SortDirection::Ascending;
1055                        }
1056                        ui.memory_mut(|mem| {
1057                            mem.data
1058                                .insert_temp(table_id.with("column_clicked"), Some(col_idx));
1059                        });
1060                    }
1061
1062                    let icon_pos = egui::pos2(
1063                        current_x + column.width - 32.0,
1064                        current_y + (header_height - 24.0) / 2.0,
1065                    );
1066                    let icon_rect = Rect::from_min_size(icon_pos, Vec2::splat(24.0));
1067
1068                    // Determine if this column is currently sorted
1069                    let is_sorted = state.sorted_column == Some(col_idx);
1070                    let sort_direction = if is_sorted {
1071                        Some(&state.sort_direction)
1072                    } else {
1073                        None
1074                    };
1075
1076                    // Draw sort arrow with enhanced visual feedback
1077                    let arrow_color = if is_sorted {
1078                        theme.sort_active_color.unwrap_or_else(|| get_global_color("primary")) // Highlight active sort column
1079                    } else {
1080                        theme.sort_inactive_color.unwrap_or_else(|| get_global_color("onSurfaceVariant"))
1081                    };
1082
1083                    let center = icon_rect.center();
1084
1085                    // Draw triangle arrows
1086                    match sort_direction {
1087                        Some(SortDirection::Ascending) => {
1088                            // Up triangle (▲)
1089                            let points = [
1090                                center + Vec2::new(0.0, -6.0), // Top point
1091                                center + Vec2::new(-5.0, 4.0), // Bottom left
1092                                center + Vec2::new(5.0, 4.0),  // Bottom right
1093                            ];
1094                            ui.painter().line_segment(
1095                                [points[0], points[1]],
1096                                Stroke::new(2.0, arrow_color),
1097                            );
1098                            ui.painter().line_segment(
1099                                [points[1], points[2]],
1100                                Stroke::new(2.0, arrow_color),
1101                            );
1102                            ui.painter().line_segment(
1103                                [points[2], points[0]],
1104                                Stroke::new(2.0, arrow_color),
1105                            );
1106                        }
1107                        Some(SortDirection::Descending) => {
1108                            // Down triangle (▼)
1109                            let points = [
1110                                center + Vec2::new(0.0, 6.0),   // Bottom point
1111                                center + Vec2::new(-5.0, -4.0), // Top left
1112                                center + Vec2::new(5.0, -4.0),  // Top right
1113                            ];
1114                            ui.painter().line_segment(
1115                                [points[0], points[1]],
1116                                Stroke::new(2.0, arrow_color),
1117                            );
1118                            ui.painter().line_segment(
1119                                [points[1], points[2]],
1120                                Stroke::new(2.0, arrow_color),
1121                            );
1122                            ui.painter().line_segment(
1123                                [points[2], points[0]],
1124                                Stroke::new(2.0, arrow_color),
1125                            );
1126                        }
1127                        None => {
1128                            // Neutral state - show both arrows faintly
1129                            let light_color = arrow_color.gamma_multiply(0.5);
1130                            // Up triangle
1131                            let up_points = [
1132                                center + Vec2::new(0.0, -8.0),
1133                                center + Vec2::new(-3.0, -2.0),
1134                                center + Vec2::new(3.0, -2.0),
1135                            ];
1136                            ui.painter().line_segment(
1137                                [up_points[0], up_points[1]],
1138                                Stroke::new(1.0, light_color),
1139                            );
1140                            ui.painter().line_segment(
1141                                [up_points[1], up_points[2]],
1142                                Stroke::new(1.0, light_color),
1143                            );
1144                            ui.painter().line_segment(
1145                                [up_points[2], up_points[0]],
1146                                Stroke::new(1.0, light_color),
1147                            );
1148
1149                            // Down triangle
1150                            let down_points = [
1151                                center + Vec2::new(0.0, 8.0),
1152                                center + Vec2::new(-3.0, 2.0),
1153                                center + Vec2::new(3.0, 2.0),
1154                            ];
1155                            ui.painter().line_segment(
1156                                [down_points[0], down_points[1]],
1157                                Stroke::new(1.0, light_color),
1158                            );
1159                            ui.painter().line_segment(
1160                                [down_points[1], down_points[2]],
1161                                Stroke::new(1.0, light_color),
1162                            );
1163                            ui.painter().line_segment(
1164                                [down_points[2], down_points[0]],
1165                                Stroke::new(1.0, light_color),
1166                            );
1167                        }
1168                    }
1169                }
1170
1171                current_x += column.width;
1172            }
1173
1174            current_y += header_height;
1175
1176            // Draw rows with dynamic heights
1177            for (row_idx, row) in rows.iter().enumerate() {
1178                let row_height = row_heights.get(row_idx).copied().unwrap_or(min_row_height);
1179                let row_rect = Rect::from_min_size(
1180                    egui::pos2(rect.min.x, current_y),
1181                    Vec2::new(total_width, row_height),
1182                );
1183
1184                let row_selected = state.selected_rows.get(row_idx).copied().unwrap_or(false);
1185                
1186                // Determine row background color with priority: custom color > selected > readonly > alternating
1187                let row_bg = if let Some(custom_color) = row.color {
1188                    custom_color
1189                } else if row_selected {
1190                    theme.selected_row_color.unwrap_or_else(|| get_global_color("primaryContainer"))
1191                } else if row.readonly {
1192                    // Subtle background for readonly rows
1193                    let surface_variant = get_global_color("surfaceVariant");
1194                    Color32::from_rgba_premultiplied(
1195                        surface_variant.r(),
1196                        surface_variant.g(),
1197                        surface_variant.b(),
1198                        (surface_variant.a() as f32 * 0.3) as u8,
1199                    )
1200                } else if row_idx % 2 == 1 {
1201                    theme.data_row_color.unwrap_or_else(|| get_global_color("surfaceVariant"))
1202                } else {
1203                    background_color
1204                };
1205
1206                ui.painter()
1207                    .rect_filled(row_rect, CornerRadius::ZERO, row_bg);
1208                    
1209                // Draw divider below row — skip when a drawer immediately follows
1210                let row_has_open_drawer = allow_drawer
1211                    && row.drawer.is_some()
1212                    && state.drawer_open_rows.contains(&row_idx);
1213                if !row_has_open_drawer && (row_idx < rows.len() - 1 || theme.show_bottom_border) {
1214                    let divider_y = current_y + row_height;
1215                    let divider_thickness = theme.divider_thickness.unwrap_or(1.0);
1216                    let divider_color = theme.divider_color.unwrap_or_else(|| get_global_color("outlineVariant"));
1217                    ui.painter().line_segment(
1218                        [
1219                            egui::pos2(rect.min.x, divider_y),
1220                            egui::pos2(rect.min.x + total_width, divider_y),
1221                        ],
1222                        Stroke::new(divider_thickness, divider_color),
1223                    );
1224                }
1225
1226                current_x = rect.min.x;
1227
1228                // Row checkbox
1229                if allow_selection && theme.show_checkbox_column {
1230                    let checkbox_rect = Rect::from_min_size(
1231                        egui::pos2(current_x, current_y),
1232                        Vec2::new(checkbox_width, row_height),
1233                    );
1234
1235                    let checkbox_center = checkbox_rect.center();
1236                    let checkbox_size = Vec2::splat(18.0);
1237                    let checkbox_inner_rect =
1238                        Rect::from_center_size(checkbox_center, checkbox_size);
1239
1240                    let checkbox_color = if row_selected {
1241                        get_global_color("primary")
1242                    } else {
1243                        Color32::TRANSPARENT
1244                    };
1245
1246                    let border_color = if row.readonly {
1247                        get_global_color("outline").linear_multiply(0.5) // Dimmed for readonly
1248                    } else {
1249                        get_global_color("outline")
1250                    };
1251
1252                    ui.painter().rect_filled(
1253                        checkbox_inner_rect,
1254                        CornerRadius::from(2.0),
1255                        checkbox_color,
1256                    );
1257                    ui.painter().rect_stroke(
1258                        checkbox_inner_rect,
1259                        CornerRadius::from(2.0),
1260                        Stroke::new(2.0, border_color),
1261                        egui::epaint::StrokeKind::Outside,
1262                    );
1263
1264                    if row_selected {
1265                        // Draw checkmark
1266                        let check_points = [
1267                            checkbox_inner_rect.min + Vec2::new(4.0, 9.0),
1268                            checkbox_inner_rect.min + Vec2::new(8.0, 13.0),
1269                            checkbox_inner_rect.min + Vec2::new(14.0, 5.0),
1270                        ];
1271                        ui.painter().line_segment(
1272                            [check_points[0], check_points[1]],
1273                            Stroke::new(2.0, Color32::WHITE),
1274                        );
1275                        ui.painter().line_segment(
1276                            [check_points[1], check_points[2]],
1277                            Stroke::new(2.0, Color32::WHITE),
1278                        );
1279                    }
1280
1281                    // Handle row checkbox click
1282                    let row_checkbox_id = table_id.with(format!("row_checkbox_{}", row_idx));
1283                    let checkbox_response =
1284                        ui.interact(checkbox_inner_rect, row_checkbox_id, Sense::click());
1285                    if checkbox_response.clicked() && !row.readonly {
1286                        if let Some(selected) = state.selected_rows.get_mut(row_idx) {
1287                            *selected = !*selected;
1288                        }
1289
1290                        // Update header checkbox state based on row selections
1291                        // Only consider non-readonly rows for header checkbox state
1292                        let non_readonly_indices: Vec<usize> = rows
1293                            .iter()
1294                            .enumerate()
1295                            .filter(|(_, row)| !row.readonly)
1296                            .map(|(idx, _)| idx)
1297                            .collect();
1298
1299                        if !non_readonly_indices.is_empty() {
1300                            let all_non_readonly_selected = non_readonly_indices
1301                                .iter()
1302                                .all(|&idx| state.selected_rows.get(idx).copied().unwrap_or(false));
1303                            let none_non_readonly_selected =
1304                                non_readonly_indices.iter().all(|&idx| {
1305                                    !state.selected_rows.get(idx).copied().unwrap_or(false)
1306                                });
1307                            state.header_checkbox =
1308                                all_non_readonly_selected && !none_non_readonly_selected;
1309                        }
1310                    }
1311
1312                    current_x += checkbox_width;
1313                }
1314
1315                // Row drawer arrow
1316                if allow_drawer {
1317                    let arrow_area_rect = Rect::from_min_size(
1318                        egui::pos2(current_x, current_y),
1319                        Vec2::new(drawer_arrow_width, row_height),
1320                    );
1321
1322                    if row.drawer.is_some() {
1323                        let is_open = state.drawer_open_rows.contains(&row_idx);
1324                        let arrow_color = get_global_color("onSurfaceVariant");
1325                        let center = arrow_area_rect.center();
1326
1327                        if is_open {
1328                            // Down chevron: v
1329                            let pts = [
1330                                center + Vec2::new(-5.0, -3.0),
1331                                center + Vec2::new(0.0, 3.0),
1332                                center + Vec2::new(5.0, -3.0),
1333                            ];
1334                            ui.painter().line_segment(
1335                                [pts[0], pts[1]],
1336                                Stroke::new(2.0, arrow_color),
1337                            );
1338                            ui.painter().line_segment(
1339                                [pts[1], pts[2]],
1340                                Stroke::new(2.0, arrow_color),
1341                            );
1342                        } else {
1343                            // Right chevron: >
1344                            let pts = [
1345                                center + Vec2::new(-3.0, -5.0),
1346                                center + Vec2::new(3.0, 0.0),
1347                                center + Vec2::new(-3.0, 5.0),
1348                            ];
1349                            ui.painter().line_segment(
1350                                [pts[0], pts[1]],
1351                                Stroke::new(2.0, arrow_color),
1352                            );
1353                            ui.painter().line_segment(
1354                                [pts[1], pts[2]],
1355                                Stroke::new(2.0, arrow_color),
1356                            );
1357                        }
1358
1359                        let arrow_id = table_id.with(format!("drawer_arrow_{}", row_idx));
1360                        let arrow_response =
1361                            ui.interact(arrow_area_rect, arrow_id, Sense::click());
1362                        if arrow_response.clicked() {
1363                            if is_open {
1364                                state.drawer_open_rows.remove(&row_idx);
1365                            } else {
1366                                state.drawer_open_rows.insert(row_idx);
1367                            }
1368                        }
1369                    }
1370
1371                    current_x += drawer_arrow_width;
1372                }
1373
1374                // Track row actions for this specific row
1375                let mut row_actions: Vec<RowAction> = Vec::new();
1376
1377                // Row cells
1378                for (cell_idx, cell) in row.cells.iter().enumerate() {
1379                    if let Some(column) = columns.get(cell_idx) {
1380                        let _cell_rect = Rect::from_min_size(
1381                            egui::pos2(current_x, current_y),
1382                            Vec2::new(column.width, row_height),
1383                        );
1384
1385                        let is_row_editing = state.editing_rows.contains(&row_idx);
1386                        let is_actions_column = column.title == "Actions";
1387
1388                        if is_actions_column {
1389                            // Render action buttons
1390                            let button_rect = Rect::from_min_size(
1391                                egui::pos2(current_x + 8.0, current_y + (row_height - 32.0) / 2.0),
1392                                Vec2::new(column.width - 16.0, 32.0),
1393                            );
1394
1395                            ui.scope_builder(egui::UiBuilder::new().max_rect(button_rect), |ui| {
1396                                egui::ScrollArea::horizontal()
1397                                    .id_salt(format!("actions_scroll_{}", row_idx))
1398                                    .auto_shrink([false, true])
1399                                    .show(ui, |ui| {
1400                                    ui.horizontal(|ui| {
1401                                        if is_row_editing {
1402                                            if ui.add(MaterialButton::filled("Save").small()).clicked() {
1403                                                row_actions.push(RowAction::Save(row_idx));
1404                                            }
1405                                            if ui.add(MaterialButton::filled("Cancel").small()).clicked() {
1406                                                row_actions.push(RowAction::Cancel(row_idx));
1407                                            }
1408                                        } else {
1409                                            if ui.add(MaterialButton::filled("Edit").small()).clicked() {
1410                                                row_actions.push(RowAction::Edit(row_idx));
1411                                            }
1412                                            if ui.add(MaterialButton::filled("Delete").small()).clicked() {
1413                                                row_actions.push(RowAction::Delete(row_idx));
1414                                            }
1415                                        }
1416                                    });
1417                                });
1418                            });
1419                        } else if is_row_editing {
1420                            // Render editable text field
1421                            let edit_rect = Rect::from_min_size(
1422                                egui::pos2(current_x + 8.0, current_y + (row_height - 24.0) / 2.0),
1423                                Vec2::new(column.width - 16.0, 24.0),
1424                            );
1425
1426                            // Get or initialize edit data
1427                            let edit_data = state.edit_data.entry(row_idx).or_insert_with(|| {
1428                                row.cells
1429                                    .iter()
1430                                    .map(|c| match &c.content {
1431                                        CellContent::Text(t) => t.text().to_string(),
1432                                        CellContent::Widget(_) => String::new(),
1433                                    })
1434                                    .collect()
1435                            });
1436
1437                            // Ensure we have enough entries for this cell
1438                            if edit_data.len() <= cell_idx {
1439                                edit_data.resize(cell_idx + 1, String::new());
1440                            }
1441
1442                            let edit_text = &mut edit_data[cell_idx];
1443
1444                            ui.scope_builder(egui::UiBuilder::new().max_rect(edit_rect), |ui| {
1445                                ui.add(
1446                                    egui::TextEdit::singleline(edit_text)
1447                                        .desired_width(column.width - 16.0),
1448                                );
1449                            });
1450                        } else {
1451                            // Determine alignment from cell or column
1452                            let h_align = cell.h_align.as_ref().unwrap_or(&column.h_align);
1453                            let v_align = cell.v_align.as_ref().unwrap_or(&column.v_align);
1454
1455                            match &cell.content {
1456                                CellContent::Text(cell_text) => {
1457                                    // Render normal text with alignment
1458                                    let available_width = column.width - 32.0; // Account for padding
1459                                    let cell_font = if let Some((ref font_id, _)) = theme.data_text_style {
1460                                        font_id.clone()
1461                                    } else {
1462                                        FontId::new(14.0, FontFamily::Proportional)
1463                                    };
1464                                    
1465                                    let text_color = if cell.placeholder {
1466                                        let base_color = get_global_color("onSurface");
1467                                        Color32::from_rgba_premultiplied(
1468                                            base_color.r(),
1469                                            base_color.g(),
1470                                            base_color.b(),
1471                                            (base_color.a() as f32 * 0.6) as u8,
1472                                        )
1473                                    } else if let Some((_, ref color)) = theme.data_text_style {
1474                                        *color
1475                                    } else {
1476                                        get_global_color("onSurface")
1477                                    };
1478
1479                                    let galley = ui.painter().layout_job(egui::text::LayoutJob {
1480                                        text: cell_text.text().to_string(),
1481                                        sections: vec![egui::text::LayoutSection {
1482                                            leading_space: 0.0,
1483                                            byte_range: 0..cell_text.text().len(),
1484                                            format: egui::TextFormat {
1485                                                font_id: cell_font,
1486                                                color: text_color,
1487                                                ..Default::default()
1488                                            },
1489                                        }],
1490                                        wrap: egui::text::TextWrapping {
1491                                            max_width: available_width,
1492                                            ..Default::default()
1493                                        },
1494                                        break_on_newline: true,
1495                                        halign: egui::Align::LEFT, // Always left-align within galley; positioning handles cell alignment
1496                                        justify: false,
1497                                        first_row_min_height: 0.0,
1498                                        round_output_to_gui: true,
1499                                    });
1500
1501                                    // Calculate horizontal position based on alignment
1502                                    let text_x = match h_align {
1503                                        HAlign::Left => current_x + 16.0,
1504                                        HAlign::Center => {
1505                                            current_x + (column.width - galley.size().x) / 2.0
1506                                        }
1507                                        HAlign::Right => {
1508                                            current_x + column.width - 16.0 - galley.size().x
1509                                        }
1510                                    };
1511
1512                                    // Calculate vertical position based on alignment
1513                                    let text_y = match v_align {
1514                                        VAlign::Top => current_y + 8.0,
1515                                        VAlign::Center => {
1516                                            current_y + (row_height - galley.size().y) / 2.0
1517                                        }
1518                                        VAlign::Bottom => {
1519                                            current_y + row_height - galley.size().y - 8.0
1520                                        }
1521                                    };
1522
1523                                    let text_pos = egui::pos2(text_x, text_y);
1524                                    ui.painter().galley(
1525                                        text_pos,
1526                                        galley,
1527                                        text_color,
1528                                    );
1529                                    
1530                                    // Draw edit icon if requested
1531                                    if cell.show_edit_icon {
1532                                        let icon_size = 16.0;
1533                                        let icon_x = current_x + column.width - icon_size - 8.0;
1534                                        let icon_y = current_y + (row_height - icon_size) / 2.0;
1535                                        let icon_rect = Rect::from_min_size(
1536                                            egui::pos2(icon_x, icon_y),
1537                                            Vec2::splat(icon_size),
1538                                        );
1539                                        // Draw simple pencil icon
1540                                        let icon_color = get_global_color("onSurfaceVariant");
1541                                        ui.painter().line_segment(
1542                                            [
1543                                                icon_rect.left_top() + Vec2::new(4.0, 10.0),
1544                                                icon_rect.left_top() + Vec2::new(10.0, 4.0),
1545                                            ],
1546                                            Stroke::new(1.5, icon_color),
1547                                        );
1548                                        ui.painter().line_segment(
1549                                            [
1550                                                icon_rect.left_top() + Vec2::new(2.0, 12.0),
1551                                                icon_rect.left_top() + Vec2::new(4.0, 10.0),
1552                                            ],
1553                                            Stroke::new(1.5, icon_color),
1554                                        );
1555                                    }
1556                                }
1557                                CellContent::Widget(widget_fn) => {
1558                                    // Render custom widget
1559                                    // Calculate widget rect based on alignment
1560                                    let padding = 8.0;
1561                                    let available_width = column.width - 2.0 * padding;
1562                                    let available_height = row_height - 2.0 * padding;
1563
1564                                    // For now, center the widget area. Alignment can be refined based on widget's actual size
1565                                    let widget_rect = match (h_align, v_align) {
1566                                        (HAlign::Left, VAlign::Top) => Rect::from_min_size(
1567                                            egui::pos2(current_x + padding, current_y + padding),
1568                                            Vec2::new(available_width, available_height),
1569                                        ),
1570                                        (HAlign::Center, VAlign::Center) => Rect::from_min_size(
1571                                            egui::pos2(current_x + padding, current_y + padding),
1572                                            Vec2::new(available_width, available_height),
1573                                        ),
1574                                        (HAlign::Right, VAlign::Center) => Rect::from_min_size(
1575                                            egui::pos2(current_x + padding, current_y + padding),
1576                                            Vec2::new(available_width, available_height),
1577                                        ),
1578                                        _ => Rect::from_min_size(
1579                                            egui::pos2(current_x + padding, current_y + padding),
1580                                            Vec2::new(available_width, available_height),
1581                                        ),
1582                                    };
1583
1584                                    ui.scope_builder(
1585                                        egui::UiBuilder::new().max_rect(widget_rect),
1586                                        |ui| {
1587                                            // Apply alignment to the UI
1588                                            match h_align {
1589                                                HAlign::Left => ui.with_layout(
1590                                                    egui::Layout::left_to_right(egui::Align::Min),
1591                                                    |ui| {
1592                                                        widget_fn(ui);
1593                                                    },
1594                                                ),
1595                                                HAlign::Center => ui.with_layout(
1596                                                    egui::Layout::left_to_right(
1597                                                        egui::Align::Center,
1598                                                    ),
1599                                                    |ui| {
1600                                                        widget_fn(ui);
1601                                                    },
1602                                                ),
1603                                                HAlign::Right => ui.with_layout(
1604                                                    egui::Layout::right_to_left(egui::Align::Min),
1605                                                    |ui| {
1606                                                        widget_fn(ui);
1607                                                    },
1608                                                ),
1609                                            };
1610                                        },
1611                                    );
1612                                }
1613                            }
1614                        }
1615
1616                        current_x += column.width;
1617                    }
1618                }
1619
1620                // Add this row's actions to the global collection
1621                all_row_actions.extend(row_actions);
1622
1623                current_y += row_height;
1624
1625                // Draw open drawer panel below this row
1626                if let Some(open_drawer_height) = drawer_heights.get(row_idx).copied() {
1627                    if open_drawer_height > 0.0 {
1628                        if let Some(drawer_fn) = &row.drawer {
1629                            let drawer_rect = Rect::from_min_size(
1630                                egui::pos2(rect.min.x, current_y),
1631                                Vec2::new(total_width, open_drawer_height),
1632                            );
1633
1634                            // Save the current clip rect and set a new one constrained to table bounds
1635                            let old_clip_rect = ui.clip_rect();
1636                            let table_clip_rect = rect.intersect(old_clip_rect);
1637                            ui.set_clip_rect(table_clip_rect);
1638
1639                            // Drawer background: slightly tinted surface
1640                            let drawer_bg = get_global_color("surfaceVariant");
1641                            ui.painter().rect_filled(
1642                                drawer_rect,
1643                                CornerRadius::ZERO,
1644                                drawer_bg,
1645                            );
1646
1647                            // Left accent stripe in primary color
1648                            let primary = get_global_color("primary");
1649                            ui.painter().rect_filled(
1650                                Rect::from_min_size(
1651                                    drawer_rect.left_top(),
1652                                    Vec2::new(3.0, open_drawer_height),
1653                                ),
1654                                CornerRadius::ZERO,
1655                                primary,
1656                            );
1657
1658                            // Render drawer content with proper clipping using child_ui
1659                            let content_rect = Rect::from_min_size(
1660                                drawer_rect.left_top() + Vec2::new(12.0, 0.0),
1661                                Vec2::new(total_width - 12.0, open_drawer_height),
1662                            );
1663
1664                            // Get parent's clip rect and intersect with our content rect for proper clipping
1665                            let parent_clip_rect = ui.clip_rect();
1666                            let clipped_rect = content_rect.intersect(parent_clip_rect);
1667
1668                            // Use child_ui with proper clip rect inheritance
1669                            let mut child_ui = ui.child_ui_with_id_source(
1670                                content_rect,
1671                                egui::Layout::top_down(egui::Align::LEFT),
1672                                format!("drawer_{}", row_idx),
1673                                None,
1674                            );
1675                            child_ui.set_clip_rect(clipped_rect);
1676                            drawer_fn(&mut child_ui);
1677
1678                            // Cache the actual measured height for next frame if auto-sizing
1679                            if drawer_row_height.is_none() {
1680                                let actual_height = child_ui.min_rect().height().max(40.0);
1681                                ui.data_mut(|data| {
1682                                    data.insert_temp(table_id.with(format!("drawer_height_{}", row_idx)), actual_height);
1683                                });
1684                            }
1685
1686                            // Divider at the bottom of the drawer
1687                            let divider_thickness = theme.divider_thickness.unwrap_or(1.0);
1688                            let divider_color = theme
1689                                .divider_color
1690                                .unwrap_or_else(|| get_global_color("outlineVariant"));
1691                            ui.painter().line_segment(
1692                                [
1693                                    egui::pos2(rect.min.x, current_y + open_drawer_height),
1694                                    egui::pos2(
1695                                        rect.min.x + total_width,
1696                                        current_y + open_drawer_height,
1697                                    ),
1698                                ],
1699                                Stroke::new(divider_thickness, divider_color),
1700                            );
1701
1702                            // Restore the original clip rect
1703                            ui.set_clip_rect(old_clip_rect);
1704
1705                            current_y += open_drawer_height;
1706                        }
1707                    }
1708                }
1709            }
1710
1711            // Draw progress indicator if visible
1712            if progress_visible {
1713                let scrim_color = Color32::from_rgba_unmultiplied(255, 255, 255, 128);
1714                ui.painter().rect_filled(rect, corner_radius, scrim_color);
1715
1716                // Draw progress bar
1717                let progress_rect = Rect::from_min_size(
1718                    egui::pos2(rect.min.x, rect.min.y + header_height),
1719                    Vec2::new(total_width, 4.0),
1720                );
1721
1722                let progress_color = get_global_color("primary");
1723                ui.painter()
1724                    .rect_filled(progress_rect, CornerRadius::ZERO, progress_color);
1725            }
1726        }
1727
1728        // Persist the state
1729        ui.data_mut(|d| d.insert_persisted(table_id, state.clone()));
1730
1731        // Store editing state back to memory for external access
1732        ui.memory_mut(|mem| {
1733            mem.data.insert_temp(
1734                table_id.with("external_edit_state"),
1735                (state.editing_rows.clone(), state.edit_data.clone()),
1736            );
1737        });
1738
1739        // Check for column clicks using stored state
1740        let column_clicked = ui
1741            .memory(|mem| {
1742                mem.data
1743                    .get_temp::<Option<usize>>(table_id.with("column_clicked"))
1744            })
1745            .flatten();
1746
1747        // Clear the stored click state
1748        ui.memory_mut(|mem| {
1749            mem.data
1750                .remove::<Option<usize>>(table_id.with("column_clicked"));
1751        });
1752
1753        // Ensure the UI cursor is advanced past the full table height
1754        // This is critical when child UIs are used for drawer content
1755        ui.expand_to_include_rect(rect);
1756
1757        DataTableResponse {
1758            response,
1759            selected_rows: state.selected_rows,
1760            header_checkbox: state.header_checkbox,
1761            column_clicked,
1762            sort_state: (state.sorted_column, state.sort_direction.clone()),
1763            row_actions: all_row_actions,
1764        }
1765    }
1766}
1767
1768impl<'a> Default for MaterialDataTable<'a> {
1769    fn default() -> Self {
1770        Self::new()
1771    }
1772}
1773
1774impl Widget for MaterialDataTable<'_> {
1775    fn ui(self, ui: &mut Ui) -> Response {
1776        self.show(ui).response
1777    }
1778}
1779
1780/// Convenience function to create a new data table.
1781pub fn data_table() -> MaterialDataTable<'static> {
1782    MaterialDataTable::new()
1783}