1use egui::{
7 scroll_area::{ScrollAreaOutput, ScrollBarVisibility},
8 Align, Id, NumExt as _, Rangef, Rect, Response, ScrollArea, Ui, Vec2, Vec2b,
9};
10
11use crate::{
12 layout::{CellDirection, CellSize, StripLayoutFlags},
13 StripLayout,
14};
15
16#[derive(Clone, Copy, Debug, PartialEq)]
19enum InitialColumnSize {
20 Absolute(f32),
22
23 Automatic(f32),
25
26 Remainder,
28}
29
30#[derive(Clone, Copy, Debug, PartialEq)]
32pub struct Column {
33 initial_width: InitialColumnSize,
34
35 width_range: Rangef,
36
37 clip: bool,
39
40 resizable: Option<bool>,
41
42 auto_size_this_frame: bool,
45}
46
47impl Column {
48 pub fn auto() -> Self {
54 Self::auto_with_initial_suggestion(100.0)
55 }
56
57 pub fn auto_with_initial_suggestion(suggested_width: f32) -> Self {
63 Self::new(InitialColumnSize::Automatic(suggested_width))
64 }
65
66 pub fn initial(width: f32) -> Self {
68 Self::new(InitialColumnSize::Absolute(width))
69 }
70
71 pub fn exact(width: f32) -> Self {
73 Self::new(InitialColumnSize::Absolute(width))
74 .range(width..=width)
75 .clip(true)
76 }
77
78 pub fn remainder() -> Self {
84 Self::new(InitialColumnSize::Remainder)
85 }
86
87 fn new(initial_width: InitialColumnSize) -> Self {
88 Self {
89 initial_width,
90 width_range: Rangef::new(0.0, f32::INFINITY),
91 resizable: None,
92 clip: false,
93 auto_size_this_frame: false,
94 }
95 }
96
97 #[inline]
102 pub fn resizable(mut self, resizable: bool) -> Self {
103 self.resizable = Some(resizable);
104 self
105 }
106
107 #[inline]
116 pub fn clip(mut self, clip: bool) -> Self {
117 self.clip = clip;
118 self
119 }
120
121 #[inline]
125 pub fn at_least(mut self, minimum: f32) -> Self {
126 self.width_range.min = minimum;
127 self
128 }
129
130 #[inline]
134 pub fn at_most(mut self, maximum: f32) -> Self {
135 self.width_range.max = maximum;
136 self
137 }
138
139 #[inline]
141 pub fn range(mut self, range: impl Into<Rangef>) -> Self {
142 self.width_range = range.into();
143 self
144 }
145
146 #[inline]
150 pub fn auto_size_this_frame(mut self, auto_size_this_frame: bool) -> Self {
151 self.auto_size_this_frame = auto_size_this_frame;
152 self
153 }
154
155 fn is_auto(&self) -> bool {
156 match self.initial_width {
157 InitialColumnSize::Automatic(_) => true,
158 InitialColumnSize::Absolute(_) | InitialColumnSize::Remainder => false,
159 }
160 }
161}
162
163fn to_sizing(columns: &[Column]) -> crate::sizing::Sizing {
164 use crate::Size;
165
166 let mut sizing = crate::sizing::Sizing::default();
167 for column in columns {
168 let size = match column.initial_width {
169 InitialColumnSize::Absolute(width) => Size::exact(width),
170 InitialColumnSize::Automatic(suggested_width) => Size::initial(suggested_width),
171 InitialColumnSize::Remainder => Size::remainder(),
172 }
173 .with_range(column.width_range);
174 sizing.add(size);
175 }
176 sizing
177}
178
179struct TableScrollOptions {
182 vscroll: bool,
183 drag_to_scroll: bool,
184 stick_to_bottom: bool,
185 scroll_to_row: Option<(usize, Option<Align>)>,
186 scroll_offset_y: Option<f32>,
187 min_scrolled_height: f32,
188 max_scroll_height: f32,
189 auto_shrink: Vec2b,
190 scroll_bar_visibility: ScrollBarVisibility,
191 animated: bool,
192}
193
194impl Default for TableScrollOptions {
195 fn default() -> Self {
196 Self {
197 vscroll: true,
198 drag_to_scroll: true,
199 stick_to_bottom: false,
200 scroll_to_row: None,
201 scroll_offset_y: None,
202 min_scrolled_height: 200.0,
203 max_scroll_height: f32::INFINITY,
204 auto_shrink: Vec2b::TRUE,
205 scroll_bar_visibility: ScrollBarVisibility::VisibleWhenNeeded,
206 animated: true,
207 }
208 }
209}
210
211pub struct TableBuilder<'a> {
248 ui: &'a mut Ui,
249 id_salt: Id,
250 columns: Vec<Column>,
251 striped: Option<bool>,
252 resizable: bool,
253 cell_layout: egui::Layout,
254 scroll_options: TableScrollOptions,
255 sense: egui::Sense,
256}
257
258impl<'a> TableBuilder<'a> {
259 pub fn new(ui: &'a mut Ui) -> Self {
260 let cell_layout = *ui.layout();
261 Self {
262 ui,
263 id_salt: Id::new("__table_state"),
264 columns: Default::default(),
265 striped: None,
266 resizable: false,
267 cell_layout,
268 scroll_options: Default::default(),
269 sense: egui::Sense::hover(),
270 }
271 }
272
273 #[inline]
277 #[deprecated = "Renamed id_salt"]
278 pub fn id_source(self, id_salt: impl std::hash::Hash) -> Self {
279 self.id_salt(id_salt)
280 }
281
282 #[inline]
286 pub fn id_salt(mut self, id_salt: impl std::hash::Hash) -> Self {
287 self.id_salt = Id::new(id_salt);
288 self
289 }
290
291 #[inline]
295 pub fn striped(mut self, striped: bool) -> Self {
296 self.striped = Some(striped);
297 self
298 }
299
300 #[inline]
302 pub fn sense(mut self, sense: egui::Sense) -> Self {
303 self.sense = sense;
304 self
305 }
306
307 #[inline]
318 pub fn resizable(mut self, resizable: bool) -> Self {
319 self.resizable = resizable;
320 self
321 }
322
323 #[inline]
325 pub fn vscroll(mut self, vscroll: bool) -> Self {
326 self.scroll_options.vscroll = vscroll;
327 self
328 }
329
330 #[inline]
334 pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self {
335 self.scroll_options.drag_to_scroll = drag_to_scroll;
336 self
337 }
338
339 #[inline]
343 pub fn stick_to_bottom(mut self, stick: bool) -> Self {
344 self.scroll_options.stick_to_bottom = stick;
345 self
346 }
347
348 #[inline]
356 pub fn scroll_to_row(mut self, row: usize, align: Option<Align>) -> Self {
357 self.scroll_options.scroll_to_row = Some((row, align));
358 self
359 }
360
361 #[inline]
365 pub fn vertical_scroll_offset(mut self, offset: f32) -> Self {
366 self.scroll_options.scroll_offset_y = Some(offset);
367 self
368 }
369
370 #[inline]
377 pub fn min_scrolled_height(mut self, min_scrolled_height: f32) -> Self {
378 self.scroll_options.min_scrolled_height = min_scrolled_height;
379 self
380 }
381
382 #[inline]
387 pub fn max_scroll_height(mut self, max_scroll_height: f32) -> Self {
388 self.scroll_options.max_scroll_height = max_scroll_height;
389 self
390 }
391
392 #[inline]
400 pub fn auto_shrink(mut self, auto_shrink: impl Into<Vec2b>) -> Self {
401 self.scroll_options.auto_shrink = auto_shrink.into();
402 self
403 }
404
405 #[inline]
409 pub fn scroll_bar_visibility(mut self, scroll_bar_visibility: ScrollBarVisibility) -> Self {
410 self.scroll_options.scroll_bar_visibility = scroll_bar_visibility;
411 self
412 }
413
414 #[inline]
418 pub fn animate_scrolling(mut self, animated: bool) -> Self {
419 self.scroll_options.animated = animated;
420 self
421 }
422
423 #[inline]
425 pub fn cell_layout(mut self, cell_layout: egui::Layout) -> Self {
426 self.cell_layout = cell_layout;
427 self
428 }
429
430 #[inline]
432 pub fn column(mut self, column: Column) -> Self {
433 self.columns.push(column);
434 self
435 }
436
437 #[inline]
439 pub fn columns(mut self, column: Column, count: usize) -> Self {
440 for _ in 0..count {
441 self.columns.push(column);
442 }
443 self
444 }
445
446 fn available_width(&self) -> f32 {
447 self.ui.available_rect_before_wrap().width()
448 - (self.scroll_options.vscroll as i32 as f32)
449 * self.ui.spacing().scroll.allocated_width()
450 }
451
452 pub fn reset(&self) {
454 let state_id = self.ui.id().with(self.id_salt);
455 TableState::reset(self.ui, state_id);
456 }
457
458 pub fn header(self, height: f32, add_header_row: impl FnOnce(TableRow<'_, '_>)) -> Table<'a> {
460 let available_width = self.available_width();
461
462 let Self {
463 ui,
464 id_salt,
465 mut columns,
466 striped,
467 resizable,
468 cell_layout,
469 scroll_options,
470 sense,
471 } = self;
472
473 for (i, column) in columns.iter_mut().enumerate() {
474 let column_resize_id = ui.id().with("resize_column").with(i);
475 if let Some(response) = ui.ctx().read_response(column_resize_id) {
476 if response.double_clicked() {
477 column.auto_size_this_frame = true;
478 }
479 }
480 }
481
482 let striped = striped.unwrap_or(ui.visuals().striped);
483
484 let state_id = ui.id().with(id_salt);
485
486 let (is_sizing_pass, state) =
487 TableState::load(ui, state_id, resizable, &columns, available_width);
488
489 let mut max_used_widths = vec![0.0; columns.len()];
490 let table_top = ui.cursor().top();
491
492 let mut ui_builder = egui::UiBuilder::new();
493 if is_sizing_pass {
494 ui_builder = ui_builder.sizing_pass();
495 }
496 ui.scope_builder(ui_builder, |ui| {
497 let mut layout = StripLayout::new(ui, CellDirection::Horizontal, cell_layout, sense);
498 let mut response: Option<Response> = None;
499 add_header_row(TableRow {
500 layout: &mut layout,
501 columns: &columns,
502 widths: &state.column_widths,
503 max_used_widths: &mut max_used_widths,
504 row_index: 0,
505 col_index: 0,
506 height,
507 striped: false,
508 hovered: false,
509 selected: false,
510 response: &mut response,
511 });
512 layout.allocate_rect();
513 });
514
515 Table {
516 ui,
517 table_top,
518 state_id,
519 columns,
520 available_width,
521 state,
522 max_used_widths,
523 is_sizing_pass,
524 resizable,
525 striped,
526 cell_layout,
527 scroll_options,
528 sense,
529 }
530 }
531
532 pub fn body<F>(self, add_body_contents: F) -> ScrollAreaOutput<()>
534 where
535 F: for<'b> FnOnce(TableBody<'b>),
536 {
537 let available_width = self.available_width();
538
539 let Self {
540 ui,
541 id_salt,
542 columns,
543 striped,
544 resizable,
545 cell_layout,
546 scroll_options,
547 sense,
548 } = self;
549
550 let striped = striped.unwrap_or(ui.visuals().striped);
551
552 let state_id = ui.id().with(id_salt);
553
554 let (is_sizing_pass, state) =
555 TableState::load(ui, state_id, resizable, &columns, available_width);
556
557 let max_used_widths = vec![0.0; columns.len()];
558 let table_top = ui.cursor().top();
559
560 Table {
561 ui,
562 table_top,
563 state_id,
564 columns,
565 available_width,
566 state,
567 max_used_widths,
568 is_sizing_pass,
569 resizable,
570 striped,
571 cell_layout,
572 scroll_options,
573 sense,
574 }
575 .body(add_body_contents)
576 }
577}
578
579#[derive(Clone, Debug)]
582#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
583struct TableState {
584 column_widths: Vec<f32>,
585
586 #[cfg_attr(feature = "serde", serde(skip))]
588 max_used_widths: Vec<f32>,
589}
590
591impl TableState {
592 fn load(
594 ui: &Ui,
595 state_id: egui::Id,
596 resizable: bool,
597 columns: &[Column],
598 available_width: f32,
599 ) -> (bool, Self) {
600 let rect = Rect::from_min_size(ui.available_rect_before_wrap().min, Vec2::ZERO);
601 ui.ctx().check_for_id_clash(state_id, rect, "Table");
602
603 #[cfg(feature = "serde")]
604 let state = ui.data_mut(|d| d.get_persisted::<Self>(state_id));
605 #[cfg(not(feature = "serde"))]
606 let state = ui.data_mut(|d| d.get_temp::<Self>(state_id));
607
608 let state = state.filter(|state| state.column_widths.len() == columns.len());
610
611 let is_sizing_pass =
612 ui.is_sizing_pass() || state.is_none() && columns.iter().any(|c| c.is_auto());
613
614 let mut state = state.unwrap_or_else(|| {
615 let initial_widths =
616 to_sizing(columns).to_lengths(available_width, ui.spacing().item_spacing.x);
617 Self {
618 column_widths: initial_widths,
619 max_used_widths: Default::default(),
620 }
621 });
622
623 if !is_sizing_pass && state.max_used_widths.len() == columns.len() {
624 let mut sizing = crate::sizing::Sizing::default();
628 for ((prev_width, max_used), column) in state
629 .column_widths
630 .iter()
631 .zip(&state.max_used_widths)
632 .zip(columns)
633 {
634 use crate::Size;
635
636 let column_resizable = column.resizable.unwrap_or(resizable);
637 let size = if column_resizable {
638 Size::exact(*prev_width)
640 } else {
641 match column.initial_width {
642 InitialColumnSize::Absolute(width) => Size::exact(width),
643 InitialColumnSize::Automatic(_) => Size::exact(*prev_width),
644 InitialColumnSize::Remainder => Size::remainder(),
645 }
646 .at_least(column.width_range.min.max(*max_used))
647 .at_most(column.width_range.max)
648 };
649 sizing.add(size);
650 }
651 state.column_widths = sizing.to_lengths(available_width, ui.spacing().item_spacing.x);
652 }
653
654 (is_sizing_pass, state)
655 }
656
657 fn store(self, ui: &egui::Ui, state_id: egui::Id) {
658 #![allow(clippy::needless_return)]
659 #[cfg(feature = "serde")]
660 {
661 return ui.data_mut(|d| d.insert_persisted(state_id, self));
662 }
663 #[cfg(not(feature = "serde"))]
664 {
665 return ui.data_mut(|d| d.insert_temp(state_id, self));
666 }
667 }
668
669 fn reset(ui: &egui::Ui, state_id: egui::Id) {
670 ui.data_mut(|d| d.remove::<Self>(state_id));
671 }
672}
673
674pub struct Table<'a> {
680 ui: &'a mut Ui,
681 table_top: f32,
682 state_id: egui::Id,
683 columns: Vec<Column>,
684 available_width: f32,
685 state: TableState,
686
687 max_used_widths: Vec<f32>,
689
690 is_sizing_pass: bool,
692 resizable: bool,
693 striped: bool,
694 cell_layout: egui::Layout,
695
696 scroll_options: TableScrollOptions,
697
698 sense: egui::Sense,
699}
700
701impl<'a> Table<'a> {
702 pub fn ui_mut(&mut self) -> &mut egui::Ui {
706 self.ui
707 }
708
709 pub fn body<F>(self, add_body_contents: F) -> ScrollAreaOutput<()>
711 where
712 F: for<'b> FnOnce(TableBody<'b>),
713 {
714 let Table {
715 ui,
716 table_top,
717 state_id,
718 columns,
719 resizable,
720 mut available_width,
721 mut state,
722 mut max_used_widths,
723 is_sizing_pass,
724 striped,
725 cell_layout,
726 scroll_options,
727 sense,
728 } = self;
729
730 let TableScrollOptions {
731 vscroll,
732 drag_to_scroll,
733 stick_to_bottom,
734 scroll_to_row,
735 scroll_offset_y,
736 min_scrolled_height,
737 max_scroll_height,
738 auto_shrink,
739 scroll_bar_visibility,
740 animated,
741 } = scroll_options;
742
743 let cursor_position = ui.cursor().min;
744
745 let mut scroll_area = ScrollArea::new([false, vscroll])
746 .id_salt(state_id.with("__scroll_area"))
747 .drag_to_scroll(drag_to_scroll)
748 .stick_to_bottom(stick_to_bottom)
749 .min_scrolled_height(min_scrolled_height)
750 .max_height(max_scroll_height)
751 .auto_shrink(auto_shrink)
752 .scroll_bar_visibility(scroll_bar_visibility)
753 .animated(animated);
754
755 if let Some(scroll_offset_y) = scroll_offset_y {
756 scroll_area = scroll_area.vertical_scroll_offset(scroll_offset_y);
757 }
758
759 let columns_ref = &columns;
760 let widths_ref = &state.column_widths;
761 let max_used_widths_ref = &mut max_used_widths;
762
763 let scroll_area_out = scroll_area.show(ui, move |ui| {
764 let mut scroll_to_y_range = None;
765
766 let clip_rect = ui.clip_rect();
767
768 let mut ui_builder = egui::UiBuilder::new();
769 if is_sizing_pass {
770 ui_builder = ui_builder.sizing_pass();
771 }
772 ui.scope_builder(ui_builder, |ui| {
773 let hovered_row_index_id = self.state_id.with("__table_hovered_row");
774 let hovered_row_index =
775 ui.data_mut(|data| data.remove_temp::<usize>(hovered_row_index_id));
776
777 let layout = StripLayout::new(ui, CellDirection::Horizontal, cell_layout, sense);
778
779 add_body_contents(TableBody {
780 layout,
781 columns: columns_ref,
782 widths: widths_ref,
783 max_used_widths: max_used_widths_ref,
784 striped,
785 row_index: 0,
786 y_range: clip_rect.y_range(),
787 scroll_to_row: scroll_to_row.map(|(r, _)| r),
788 scroll_to_y_range: &mut scroll_to_y_range,
789 hovered_row_index,
790 hovered_row_index_id,
791 });
792
793 if scroll_to_row.is_some() && scroll_to_y_range.is_none() {
794 scroll_to_y_range = Some(Rangef::new(f32::INFINITY, f32::INFINITY));
796 }
797 });
798
799 if let Some(y_range) = scroll_to_y_range {
800 let x = 0.0; let rect = egui::Rect::from_x_y_ranges(x..=x, y_range);
802 let align = scroll_to_row.and_then(|(_, a)| a);
803 ui.scroll_to_rect(rect, align);
804 }
805 });
806
807 let bottom = ui.min_rect().bottom();
808
809 let spacing_x = ui.spacing().item_spacing.x;
810 let mut x = cursor_position.x - spacing_x * 0.5;
811 for (i, column_width) in state.column_widths.iter_mut().enumerate() {
812 let column = &columns[i];
813 let column_is_resizable = column.resizable.unwrap_or(resizable);
814 let width_range = column.width_range;
815
816 let is_last_column = i + 1 == columns.len();
817 if is_last_column
818 && column.initial_width == InitialColumnSize::Remainder
819 && !ui.is_sizing_pass()
820 {
821 let eps = 0.1; *column_width = available_width - eps;
824 if !column.clip {
825 *column_width = column_width.at_least(max_used_widths[i]);
826 }
827 *column_width = width_range.clamp(*column_width);
828 break;
829 }
830
831 if ui.is_sizing_pass() {
832 if column.clip {
833 *column_width = column_width.min(max_used_widths[i]);
835 } else {
836 *column_width = max_used_widths[i];
837 }
838 } else if !column.clip {
839 *column_width = column_width.at_least(max_used_widths[i]);
842 }
843 *column_width = width_range.clamp(*column_width);
844
845 x += *column_width + spacing_x;
846
847 if column.is_auto() && (is_sizing_pass || !column_is_resizable) {
848 *column_width = width_range.clamp(max_used_widths[i]);
849 } else if column_is_resizable {
850 let column_resize_id = ui.id().with("resize_column").with(i);
851
852 let mut p0 = egui::pos2(x, table_top);
853 let mut p1 = egui::pos2(x, bottom);
854 let line_rect = egui::Rect::from_min_max(p0, p1)
855 .expand(ui.style().interaction.resize_grab_radius_side);
856
857 let resize_response =
858 ui.interact(line_rect, column_resize_id, egui::Sense::click_and_drag());
859
860 if column.auto_size_this_frame {
861 *column_width = width_range.clamp(max_used_widths[i]);
863 } else if resize_response.dragged() {
864 if let Some(pointer) = ui.ctx().pointer_latest_pos() {
865 let mut new_width = *column_width + pointer.x - x;
866 if !column.clip {
867 let max_shrinkage_per_frame = 8.0;
874 new_width =
875 new_width.at_least(max_used_widths[i] - max_shrinkage_per_frame);
876 }
877 new_width = width_range.clamp(new_width);
878
879 let x = x - *column_width + new_width;
880 (p0.x, p1.x) = (x, x);
881
882 *column_width = new_width;
883 }
884 }
885
886 let dragging_something_else =
887 ui.input(|i| i.pointer.any_down() || i.pointer.any_pressed());
888 let resize_hover = resize_response.hovered() && !dragging_something_else;
889
890 if resize_hover || resize_response.dragged() {
891 ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeColumn);
892 }
893
894 let stroke = if resize_response.dragged() {
895 ui.style().visuals.widgets.active.bg_stroke
896 } else if resize_hover {
897 ui.style().visuals.widgets.hovered.bg_stroke
898 } else {
899 ui.visuals().widgets.noninteractive.bg_stroke
901 };
902
903 ui.painter().line_segment([p0, p1], stroke);
904 }
905
906 available_width -= *column_width + spacing_x;
907 }
908
909 state.max_used_widths = max_used_widths;
910
911 state.store(ui, state_id);
912 scroll_area_out
913 }
914}
915
916pub struct TableBody<'a> {
920 layout: StripLayout<'a>,
921
922 columns: &'a [Column],
923
924 widths: &'a [f32],
926
927 max_used_widths: &'a mut [f32],
929
930 striped: bool,
931 row_index: usize,
932 y_range: Rangef,
933
934 scroll_to_row: Option<usize>,
936
937 scroll_to_y_range: &'a mut Option<Rangef>,
940
941 hovered_row_index: Option<usize>,
942
943 hovered_row_index_id: egui::Id,
945}
946
947impl<'a> TableBody<'a> {
948 pub fn ui_mut(&mut self) -> &mut egui::Ui {
952 self.layout.ui
953 }
954
955 pub fn max_rect(&self) -> Rect {
957 self.layout
958 .rect
959 .translate(egui::vec2(0.0, self.scroll_offset_y()))
960 }
961
962 fn scroll_offset_y(&self) -> f32 {
963 self.y_range.min - self.layout.rect.top()
964 }
965
966 pub fn widths(&self) -> &[f32] {
972 self.widths
973 }
974
975 pub fn row(&mut self, height: f32, add_row_content: impl FnOnce(TableRow<'a, '_>)) {
980 let mut response: Option<Response> = None;
981 let top_y = self.layout.cursor.y;
982 add_row_content(TableRow {
983 layout: &mut self.layout,
984 columns: self.columns,
985 widths: self.widths,
986 max_used_widths: self.max_used_widths,
987 row_index: self.row_index,
988 col_index: 0,
989 height,
990 striped: self.striped && self.row_index % 2 == 0,
991 hovered: self.hovered_row_index == Some(self.row_index),
992 selected: false,
993 response: &mut response,
994 });
995 self.capture_hover_state(&response, self.row_index);
996 let bottom_y = self.layout.cursor.y;
997
998 if Some(self.row_index) == self.scroll_to_row {
999 *self.scroll_to_y_range = Some(Rangef::new(top_y, bottom_y));
1000 }
1001
1002 self.row_index += 1;
1003 }
1004
1005 pub fn rows(
1030 mut self,
1031 row_height_sans_spacing: f32,
1032 total_rows: usize,
1033 mut add_row_content: impl FnMut(TableRow<'_, '_>),
1034 ) {
1035 let spacing = self.layout.ui.spacing().item_spacing;
1036 let row_height_with_spacing = row_height_sans_spacing + spacing.y;
1037
1038 if let Some(scroll_to_row) = self.scroll_to_row {
1039 let scroll_to_row = scroll_to_row.at_most(total_rows.saturating_sub(1)) as f32;
1040 *self.scroll_to_y_range = Some(Rangef::new(
1041 self.layout.cursor.y + scroll_to_row * row_height_with_spacing,
1042 self.layout.cursor.y + (scroll_to_row + 1.0) * row_height_with_spacing,
1043 ));
1044 }
1045
1046 let scroll_offset_y = self
1047 .scroll_offset_y()
1048 .min(total_rows as f32 * row_height_with_spacing);
1049 let max_height = self.y_range.span();
1050 let mut min_row = 0;
1051
1052 if scroll_offset_y > 0.0 {
1053 min_row = (scroll_offset_y / row_height_with_spacing).floor() as usize;
1054 self.add_buffer(min_row as f32 * row_height_with_spacing);
1055 }
1056
1057 let max_row =
1058 ((scroll_offset_y + max_height) / row_height_with_spacing).ceil() as usize + 1;
1059 let max_row = max_row.min(total_rows);
1060
1061 for row_index in min_row..max_row {
1062 let mut response: Option<Response> = None;
1063 add_row_content(TableRow {
1064 layout: &mut self.layout,
1065 columns: self.columns,
1066 widths: self.widths,
1067 max_used_widths: self.max_used_widths,
1068 row_index,
1069 col_index: 0,
1070 height: row_height_sans_spacing,
1071 striped: self.striped && (row_index + self.row_index) % 2 == 0,
1072 hovered: self.hovered_row_index == Some(row_index),
1073 selected: false,
1074 response: &mut response,
1075 });
1076 self.capture_hover_state(&response, row_index);
1077 }
1078
1079 if total_rows - max_row > 0 {
1080 let skip_height = (total_rows - max_row) as f32 * row_height_with_spacing;
1081 self.add_buffer(skip_height - spacing.y);
1082 }
1083 }
1084
1085 pub fn heterogeneous_rows(
1114 mut self,
1115 heights: impl Iterator<Item = f32>,
1116 mut add_row_content: impl FnMut(TableRow<'_, '_>),
1117 ) {
1118 let spacing = self.layout.ui.spacing().item_spacing;
1119 let mut enumerated_heights = heights.enumerate();
1120
1121 let max_height = self.y_range.span();
1122 let scroll_offset_y = self.scroll_offset_y() as f64;
1123
1124 let scroll_to_y_range_offset = self.layout.cursor.y as f64;
1125
1126 let mut cursor_y: f64 = 0.0;
1127
1128 for (row_index, row_height) in &mut enumerated_heights {
1130 let old_cursor_y = cursor_y;
1131 cursor_y += (row_height + spacing.y) as f64;
1132
1133 if Some(row_index) == self.scroll_to_row {
1134 *self.scroll_to_y_range = Some(Rangef::new(
1135 (scroll_to_y_range_offset + old_cursor_y) as f32,
1136 (scroll_to_y_range_offset + cursor_y) as f32,
1137 ));
1138 }
1139
1140 if cursor_y >= scroll_offset_y {
1141 self.add_buffer(old_cursor_y as f32); let mut response: Option<Response> = None;
1144 add_row_content(TableRow {
1145 layout: &mut self.layout,
1146 columns: self.columns,
1147 widths: self.widths,
1148 max_used_widths: self.max_used_widths,
1149 row_index,
1150 col_index: 0,
1151 height: row_height,
1152 striped: self.striped && (row_index + self.row_index) % 2 == 0,
1153 hovered: self.hovered_row_index == Some(row_index),
1154 selected: false,
1155 response: &mut response,
1156 });
1157 self.capture_hover_state(&response, row_index);
1158 break;
1159 }
1160 }
1161
1162 for (row_index, row_height) in &mut enumerated_heights {
1164 let top_y = cursor_y;
1165 let mut response: Option<Response> = None;
1166 add_row_content(TableRow {
1167 layout: &mut self.layout,
1168 columns: self.columns,
1169 widths: self.widths,
1170 max_used_widths: self.max_used_widths,
1171 row_index,
1172 col_index: 0,
1173 height: row_height,
1174 striped: self.striped && (row_index + self.row_index) % 2 == 0,
1175 hovered: self.hovered_row_index == Some(row_index),
1176 selected: false,
1177 response: &mut response,
1178 });
1179 self.capture_hover_state(&response, row_index);
1180 cursor_y += (row_height + spacing.y) as f64;
1181
1182 if Some(row_index) == self.scroll_to_row {
1183 *self.scroll_to_y_range = Some(Rangef::new(
1184 (scroll_to_y_range_offset + top_y) as f32,
1185 (scroll_to_y_range_offset + cursor_y) as f32,
1186 ));
1187 }
1188
1189 if cursor_y > scroll_offset_y + max_height as f64 {
1190 break;
1191 }
1192 }
1193
1194 let mut height_below_visible: f64 = 0.0;
1196 for (row_index, row_height) in enumerated_heights {
1197 height_below_visible += (row_height + spacing.y) as f64;
1198
1199 let top_y = cursor_y;
1200 cursor_y += (row_height + spacing.y) as f64;
1201 if Some(row_index) == self.scroll_to_row {
1202 *self.scroll_to_y_range = Some(Rangef::new(
1203 (scroll_to_y_range_offset + top_y) as f32,
1204 (scroll_to_y_range_offset + cursor_y) as f32,
1205 ));
1206 }
1207 }
1208
1209 if self.scroll_to_row.is_some() && self.scroll_to_y_range.is_none() {
1210 *self.scroll_to_y_range =
1212 Some(Rangef::point((scroll_to_y_range_offset + cursor_y) as f32));
1213 }
1214
1215 if height_below_visible > 0.0 {
1216 self.add_buffer(height_below_visible as f32);
1219 }
1220 }
1221
1222 fn add_buffer(&mut self, height: f32) {
1225 self.layout.skip_space(egui::vec2(0.0, height));
1226 }
1227
1228 fn capture_hover_state(&self, response: &Option<Response>, row_index: usize) {
1231 let is_row_hovered = response.as_ref().map_or(false, |r| r.hovered());
1232 if is_row_hovered {
1233 self.layout
1234 .ui
1235 .data_mut(|data| data.insert_temp(self.hovered_row_index_id, row_index));
1236 }
1237 }
1238}
1239
1240impl<'a> Drop for TableBody<'a> {
1241 fn drop(&mut self) {
1242 self.layout.allocate_rect();
1243 }
1244}
1245
1246pub struct TableRow<'a, 'b> {
1249 layout: &'b mut StripLayout<'a>,
1250 columns: &'b [Column],
1251 widths: &'b [f32],
1252
1253 max_used_widths: &'b mut [f32],
1255
1256 row_index: usize,
1257 col_index: usize,
1258 height: f32,
1259
1260 striped: bool,
1261 hovered: bool,
1262 selected: bool,
1263
1264 response: &'b mut Option<Response>,
1265}
1266
1267impl<'a, 'b> TableRow<'a, 'b> {
1268 #[cfg_attr(debug_assertions, track_caller)]
1272 pub fn col(&mut self, add_cell_contents: impl FnOnce(&mut Ui)) -> (Rect, Response) {
1273 let col_index = self.col_index;
1274
1275 let clip = self.columns.get(col_index).map_or(false, |c| c.clip);
1276 let auto_size_this_frame = self
1277 .columns
1278 .get(col_index)
1279 .map_or(false, |c| c.auto_size_this_frame);
1280
1281 let width = if let Some(width) = self.widths.get(col_index) {
1282 self.col_index += 1;
1283 *width
1284 } else {
1285 crate::log_or_panic!(
1286 "Added more `Table` columns than were pre-allocated ({} pre-allocated)",
1287 self.widths.len()
1288 );
1289 8.0 };
1291
1292 let width = CellSize::Absolute(width);
1293 let height = CellSize::Absolute(self.height);
1294
1295 let flags = StripLayoutFlags {
1296 clip,
1297 striped: self.striped,
1298 hovered: self.hovered,
1299 selected: self.selected,
1300 sizing_pass: auto_size_this_frame || self.layout.ui.is_sizing_pass(),
1301 };
1302
1303 let (used_rect, response) = self.layout.add(
1304 flags,
1305 width,
1306 height,
1307 egui::Id::new((self.row_index, col_index)),
1308 add_cell_contents,
1309 );
1310
1311 if let Some(max_w) = self.max_used_widths.get_mut(col_index) {
1312 *max_w = max_w.max(used_rect.width());
1313 }
1314
1315 *self.response = Some(
1316 self.response
1317 .as_ref()
1318 .map_or(response.clone(), |r| r.union(response.clone())),
1319 );
1320
1321 (used_rect, response)
1322 }
1323
1324 #[inline]
1326 pub fn set_selected(&mut self, selected: bool) {
1327 self.selected = selected;
1328 }
1329
1330 #[inline]
1332 pub fn set_hovered(&mut self, hovered: bool) {
1333 self.hovered = hovered;
1334 }
1335
1336 pub fn response(&self) -> Response {
1340 self.response
1341 .clone()
1342 .expect("Should only be called after `col`")
1343 }
1344
1345 #[inline]
1347 pub fn index(&self) -> usize {
1348 self.row_index
1349 }
1350
1351 #[inline]
1353 pub fn col_index(&self) -> usize {
1354 self.col_index
1355 }
1356}
1357
1358impl<'a, 'b> Drop for TableRow<'a, 'b> {
1359 #[inline]
1360 fn drop(&mut self) {
1361 self.layout.end_line();
1362 }
1363}