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#[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#[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#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
78pub struct DataTableState {
79 pub selected_rows: Vec<bool>,
81 pub header_checkbox: bool,
83 pub column_sorts: HashMap<String, SortDirection>,
85 pub sorted_column: Option<usize>,
87 pub sort_direction: SortDirection,
89 pub editing_rows: std::collections::HashSet<usize>,
91 pub edit_data: HashMap<usize, Vec<String>>,
93 pub drawer_open_rows: HashSet<usize>,
95}
96
97#[derive(Debug)]
102pub struct DataTableResponse {
103 pub response: Response,
105 pub selected_rows: Vec<bool>,
107 pub header_checkbox: bool,
109 pub column_clicked: Option<usize>,
111 pub sort_state: (Option<usize>, SortDirection),
113 pub row_actions: Vec<RowAction>,
115}
116
117#[derive(Debug, Clone)]
119pub enum RowAction {
120 Edit(usize),
122 Delete(usize),
124 Save(usize),
126 Cancel(usize),
128}
129
130pub 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#[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 pub title: String,
213 pub header_widget: Option<std::sync::Arc<dyn Fn(&mut Ui) + Send + Sync>>,
215 pub width: f32,
217 pub numeric: bool,
219 pub sortable: bool,
221 pub sort_direction: Option<SortDirection>,
223 pub h_align: HAlign,
225 pub v_align: VAlign,
227 pub tooltip: Option<String>,
229 pub heading_alignment: Option<HAlign>,
231 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 selection_externally_set: bool,
312 readonly: bool,
313 id: Option<String>,
314 color: Option<Color32>,
315 on_hover: bool,
316 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 pub fn cell(mut self, text: impl Into<WidgetText>) -> Self {
338 self.cells.push(DataTableCell::text(text));
339 self
340 }
341
342 pub fn custom_cell(mut self, cell: DataTableCell) -> Self {
344 self.cells.push(cell);
345 self
346 }
347
348 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 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 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 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 pub fn get_sort_state(&self) -> (Option<usize>, SortDirection) {
425 (self.sorted_column, self.sort_direction.clone())
426 }
427
428 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, 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 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 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 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 pub fn id(mut self, id: impl Into<Id>) -> Self {
525 self.id = Some(id.into());
526 self
527 }
528
529 pub fn allow_selection(mut self, allow: bool) -> Self {
531 self.allow_selection = allow;
532 self
533 }
534
535 pub fn allow_drawer(mut self, allow: bool) -> Self {
538 self.allow_drawer = allow;
539 self
540 }
541
542 pub fn drawer_row_height(mut self, height: f32) -> Self {
545 self.drawer_row_height = Some(height);
546 self
547 }
548
549 pub fn sticky_header(mut self, sticky: bool) -> Self {
551 self.sticky_header = sticky;
552 self
553 }
554
555 pub fn show_progress(mut self, show: bool) -> Self {
557 self.progress_visible = show;
558 self
559 }
560
561 pub fn corner_radius(mut self, corner_radius: impl Into<CornerRadius>) -> Self {
563 self.corner_radius = corner_radius.into();
564 self
565 }
566
567 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 pub fn auto_row_height(mut self, enabled: bool) -> Self {
580 self.auto_height = enabled;
581 if enabled {
582 self.theme.data_row_min_height = Some(20.0);
584 }
585 self
586 }
587
588 pub fn min_row_height(mut self, height: f32) -> Self {
591 self.theme.data_row_min_height = Some(height);
592 self
593 }
594
595 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 pub fn show(self, ui: &mut Ui) -> DataTableResponse {
610 let (background_color, border_stroke) = self.get_table_style();
611
612 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 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 let mut state: DataTableState =
638 ui.data_mut(|d| d.get_persisted(table_id).unwrap_or_default());
639
640 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 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 if state.selected_rows.len() != self.rows.len() {
659 state.selected_rows.resize(self.rows.len(), false);
660 }
661
662 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 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 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 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 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 let mut header_height: f32 = min_header_height;
749 for column in &columns {
750 let available_width = column.width - 48.0; 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; header_height = header_height.max(content_height);
777 }
778
779 let mut row_heights = Vec::new();
781 for row in &rows {
782 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, 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; max_height = max_height.max(content_height);
821 }
822 CellContent::Widget(_) => {
823 if !auto_height {
826 max_height = max_height.max(min_row_height);
827 }
828 }
829 }
830 }
831 }
832
833 let final_height = max_height.max(min_row_height);
835 row_heights.push(final_height);
836 }
837
838 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 if let Some(fixed_height) = drawer_row_height {
849 fixed_height
850 } else {
851 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) }
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 let mut all_row_actions: Vec<RowAction> = Vec::new();
869
870 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 ui.advance_cursor_after_rect(rect);
900
901 if ui.is_rect_visible(rect) {
902 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 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 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 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 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 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 if allow_drawer {
989 current_x += drawer_arrow_width;
990 }
991
992 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 let available_width = column.width - 48.0; 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 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 if let Some(ref tooltip) = column.tooltip {
1040 header_response = header_response.on_hover_text(tooltip);
1041 }
1042
1043 if header_response.clicked() {
1044 if state.sorted_column == Some(col_idx) {
1046 state.sort_direction = match state.sort_direction {
1048 SortDirection::Ascending => SortDirection::Descending,
1049 SortDirection::Descending => SortDirection::Ascending,
1050 };
1051 } else {
1052 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 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 let arrow_color = if is_sorted {
1078 theme.sort_active_color.unwrap_or_else(|| get_global_color("primary")) } else {
1080 theme.sort_inactive_color.unwrap_or_else(|| get_global_color("onSurfaceVariant"))
1081 };
1082
1083 let center = icon_rect.center();
1084
1085 match sort_direction {
1087 Some(SortDirection::Ascending) => {
1088 let points = [
1090 center + Vec2::new(0.0, -6.0), center + Vec2::new(-5.0, 4.0), center + Vec2::new(5.0, 4.0), ];
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 let points = [
1110 center + Vec2::new(0.0, 6.0), center + Vec2::new(-5.0, -4.0), center + Vec2::new(5.0, -4.0), ];
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 let light_color = arrow_color.gamma_multiply(0.5);
1130 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 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 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 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 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 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 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) } 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 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 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 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 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 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 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 let mut row_actions: Vec<RowAction> = Vec::new();
1376
1377 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 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 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 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 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 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 let available_width = column.width - 32.0; 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, justify: false,
1497 first_row_min_height: 0.0,
1498 round_output_to_gui: true,
1499 });
1500
1501 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 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 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 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 let padding = 8.0;
1561 let available_width = column.width - 2.0 * padding;
1562 let available_height = row_height - 2.0 * padding;
1563
1564 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 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 all_row_actions.extend(row_actions);
1622
1623 current_y += row_height;
1624
1625 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 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 let drawer_bg = get_global_color("surfaceVariant");
1641 ui.painter().rect_filled(
1642 drawer_rect,
1643 CornerRadius::ZERO,
1644 drawer_bg,
1645 );
1646
1647 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 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 let parent_clip_rect = ui.clip_rect();
1666 let clipped_rect = content_rect.intersect(parent_clip_rect);
1667
1668 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 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 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 ui.set_clip_rect(old_clip_rect);
1704
1705 current_y += open_drawer_height;
1706 }
1707 }
1708 }
1709 }
1710
1711 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 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 ui.data_mut(|d| d.insert_persisted(table_id, state.clone()));
1730
1731 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 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 ui.memory_mut(|mem| {
1749 mem.data
1750 .remove::<Option<usize>>(table_id.with("column_clicked"));
1751 });
1752
1753 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
1780pub fn data_table() -> MaterialDataTable<'static> {
1782 MaterialDataTable::new()
1783}