1use crate::theme::get_global_color;
2use egui::{
3 ecolor::Color32,
4 epaint::{Stroke, CornerRadius},
5 FontFamily, FontId,
6 Id, Rect, Response, Sense, Ui, Vec2, Widget, WidgetText,
7};
8use std::collections::{HashMap, HashSet};
9
10#[derive(Clone, Debug, Default)]
15pub struct DataTableState {
16 pub selected_rows: Vec<bool>,
18 pub header_checkbox: bool,
20 pub column_sorts: HashMap<String, SortDirection>,
22 pub sorted_column: Option<usize>,
24 pub sort_direction: SortDirection,
26 pub editing_rows: std::collections::HashSet<usize>,
28 pub edit_data: HashMap<usize, Vec<String>>,
30}
31
32#[derive(Debug)]
37pub struct DataTableResponse {
38 pub response: Response,
40 pub selected_rows: Vec<bool>,
42 pub header_checkbox: bool,
44 pub column_clicked: Option<usize>,
46 pub sort_state: (Option<usize>, SortDirection),
48 pub row_actions: Vec<RowAction>,
50}
51
52#[derive(Debug, Clone)]
54pub enum RowAction {
55 Edit(usize),
57 Delete(usize),
59 Save(usize),
61 Cancel(usize),
63}
64
65#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
88pub struct MaterialDataTable<'a> {
89 columns: Vec<DataTableColumn>,
90 rows: Vec<DataTableRow<'a>>,
91 id: Option<Id>,
92 allow_selection: bool,
93 sticky_header: bool,
94 progress_visible: bool,
95 corner_radius: CornerRadius,
96 sorted_column: Option<usize>,
97 sort_direction: SortDirection,
98}
99
100#[derive(Clone, Debug)]
101pub struct DataTableColumn {
102 pub title: String,
104 pub width: f32,
106 pub numeric: bool,
108 pub sortable: bool,
110 pub sort_direction: Option<SortDirection>,
112}
113
114#[derive(Clone, Debug, PartialEq)]
115pub enum SortDirection {
116 Ascending,
117 Descending,
118}
119
120impl Default for SortDirection {
121 fn default() -> Self {
122 SortDirection::Ascending
123 }
124}
125
126pub struct DataTableRow<'a> {
127 cells: Vec<WidgetText>,
128 selected: bool,
129 readonly: bool,
130 id: Option<String>,
131 _phantom: std::marker::PhantomData<&'a ()>,
132}
133
134impl<'a> DataTableRow<'a> {
135 pub fn new() -> Self {
136 Self {
137 cells: Vec::new(),
138 selected: false,
139 readonly: false,
140 id: None,
141 _phantom: std::marker::PhantomData,
142 }
143 }
144
145 pub fn cell(mut self, text: impl Into<WidgetText>) -> Self {
146 self.cells.push(text.into());
147 self
148 }
149
150 pub fn selected(mut self, selected: bool) -> Self {
151 self.selected = selected;
152 self
153 }
154
155 pub fn readonly(mut self, readonly: bool) -> Self {
156 self.readonly = readonly;
157 self
158 }
159
160 pub fn id(mut self, id: impl Into<String>) -> Self {
161 self.id = Some(id.into());
162 self
163 }
164}
165
166impl<'a> MaterialDataTable<'a> {
167 pub fn new() -> Self {
169 Self {
170 columns: Vec::new(),
171 rows: Vec::new(),
172 id: None,
173 allow_selection: false,
174 sticky_header: false,
175 progress_visible: false,
176 corner_radius: CornerRadius::from(4.0),
177 sorted_column: None,
178 sort_direction: SortDirection::Ascending,
179 }
180 }
181
182 pub fn sort_by(mut self, column_index: usize, direction: SortDirection) -> Self {
184 self.sorted_column = Some(column_index);
185 self.sort_direction = direction;
186 self
187 }
188
189 pub fn get_sort_state(&self) -> (Option<usize>, SortDirection) {
191 (self.sorted_column, self.sort_direction.clone())
192 }
193
194 pub fn column(mut self, title: impl Into<String>, width: f32, numeric: bool) -> Self {
196 self.columns.push(DataTableColumn {
197 title: title.into(),
198 width,
199 numeric,
200 sortable: true, sort_direction: None,
202 });
203 self
204 }
205
206 pub fn sortable_column(mut self, title: impl Into<String>, width: f32, numeric: bool) -> Self {
208 self.columns.push(DataTableColumn {
209 title: title.into(),
210 width,
211 numeric,
212 sortable: true,
213 sort_direction: None,
214 });
215 self
216 }
217
218 pub fn row<F>(mut self, f: F) -> Self
220 where
221 F: FnOnce(DataTableRow<'a>) -> DataTableRow<'a>,
222 {
223 let row = f(DataTableRow::new());
224 self.rows.push(row);
225 self
226 }
227
228 pub fn id(mut self, id: impl Into<Id>) -> Self {
230 self.id = Some(id.into());
231 self
232 }
233
234 pub fn allow_selection(mut self, allow: bool) -> Self {
236 self.allow_selection = allow;
237 self
238 }
239
240 pub fn sticky_header(mut self, sticky: bool) -> Self {
242 self.sticky_header = sticky;
243 self
244 }
245
246 pub fn show_progress(mut self, show: bool) -> Self {
248 self.progress_visible = show;
249 self
250 }
251
252 pub fn corner_radius(mut self, corner_radius: impl Into<CornerRadius>) -> Self {
254 self.corner_radius = corner_radius.into();
255 self
256 }
257
258 fn get_table_style(&self) -> (Color32, Stroke) {
259 let md_surface = get_global_color("surface");
260 let md_outline = get_global_color("outline");
261 (md_surface, Stroke::new(1.0, md_outline))
262 }
263
264 pub fn show(self, ui: &mut Ui) -> DataTableResponse {
266 let (background_color, border_stroke) = self.get_table_style();
267
268 let table_id = self.id.unwrap_or_else(|| {
270 use std::collections::hash_map::DefaultHasher;
271 use std::hash::{Hash, Hasher};
272 let mut hasher = DefaultHasher::new();
273
274 for col in &self.columns {
276 col.title.hash(&mut hasher);
277 col.width.to_bits().hash(&mut hasher);
278 }
279 for (i, row) in self.rows.iter().take(3).enumerate() {
280 i.hash(&mut hasher);
281 for cell in &row.cells {
282 cell.text().hash(&mut hasher);
283 }
284 }
285 self.rows.len().hash(&mut hasher);
286 Id::new(format!("datatable_{}", hasher.finish()))
287 });
288
289 let mut state: DataTableState = ui.data_mut(|d| d.get_persisted(table_id).unwrap_or_default());
291
292 if let Some(external_editing_state) = ui.memory(|mem| {
294 mem.data.get_temp::<(HashSet<usize>, HashMap<usize, Vec<String>>)>(table_id.with("external_edit_state"))
295 }) {
296 state.editing_rows = external_editing_state.0;
297 state.edit_data = external_editing_state.1;
298 }
299
300 if state.sorted_column.is_none() && self.sorted_column.is_some() {
302 state.sorted_column = self.sorted_column;
303 state.sort_direction = self.sort_direction.clone();
304 }
305
306 if state.selected_rows.len() != self.rows.len() {
308 state.selected_rows.resize(self.rows.len(), false);
309 }
310
311 for (i, row) in self.rows.iter().enumerate() {
313 if i < state.selected_rows.len() && row.selected {
314 state.selected_rows[i] = row.selected;
315 }
316 }
317
318 let MaterialDataTable {
319 columns,
320 mut rows,
321 allow_selection,
322 sticky_header: _,
323 progress_visible,
324 corner_radius,
325 ..
326 } = self;
327
328 if let Some(sort_col_idx) = state.sorted_column {
330 if let Some(sort_column) = columns.get(sort_col_idx) {
331 rows.sort_by(|a, b| {
332 let cell_a = a.cells.get(sort_col_idx).map(|c| c.text()).unwrap_or("");
333 let cell_b = b.cells.get(sort_col_idx).map(|c| c.text()).unwrap_or("");
334
335 let comparison = if sort_column.numeric {
336 let a_num: f64 = cell_a.trim_start_matches('$').parse().unwrap_or(0.0);
338 let b_num: f64 = cell_b.trim_start_matches('$').parse().unwrap_or(0.0);
339 a_num.partial_cmp(&b_num).unwrap_or(std::cmp::Ordering::Equal)
340 } else {
341 cell_a.cmp(cell_b)
343 };
344
345 match state.sort_direction {
346 SortDirection::Ascending => comparison,
347 SortDirection::Descending => comparison.reverse(),
348 }
349 });
350 }
351 }
352
353 let checkbox_width = if allow_selection { 48.0 } else { 0.0 };
355 let total_width = checkbox_width + columns.iter().map(|col| col.width).sum::<f32>();
356 let min_row_height = 52.0;
357 let min_header_height = 56.0;
358
359 let mut header_height: f32 = min_header_height;
361 for column in &columns {
362 let available_width = column.width - 48.0; let header_font = FontId::new(16.0, FontFamily::Proportional);
364
365 let galley = ui.fonts(|f| f.layout_job(egui::text::LayoutJob {
366 text: column.title.clone(),
367 sections: vec![egui::text::LayoutSection {
368 leading_space: 0.0,
369 byte_range: 0..column.title.len(),
370 format: egui::TextFormat {
371 font_id: header_font,
372 color: get_global_color("onSurface"),
373 ..Default::default()
374 },
375 }],
376 wrap: egui::text::TextWrapping {
377 max_width: available_width,
378 ..Default::default()
379 },
380 break_on_newline: true,
381 halign: egui::Align::LEFT,
382 justify: false,
383 first_row_min_height: 0.0,
384 round_output_to_gui: true,
385 }));
386
387 let content_height: f32 = galley.size().y + 16.0; header_height = header_height.max(content_height);
389 }
390
391 let mut row_heights = Vec::new();
393 for row in &rows {
394 let mut max_height: f32 = min_row_height;
395 for (cell_idx, cell_text) in row.cells.iter().enumerate() {
396 if let Some(column) = columns.get(cell_idx) {
397 let available_width = column.width - 32.0;
398 let cell_font = FontId::new(14.0, FontFamily::Proportional);
399
400 let galley = ui.fonts(|f| f.layout_job(egui::text::LayoutJob {
401 text: cell_text.text().to_string(),
402 sections: vec![egui::text::LayoutSection {
403 leading_space: 0.0,
404 byte_range: 0..cell_text.text().len(),
405 format: egui::TextFormat {
406 font_id: cell_font,
407 color: get_global_color("onSurface"),
408 ..Default::default()
409 },
410 }],
411 wrap: egui::text::TextWrapping {
412 max_width: available_width,
413 ..Default::default()
414 },
415 break_on_newline: true,
416 halign: if column.numeric { egui::Align::RIGHT } else { egui::Align::LEFT },
417 justify: false,
418 first_row_min_height: 0.0,
419 round_output_to_gui: true,
420 }));
421
422 let content_height: f32 = galley.size().y + 16.0; max_height = max_height.max(content_height);
424 }
425 }
426 row_heights.push(max_height);
427 }
428
429 let total_height = header_height + row_heights.iter().sum::<f32>();
430
431 let mut all_row_actions: Vec<RowAction> = Vec::new();
433
434 let desired_size = Vec2::new(total_width, total_height);
435 let response = ui.allocate_response(desired_size, Sense::click());
436 let rect = response.rect;
437
438 if ui.is_rect_visible(rect) {
439 ui.painter().rect_filled(rect, corner_radius, background_color);
441 ui.painter().rect_stroke(rect, corner_radius, border_stroke, egui::epaint::StrokeKind::Outside);
442
443 let mut current_y = rect.min.y;
444
445 let header_rect = Rect::from_min_size(rect.min, Vec2::new(total_width, header_height));
447 let header_bg = get_global_color("surfaceVariant");
448 ui.painter().rect_filled(header_rect, CornerRadius::ZERO, header_bg);
449
450 let mut current_x = rect.min.x;
451
452 if allow_selection {
454 let checkbox_rect = Rect::from_min_size(
455 egui::pos2(current_x, current_y),
456 Vec2::new(checkbox_width, header_height)
457 );
458
459 let checkbox_center = checkbox_rect.center();
460 let checkbox_size = Vec2::splat(18.0);
461 let checkbox_inner_rect = Rect::from_center_size(checkbox_center, checkbox_size);
462
463 let checkbox_color = if state.header_checkbox {
464 get_global_color("primary")
465 } else {
466 Color32::TRANSPARENT
467 };
468
469 ui.painter().rect_filled(
470 checkbox_inner_rect,
471 CornerRadius::from(2.0),
472 checkbox_color
473 );
474 ui.painter().rect_stroke(
475 checkbox_inner_rect,
476 CornerRadius::from(2.0),
477 Stroke::new(2.0, get_global_color("outline")),
478 egui::epaint::StrokeKind::Outside
479 );
480
481 if state.header_checkbox {
482 let check_points = [
484 checkbox_inner_rect.min + Vec2::new(4.0, 9.0),
485 checkbox_inner_rect.min + Vec2::new(8.0, 13.0),
486 checkbox_inner_rect.min + Vec2::new(14.0, 5.0),
487 ];
488 ui.painter().line_segment(
489 [check_points[0], check_points[1]],
490 Stroke::new(2.0, Color32::WHITE)
491 );
492 ui.painter().line_segment(
493 [check_points[1], check_points[2]],
494 Stroke::new(2.0, Color32::WHITE)
495 );
496 }
497
498 let header_checkbox_id = table_id.with("header_checkbox");
500 let checkbox_response = ui.interact(checkbox_inner_rect, header_checkbox_id, Sense::click());
501 if checkbox_response.clicked() {
502 state.header_checkbox = !state.header_checkbox;
503 for (idx, selected) in state.selected_rows.iter_mut().enumerate() {
505 if let Some(row) = rows.get(idx) {
506 if !row.readonly {
507 *selected = state.header_checkbox;
508 }
509 }
510 }
511 }
512
513 current_x += checkbox_width;
514 }
515
516 for (col_idx, column) in columns.iter().enumerate() {
518 let col_rect = Rect::from_min_size(
519 egui::pos2(current_x, current_y),
520 Vec2::new(column.width, header_height)
521 );
522
523 let available_width = column.width - 48.0; let header_font = FontId::new(16.0, FontFamily::Proportional);
526
527 let galley = ui.fonts(|f| f.layout_job(egui::text::LayoutJob {
528 text: column.title.clone(),
529 sections: vec![egui::text::LayoutSection {
530 leading_space: 0.0,
531 byte_range: 0..column.title.len(),
532 format: egui::TextFormat {
533 font_id: header_font,
534 color: get_global_color("onSurface"),
535 ..Default::default()
536 },
537 }],
538 wrap: egui::text::TextWrapping {
539 max_width: available_width,
540 ..Default::default()
541 },
542 break_on_newline: true,
543 halign: egui::Align::LEFT,
544 justify: false,
545 first_row_min_height: 0.0,
546 round_output_to_gui: true,
547 }));
548
549 let text_pos = egui::pos2(
550 current_x + 16.0,
551 current_y + (header_height - galley.size().y) / 2.0
552 );
553
554 ui.painter().galley(text_pos, galley, get_global_color("onSurface"));
555
556 if column.sortable {
558 let header_click_id = table_id.with(format!("column_header_{}", col_idx));
559 let header_response = ui.interact(col_rect, header_click_id, Sense::click());
560 if header_response.clicked() {
561 if state.sorted_column == Some(col_idx) {
563 state.sort_direction = match state.sort_direction {
565 SortDirection::Ascending => SortDirection::Descending,
566 SortDirection::Descending => SortDirection::Ascending,
567 };
568 } else {
569 state.sorted_column = Some(col_idx);
571 state.sort_direction = SortDirection::Ascending;
572 }
573 ui.memory_mut(|mem| {
574 mem.data.insert_temp(table_id.with("column_clicked"), Some(col_idx));
575 });
576 }
577
578 let icon_pos = egui::pos2(
579 current_x + column.width - 32.0,
580 current_y + (header_height - 24.0) / 2.0
581 );
582 let icon_rect = Rect::from_min_size(icon_pos, Vec2::splat(24.0));
583
584 let is_sorted = state.sorted_column == Some(col_idx);
586 let sort_direction = if is_sorted { Some(&state.sort_direction) } else { None };
587
588 let arrow_color = if is_sorted {
590 get_global_color("primary") } else {
592 get_global_color("onSurfaceVariant")
593 };
594
595 let center = icon_rect.center();
596
597 match sort_direction {
599 Some(SortDirection::Ascending) => {
600 let points = [
602 center + Vec2::new(0.0, -6.0), center + Vec2::new(-5.0, 4.0), center + Vec2::new(5.0, 4.0), ];
606 ui.painter().line_segment([points[0], points[1]], Stroke::new(2.0, arrow_color));
607 ui.painter().line_segment([points[1], points[2]], Stroke::new(2.0, arrow_color));
608 ui.painter().line_segment([points[2], points[0]], Stroke::new(2.0, arrow_color));
609 },
610 Some(SortDirection::Descending) => {
611 let points = [
613 center + Vec2::new(0.0, 6.0), center + Vec2::new(-5.0, -4.0), center + Vec2::new(5.0, -4.0), ];
617 ui.painter().line_segment([points[0], points[1]], Stroke::new(2.0, arrow_color));
618 ui.painter().line_segment([points[1], points[2]], Stroke::new(2.0, arrow_color));
619 ui.painter().line_segment([points[2], points[0]], Stroke::new(2.0, arrow_color));
620 },
621 None => {
622 let light_color = arrow_color.gamma_multiply(0.5);
624 let up_points = [
626 center + Vec2::new(0.0, -8.0),
627 center + Vec2::new(-3.0, -2.0),
628 center + Vec2::new(3.0, -2.0),
629 ];
630 ui.painter().line_segment([up_points[0], up_points[1]], Stroke::new(1.0, light_color));
631 ui.painter().line_segment([up_points[1], up_points[2]], Stroke::new(1.0, light_color));
632 ui.painter().line_segment([up_points[2], up_points[0]], Stroke::new(1.0, light_color));
633
634 let down_points = [
636 center + Vec2::new(0.0, 8.0),
637 center + Vec2::new(-3.0, 2.0),
638 center + Vec2::new(3.0, 2.0),
639 ];
640 ui.painter().line_segment([down_points[0], down_points[1]], Stroke::new(1.0, light_color));
641 ui.painter().line_segment([down_points[1], down_points[2]], Stroke::new(1.0, light_color));
642 ui.painter().line_segment([down_points[2], down_points[0]], Stroke::new(1.0, light_color));
643 }
644 }
645 }
646
647 current_x += column.width;
648 }
649
650 current_y += header_height;
651
652
653 for (row_idx, row) in rows.iter().enumerate() {
655 let row_height = row_heights.get(row_idx).copied().unwrap_or(min_row_height);
656 let row_rect = Rect::from_min_size(
657 egui::pos2(rect.min.x, current_y),
658 Vec2::new(total_width, row_height)
659 );
660
661 let row_selected = state.selected_rows.get(row_idx).copied().unwrap_or(false);
662 let row_bg = if row_selected {
663 get_global_color("primaryContainer")
664 } else if row.readonly {
665 let surface_variant = get_global_color("surfaceVariant");
667 Color32::from_rgba_premultiplied(
668 surface_variant.r(),
669 surface_variant.g(),
670 surface_variant.b(),
671 (surface_variant.a() as f32 * 0.3) as u8
672 )
673 } else if row_idx % 2 == 1 {
674 get_global_color("surfaceVariant")
675 } else {
676 background_color
677 };
678
679 ui.painter().rect_filled(row_rect, CornerRadius::ZERO, row_bg);
680
681 current_x = rect.min.x;
682
683 if allow_selection {
685 let checkbox_rect = Rect::from_min_size(
686 egui::pos2(current_x, current_y),
687 Vec2::new(checkbox_width, row_height)
688 );
689
690 let checkbox_center = checkbox_rect.center();
691 let checkbox_size = Vec2::splat(18.0);
692 let checkbox_inner_rect = Rect::from_center_size(checkbox_center, checkbox_size);
693
694 let checkbox_color = if row_selected {
695 get_global_color("primary")
696 } else {
697 Color32::TRANSPARENT
698 };
699
700 let border_color = if row.readonly {
701 get_global_color("outline").linear_multiply(0.5) } else {
703 get_global_color("outline")
704 };
705
706 ui.painter().rect_filled(
707 checkbox_inner_rect,
708 CornerRadius::from(2.0),
709 checkbox_color
710 );
711 ui.painter().rect_stroke(
712 checkbox_inner_rect,
713 CornerRadius::from(2.0),
714 Stroke::new(2.0, border_color),
715 egui::epaint::StrokeKind::Outside
716 );
717
718 if row_selected {
719 let check_points = [
721 checkbox_inner_rect.min + Vec2::new(4.0, 9.0),
722 checkbox_inner_rect.min + Vec2::new(8.0, 13.0),
723 checkbox_inner_rect.min + Vec2::new(14.0, 5.0),
724 ];
725 ui.painter().line_segment(
726 [check_points[0], check_points[1]],
727 Stroke::new(2.0, Color32::WHITE)
728 );
729 ui.painter().line_segment(
730 [check_points[1], check_points[2]],
731 Stroke::new(2.0, Color32::WHITE)
732 );
733 }
734
735 let row_checkbox_id = table_id.with(format!("row_checkbox_{}", row_idx));
737 let checkbox_response = ui.interact(checkbox_inner_rect, row_checkbox_id, Sense::click());
738 if checkbox_response.clicked() && !row.readonly {
739 if let Some(selected) = state.selected_rows.get_mut(row_idx) {
740 *selected = !*selected;
741 }
742
743 let non_readonly_indices: Vec<usize> = rows.iter()
746 .enumerate()
747 .filter(|(_, row)| !row.readonly)
748 .map(|(idx, _)| idx)
749 .collect();
750
751 if !non_readonly_indices.is_empty() {
752 let all_non_readonly_selected = non_readonly_indices.iter()
753 .all(|&idx| state.selected_rows.get(idx).copied().unwrap_or(false));
754 let none_non_readonly_selected = non_readonly_indices.iter()
755 .all(|&idx| !state.selected_rows.get(idx).copied().unwrap_or(false));
756 state.header_checkbox = all_non_readonly_selected && !none_non_readonly_selected;
757 }
758 }
759
760 current_x += checkbox_width;
761 }
762
763 let mut row_actions: Vec<RowAction> = Vec::new();
765
766 for (cell_idx, cell_text) in row.cells.iter().enumerate() {
768 if let Some(column) = columns.get(cell_idx) {
769 let _cell_rect = Rect::from_min_size(
770 egui::pos2(current_x, current_y),
771 Vec2::new(column.width, row_height)
772 );
773
774 let is_row_editing = state.editing_rows.contains(&row_idx);
775 let is_actions_column = column.title == "Actions";
776
777 if is_actions_column {
778 let button_rect = Rect::from_min_size(
780 egui::pos2(current_x + 8.0, current_y + (row_height - 32.0) / 2.0),
781 Vec2::new(column.width - 16.0, 32.0)
782 );
783
784 ui.scope_builder(egui::UiBuilder::new().max_rect(button_rect), |ui| {
785 ui.horizontal(|ui| {
786 if is_row_editing {
787 if ui.small_button("Save").clicked() {
788 row_actions.push(RowAction::Save(row_idx));
789 }
790 if ui.small_button("Cancel").clicked() {
791 row_actions.push(RowAction::Cancel(row_idx));
792 }
793 } else {
794 if ui.small_button("Edit").clicked() {
795 row_actions.push(RowAction::Edit(row_idx));
796 }
797 if ui.small_button("Delete").clicked() {
798 row_actions.push(RowAction::Delete(row_idx));
799 }
800 }
801 });
802 });
803 } else if is_row_editing {
804 let edit_rect = Rect::from_min_size(
806 egui::pos2(current_x + 8.0, current_y + (row_height - 24.0) / 2.0),
807 Vec2::new(column.width - 16.0, 24.0)
808 );
809
810 let edit_data = state.edit_data
812 .entry(row_idx)
813 .or_insert_with(|| {
814 row.cells.iter().map(|c| c.text().to_string()).collect()
815 });
816
817 if edit_data.len() <= cell_idx {
819 edit_data.resize(cell_idx + 1, String::new());
820 }
821
822 let edit_text = &mut edit_data[cell_idx];
823
824 ui.scope_builder(egui::UiBuilder::new().max_rect(edit_rect), |ui| {
825 ui.add(egui::TextEdit::singleline(edit_text)
826 .desired_width(column.width - 16.0));
827 });
828 } else {
829 let _text_align = if column.numeric {
831 egui::Align2::RIGHT_CENTER
832 } else {
833 egui::Align2::LEFT_CENTER
834 };
835
836 let available_width = column.width - 32.0; let cell_font = FontId::new(14.0, FontFamily::Proportional);
839
840 let galley = ui.fonts(|f| f.layout_job(egui::text::LayoutJob {
841 text: cell_text.text().to_string(),
842 sections: vec![egui::text::LayoutSection {
843 leading_space: 0.0,
844 byte_range: 0..cell_text.text().len(),
845 format: egui::TextFormat {
846 font_id: cell_font,
847 color: get_global_color("onSurface"),
848 ..Default::default()
849 },
850 }],
851 wrap: egui::text::TextWrapping {
852 max_width: available_width,
853 ..Default::default()
854 },
855 break_on_newline: true,
856 halign: if column.numeric { egui::Align::RIGHT } else { egui::Align::LEFT },
857 justify: false,
858 first_row_min_height: 0.0,
859 round_output_to_gui: true,
860 }));
861
862 let text_pos = if column.numeric {
863 egui::pos2(current_x + column.width - 16.0 - galley.size().x, current_y + (row_height - galley.size().y) / 2.0)
864 } else {
865 egui::pos2(current_x + 16.0, current_y + (row_height - galley.size().y) / 2.0)
866 };
867
868 ui.painter().galley(text_pos, galley, get_global_color("onSurface"));
869 }
870
871 current_x += column.width;
872 }
873 }
874
875 all_row_actions.extend(row_actions);
877
878 current_y += row_height;
879 }
880
881 if progress_visible {
883 let scrim_color = Color32::from_rgba_unmultiplied(255, 255, 255, 128);
884 ui.painter().rect_filled(rect, corner_radius, scrim_color);
885
886 let progress_rect = Rect::from_min_size(
888 egui::pos2(rect.min.x, rect.min.y + header_height),
889 Vec2::new(total_width, 4.0)
890 );
891
892 let progress_color = get_global_color("primary");
893 ui.painter().rect_filled(progress_rect, CornerRadius::ZERO, progress_color);
894 }
895 }
896
897 ui.data_mut(|d| d.insert_persisted(table_id, state.clone()));
899
900 ui.memory_mut(|mem| {
902 mem.data.insert_temp(table_id.with("external_edit_state"), (state.editing_rows.clone(), state.edit_data.clone()));
903 });
904
905 let column_clicked = ui.memory(|mem| {
907 mem.data.get_temp::<Option<usize>>(table_id.with("column_clicked"))
908 }).flatten();
909
910 ui.memory_mut(|mem| {
912 mem.data.remove::<Option<usize>>(table_id.with("column_clicked"));
913 });
914
915 DataTableResponse {
916 response,
917 selected_rows: state.selected_rows,
918 header_checkbox: state.header_checkbox,
919 column_clicked,
920 sort_state: (state.sorted_column, state.sort_direction.clone()),
921 row_actions: all_row_actions,
922 }
923 }
924}
925
926impl<'a> Default for MaterialDataTable<'a> {
927 fn default() -> Self {
928 Self::new()
929 }
930}
931
932impl Widget for MaterialDataTable<'_> {
933 fn ui(self, ui: &mut Ui) -> Response {
934 self.show(ui).response
935 }
936}
937
938pub fn data_table() -> MaterialDataTable<'static> {
940 MaterialDataTable::new()
941}