1use std::{
2 collections::{BTreeMap, btree_map::Entry},
3 ops::{Range, RangeInclusive},
4};
5
6use egui::{
7 Align, Context, Id, IdMap, IdSalt, Layout, NumExt as _, Rangef, Rect, Response, Ui, UiBuilder,
8 Vec2, Vec2b, vec2,
9};
10
11use super::{
12 SplitScroll, SplitScrollDelegate,
13 columns::{Column, ColumnFlags},
14};
15
16#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
18pub enum AutoSizeMode {
19 #[default]
21 Never,
22
23 Always,
25
26 OnParentResize,
28}
29
30#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
31pub struct TableState {
32 pub col_widths: IdMap<f32>,
34
35 pub parent_width: Option<f32>,
36}
37
38impl TableState {
39 #[must_use]
40 pub fn load(ctx: &egui::Context, id: Id) -> Option<Self> {
41 ctx.data_mut(|d| d.get_persisted(id))
42 }
43
44 pub fn store(self, ctx: &egui::Context, id: Id) {
45 ctx.data_mut(|d| d.insert_persisted(id, self));
46 }
47
48 #[must_use]
49 pub fn id(ui: &Ui, id_salt: IdSalt) -> Id {
50 ui.make_persistent_id(id_salt)
51 }
52
53 pub fn reset(ctx: &egui::Context, id: Id) {
54 ctx.data_mut(|d| {
55 d.remove::<Self>(id);
56 });
57 }
58}
59
60#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
64pub struct HeaderRow {
65 pub height: f32,
66
67 pub groups: Vec<Range<usize>>,
72}
73
74impl HeaderRow {
75 #[must_use]
76 pub const fn new(height: f32) -> Self {
77 Self {
78 height,
79 groups: Vec::new(),
80 }
81 }
82}
83
84pub struct Table {
103 columns: Vec<Column>,
105
106 id_salt: IdSalt,
111
112 num_sticky_cols: usize,
114
115 headers: Vec<HeaderRow>,
117
118 num_rows: u64,
120
121 auto_size_mode: AutoSizeMode,
123
124 scroll_to_columns: Option<(RangeInclusive<usize>, Option<Align>)>,
125 scroll_to_rows: Option<(RangeInclusive<u64>, Option<Align>)>,
126
127 stick_to_bottom: bool,
131 max_height: Option<f32>,
132 max_rows: Option<u64>,
133}
134
135impl Default for Table {
136 fn default() -> Self {
137 Self {
138 columns: vec![],
139 id_salt: IdSalt::new("table"),
140 num_sticky_cols: 0,
141 headers: vec![HeaderRow::new(16.0)],
142 num_rows: 0,
143 auto_size_mode: AutoSizeMode::default(),
144 scroll_to_columns: None,
145 scroll_to_rows: None,
146 stick_to_bottom: false,
147 max_height: None,
148 max_rows: None,
149 }
150 }
151}
152
153#[derive(Clone, Debug)]
154#[non_exhaustive]
155pub struct CellInfo {
156 pub col_nr: usize,
157
158 pub row_nr: u64,
159
160 pub table_id: Id,
162
163 pub row_hovered: bool,
165}
166
167#[derive(Clone, Debug)]
168#[non_exhaustive]
169pub struct HeaderCellInfo {
170 pub group_index: usize,
171
172 pub col_range: Range<usize>,
173
174 pub row_nr: usize,
176
177 pub table_id: Id,
179}
180
181#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
183#[non_exhaustive]
184pub struct PrefetchInfo {
185 pub num_sticky_columns: usize,
187
188 pub visible_columns: Range<usize>,
190
191 pub visible_rows: Range<u64>,
193
194 pub table_id: Id,
196}
197
198pub trait TableDelegate {
202 fn prepare(&mut self, _info: &PrefetchInfo) {}
206
207 fn header_cell_ui(&mut self, ui: &mut Ui, cell: &HeaderCellInfo);
211
212 fn row_ui(&mut self, _ui: &mut Ui, _row_nr: u64) {}
219
220 fn cell_ui(&mut self, ui: &mut Ui, cell: &CellInfo);
224
225 #[allow(clippy::cast_precision_loss)]
232 fn row_top_offset(&self, _ctx: &Context, _table_id: Id, row_nr: u64) -> f32 {
233 row_nr as f32 * self.default_row_height()
234 }
235
236 fn default_row_height(&self) -> f32 {
240 20.0
241 }
242
243 fn uniform_row_height(&self) -> Option<f32> {
244 Some(self.default_row_height())
245 }
246}
247
248impl Table {
249 #[must_use]
251 #[inline]
252 pub fn new() -> Self {
253 Self::default()
254 }
255
256 #[must_use]
261 #[inline]
262 pub fn id_salt(mut self, id_salt: impl egui::AsIdSalt) -> Self {
263 self.id_salt = IdSalt::new(id_salt);
264 self
265 }
266
267 #[must_use]
268 #[inline]
269 pub const fn max_rows(mut self, max_rows: u64) -> Self {
270 self.max_rows = Some(max_rows);
271 self
272 }
273
274 #[must_use]
276 #[inline]
277 pub const fn num_rows(mut self, num_rows: u64) -> Self {
278 self.num_rows = num_rows;
279 self
280 }
281
282 #[must_use]
284 #[inline]
285 pub fn columns(mut self, columns: impl Into<Vec<Column>>) -> Self {
286 self.columns = columns.into();
287 self
288 }
289
290 #[must_use]
294 #[inline]
295 pub const fn num_sticky_cols(mut self, num_sticky_cols: usize) -> Self {
296 self.num_sticky_cols = num_sticky_cols;
297 self
298 }
299
300 #[must_use]
302 #[inline]
303 pub fn headers(mut self, headers: impl Into<Vec<HeaderRow>>) -> Self {
304 self.headers = headers.into();
305 self
306 }
307
308 #[must_use]
310 #[inline]
311 pub const fn auto_size_mode(mut self, auto_size_mode: AutoSizeMode) -> Self {
312 self.auto_size_mode = auto_size_mode;
313 self
314 }
315
316 #[must_use]
317 #[inline]
318 pub const fn max_height(mut self, max_height: f32) -> Self {
319 self.max_height = Some(max_height);
320 self
321 }
322
323 #[must_use]
330 #[inline]
331 pub const fn stick_to_bottom(mut self, stick: bool) -> Self {
332 self.stick_to_bottom = stick;
333 self
334 }
335
336 #[must_use]
339 #[inline]
340 pub fn get_id(&self, ui: &Ui) -> Id {
341 TableState::id(ui, self.id_salt)
342 }
343
344 #[must_use]
352 #[inline]
353 pub const fn scroll_to_row(self, row: u64, align: Option<Align>) -> Self {
354 self.scroll_to_rows(row..=row, align)
355 }
356
357 #[must_use]
361 #[inline]
362 pub const fn scroll_to_rows(mut self, rows: RangeInclusive<u64>, align: Option<Align>) -> Self {
363 self.scroll_to_rows = Some((rows, align));
364 self
365 }
366
367 #[must_use]
375 #[inline]
376 pub const fn scroll_to_column(self, column: usize, align: Option<Align>) -> Self {
377 self.scroll_to_columns(column..=column, align)
378 }
379
380 #[must_use]
384 #[inline]
385 pub const fn scroll_to_columns(
386 mut self,
387 columns: RangeInclusive<usize>,
388 align: Option<Align>,
389 ) -> Self {
390 self.scroll_to_columns = Some((columns, align));
391 self
392 }
393
394 #[expect(clippy::unused_self)] fn get_row_top_offset(
399 &self,
400 ctx: &Context,
401 table_id: Id,
402 table_delegate: &dyn TableDelegate,
403 row_nr: u64,
404 ) -> f32 {
405 table_delegate.row_top_offset(ctx, table_id, row_nr)
406 }
407
408 fn get_row_nr_at_y_offset(
410 &self,
411 ctx: &Context,
412 table_id: Id,
413 table_delegate: &dyn TableDelegate,
414 y_offset: f32,
415 ) -> u64 {
416 if let Some(height) = table_delegate.uniform_row_height()
417 && height > 0.0
418 {
419 return ((y_offset / height) as u64).at_most(self.num_rows.saturating_sub(1));
420 }
421
422 partition_point(0..=self.num_rows, |row_nr| {
424 y_offset <= self.get_row_top_offset(ctx, table_id, table_delegate, row_nr)
425 })
426 .saturating_sub(1)
427 }
428
429 pub fn show(mut self, ui: &mut Ui, table_delegate: &mut dyn TableDelegate) -> Response {
430 self.num_sticky_cols = self.num_sticky_cols.at_most(self.columns.len());
431
432 let id = TableState::id(ui, self.id_salt);
433 let state = TableState::load(ui, id);
434 let is_new = state.is_none();
435 let mut state = state.unwrap_or_default();
436
437 for (i, column) in self.columns.iter_mut().enumerate() {
438 let column_id = column.id_for(i);
439 let cached_width = state.col_widths.get(&column_id).copied();
440 if let Some(existing_width) = cached_width {
441 column.current = existing_width;
442 } else {
443 if column.is_auto_fit() {
445 column.flags.set(ColumnFlags::AUTO_SIZE_THIS_FRAME, true);
446 }
447 }
448 column.current = column.range.clamp(column.current);
449
450 if is_new && column.is_auto_fit() {
452 column.flags.set(ColumnFlags::AUTO_SIZE_THIS_FRAME, true);
453 }
454 }
455
456 let do_full_sizing_pass = is_new
458 && self
459 .columns
460 .iter()
461 .any(super::columns::Column::is_auto_size_this_frame);
462
463 let parent_width = ui.available_width();
464 let auto_size = match self.auto_size_mode {
465 AutoSizeMode::Never => false,
466 AutoSizeMode::Always => true,
467 AutoSizeMode::OnParentResize => state.parent_width != Some(parent_width),
468 };
469 if auto_size {
470 Column::auto_size(&mut self.columns, parent_width);
471 }
472 state.parent_width = Some(parent_width);
473
474 let col_x = {
475 let mut x = ui.cursor().min.x;
476 let mut col_x = Vec::with_capacity(self.columns.len() + 1);
477 col_x.push(x);
478 for column in &self.columns {
479 x += column.current;
480 col_x.push(x);
481 }
482 col_x
483 };
484
485 let header_row_y = {
486 let mut y = ui.cursor().min.y;
487 let mut sticky_row_y = Vec::with_capacity(self.headers.len() + 1);
488 sticky_row_y.push(y);
489 for header in &self.headers {
490 y += header.height;
491 sticky_row_y.push(y);
492 }
493 sticky_row_y
494 };
495
496 let sticky_size = Vec2::new(
497 self.columns[..self.num_sticky_cols]
498 .iter()
499 .map(|c| c.current)
500 .sum(),
501 self.headers.iter().map(|h| h.height).sum(),
502 );
503
504 let mut ui_builder = UiBuilder::new().layout(Layout::top_down(Align::Min));
505 if do_full_sizing_pass {
506 ui_builder = ui_builder.sizing_pass().invisible();
507 ui.request_discard("Full egui_table sizing");
508 }
509 let response = ui
510 .scope_builder(ui_builder, |ui| {
511 ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
513
514 let num_columns = self.columns.len();
515
516 for (col_nr, column) in self.columns.iter_mut().enumerate() {
517 if column.is_resizable() {
518 let column_resize_id = id.with(column.id_for(col_nr)).with("resize");
519 if let Some(response) = ui.read_response(column_resize_id)
520 && response.double_clicked()
521 {
522 column.flags.set(ColumnFlags::AUTO_SIZE_THIS_FRAME, true);
523 }
524 }
525 if column.is_auto_size_this_frame() {
526 ui.request_discard("egui_table column sizing");
527 }
528 }
529
530 SplitScroll {
531 scroll_enabled: Vec2b::new(true, true),
532 fixed_size: sticky_size,
533 scroll_outer_size: {
534 let total_rows_height =
536 self.get_row_top_offset(ui, id, table_delegate, self.num_rows);
537 let total_content_height = sticky_size.y + total_rows_height;
538
539 let min_rows = self.num_rows.min(10);
542 let min_rows_height =
543 self.get_row_top_offset(ui, id, table_delegate, min_rows);
544 let min_table_height = sticky_size.y + min_rows_height;
545
546 let max_height_limit = if let Some(max_r) = self.max_rows {
548 let max_rows_height =
549 self.get_row_top_offset(ui, id, table_delegate, max_r);
550 sticky_size.y + max_rows_height
551 } else {
552 self.max_height.unwrap_or_else(|| ui.clip_rect().height())
553 };
554
555 let available_height = ui
556 .available_height()
557 .at_most(max_height_limit)
558 .max(min_table_height);
559
560 let allocated_height = total_content_height.min(available_height);
561
562 Vec2::new(
563 (ui.available_width() - sticky_size.x).max(0.0),
564 (allocated_height - sticky_size.y).max(0.0),
565 )
566 },
567 scroll_content_size: Vec2::new(
568 self.columns[self.num_sticky_cols..]
569 .iter()
570 .map(|c| c.current)
571 .sum(),
572 self.get_row_top_offset(ui, id, table_delegate, self.num_rows),
573 ),
574 stick_to_bottom: self.stick_to_bottom,
575 }
576 .show(
577 ui,
578 &mut TableSplitScrollDelegate {
579 id,
580 table_delegate,
581 state: &mut state,
582 table: &mut self,
583 col_x,
584 header_row_y,
585 max_column_widths: vec![0.0; num_columns],
586 visible_column_lines: BTreeMap::default(),
587 do_full_sizing_pass,
588 has_prefetched: false,
589 egui_ctx: ui.clone(),
590 col_interaction: BTreeMap::default(),
591 dragging_col: None,
592 },
593 );
594 })
595 .response;
596
597 state.store(ui, id);
598 response
599 }
600}
601
602#[derive(Clone, Copy, Debug)]
603struct ColumnResizer {
604 scroll_offset: Vec2,
605
606 top: f32,
607}
608
609fn update(map: &mut BTreeMap<usize, ColumnResizer>, key: usize, value: ColumnResizer) {
610 match map.entry(key) {
611 Entry::Vacant(entry) => {
612 entry.insert(value);
613 }
614 Entry::Occupied(mut entry) => {
615 entry.get_mut().top = entry.get_mut().top.min(value.top);
616 }
617 }
618}
619
620struct TableSplitScrollDelegate<'a> {
621 id: Id,
622 table_delegate: &'a mut dyn TableDelegate,
623 table: &'a mut Table,
624 state: &'a mut TableState,
625
626 col_x: Vec<f32>,
628
629 header_row_y: Vec<f32>,
631
632 max_column_widths: Vec<f32>,
634
635 visible_column_lines: BTreeMap<usize, ColumnResizer>,
637
638 do_full_sizing_pass: bool,
639
640 has_prefetched: bool,
641
642 egui_ctx: Context,
643
644 col_interaction: BTreeMap<usize, (bool, bool)>,
645 dragging_col: Option<usize>,
646}
647
648impl TableSplitScrollDelegate<'_> {
649 fn get_row_top_offset(&self, row_nr: u64) -> f32 {
651 self.table
652 .get_row_top_offset(&self.egui_ctx, self.id, self.table_delegate, row_nr)
653 }
654
655 fn get_row_nr_at_y_offset(&self, y_offset: f32) -> u64 {
657 self.table
658 .get_row_nr_at_y_offset(&self.egui_ctx, self.id, self.table_delegate, y_offset)
659 }
660
661 fn header_ui(&mut self, ui: &mut Ui, scroll_offset: Vec2) {
662 let viewport = ui.clip_rect().translate(scroll_offset);
664
665 #[allow(clippy::float_cmp)]
666 let col_range = if self.table.columns.is_empty() || viewport.left() == viewport.right() {
667 0..0
668 } else if self.do_full_sizing_pass {
669 0..self.table.columns.len()
671 } else {
672 let col_idx_at = |x: f32| -> usize {
673 self.col_x
674 .partition_point(|&col_x| col_x < x)
675 .saturating_sub(1)
676 .at_most(self.table.columns.len() - 1)
677 };
678
679 col_idx_at(viewport.min.x)..col_idx_at(viewport.max.x) + 1
680 };
681
682 let last_header_row_y = self.header_row_y.last().copied().unwrap_or(0.0);
683
684 for (row_nr, header_row) in self.table.headers.iter().enumerate() {
685 let groups = if header_row.groups.is_empty() {
686 (0..self.table.columns.len()).map(|i| i..i + 1).collect()
687 } else {
688 header_row.groups.clone()
689 };
690
691 let y_range = Rangef::new(self.header_row_y[row_nr], self.header_row_y[row_nr + 1]);
692
693 for (group_index, col_range_group) in groups.into_iter().enumerate() {
694 let start = col_range_group.start;
695 let end = col_range_group.end;
696
697 if end <= col_range.start || start >= col_range.end {
699 continue;
700 }
701
702 let mut header_rect =
703 Rect::from_x_y_ranges(self.col_x[start]..=self.col_x[end], y_range)
704 .translate(-scroll_offset);
705
706 if 0 < start
707 && self.table.columns[start - 1].is_resizable()
708 && ui.clip_rect().x_range().contains(header_rect.left())
709 {
710 update(
712 &mut self.visible_column_lines,
713 start - 1,
714 ColumnResizer {
715 scroll_offset,
716 top: header_rect.top(),
717 },
718 );
719 }
720
721 let clip_rect = header_rect;
722
723 let last_column = &self.table.columns[end - 1];
724 let auto_size_this_frame = last_column.is_auto_size_this_frame();
725
726 if auto_size_this_frame {
727 header_rect.max.x = header_rect.min.x
728 + self.table.columns[start..end]
729 .iter()
730 .map(|column| column.range.min)
731 .sum::<f32>();
732 }
733
734 let mut ui_builder = UiBuilder::new()
735 .max_rect(header_rect)
736 .id_salt(("header", row_nr, group_index))
737 .layout(egui::Layout::left_to_right(egui::Align::Center));
738 if auto_size_this_frame {
739 ui_builder = ui_builder.sizing_pass();
740 }
741 let mut cell_ui = ui.new_child(ui_builder);
742 cell_ui.shrink_clip_rect(clip_rect);
743
744 self.table_delegate.header_cell_ui(
745 &mut cell_ui,
746 &HeaderCellInfo {
747 group_index,
748 col_range: col_range_group,
749 row_nr,
750 table_id: self.id,
751 },
752 );
753
754 if start + 1 == end {
755 let col_nr = start;
757 let column = &self.table.columns[start];
758 let width = &mut self.max_column_widths[col_nr];
759 *width = width.max(cell_ui.min_size().x);
760
761 if column.is_resizable()
763 && ui.clip_rect().x_range().contains(header_rect.right())
764 {
765 update(
766 &mut self.visible_column_lines,
767 col_nr,
768 ColumnResizer {
769 scroll_offset,
770 top: header_rect.top(),
771 },
772 );
773 }
774 }
775 }
776 }
777
778 for (col_nr, ColumnResizer { scroll_offset, top }) in &self.visible_column_lines {
780 let col_nr = *col_nr;
781 let Some(column) = self.table.columns.get(col_nr) else {
782 continue;
783 };
784 if !column.is_resizable() {
785 continue;
786 }
787
788 let column_id = column.id_for(col_nr);
789 let new_width = self
790 .state
791 .col_widths
792 .get(&column_id)
793 .copied()
794 .unwrap_or(column.current);
795 let old_width = column.current;
796
797 let x = self.col_x[col_nr + 1] - scroll_offset.x + (new_width - old_width);
798 let yrange = Rangef::new(*top, last_header_row_y);
799
800 let (hovered, dragged) = self
801 .col_interaction
802 .get(&col_nr)
803 .copied()
804 .unwrap_or((false, false));
805 let stroke = if dragged {
806 ui.style().visuals.widgets.active.bg_stroke
807 } else if hovered {
808 ui.style().visuals.widgets.hovered.bg_stroke
809 } else {
810 ui.visuals().widgets.noninteractive.bg_stroke
811 };
812
813 ui.painter().vline(x, yrange, stroke);
814 }
815 }
816
817 fn region_ui(&mut self, ui: &mut Ui, scroll_offset: Vec2, do_prefetch: bool) {
818 let viewport = ui.clip_rect().translate(scroll_offset);
820 let last_header_row_y = self.header_row_y.last().copied().unwrap_or(0.0);
821
822 #[allow(clippy::float_cmp)]
823 let col_range = if self.table.columns.is_empty() || viewport.left() == viewport.right() {
824 0..0
825 } else if self.do_full_sizing_pass {
826 0..self.table.columns.len()
828 } else {
829 let col_idx_at = |x: f32| -> usize {
831 self.col_x
832 .partition_point(|&col_x| col_x < x)
833 .saturating_sub(1)
834 .at_most(self.table.columns.len() - 1)
835 };
836
837 col_idx_at(viewport.min.x)..col_idx_at(viewport.max.x) + 1
838 };
839
840 #[allow(clippy::float_cmp)]
841 let row_range = if self.table.num_rows == 0 || viewport.top() == viewport.bottom() {
842 0..0
843 } else {
844 let row_idx_at = |y: f32| -> u64 {
846 let row_nr = self.get_row_nr_at_y_offset(y - last_header_row_y);
847 row_nr.at_most(self.table.num_rows.saturating_sub(1))
848 };
849
850 let margin = if do_prefetch {
851 1.0 } else {
853 0.0
854 };
855
856 row_idx_at(viewport.min.y - margin)..row_idx_at(viewport.max.y + margin) + 1
857 };
858
859 if do_prefetch {
860 self.table_delegate.prepare(&PrefetchInfo {
861 num_sticky_columns: self.table.num_sticky_cols,
862 visible_columns: col_range.clone(),
863 visible_rows: row_range.clone(),
864 table_id: self.id,
865 });
866 self.has_prefetched = true;
867 } else {
868 debug_assert!(
869 self.has_prefetched,
870 "SplitScroll delegate methods called in unexpected order"
871 );
872 }
873
874 let pointer_pos = ui.ctx().pointer_latest_pos();
875 let current_frame = ui.ctx().cumulative_frame_nr();
876 let hovered_row_id = self.id.with("hovered_row");
877
878 for row_nr in row_range {
879 let y_range = Rangef::new(
880 last_header_row_y + self.get_row_top_offset(row_nr),
881 last_header_row_y + self.get_row_top_offset(row_nr + 1),
882 );
883
884 let row_x_range = self.col_x[0]..=self.col_x[self.col_x.len() - 1];
885 let row_rect = Rect::from_x_y_ranges(row_x_range, y_range).translate(-scroll_offset);
886
887 if let Some(pos) = pointer_pos {
889 let visible_row_rect = row_rect.intersect(ui.clip_rect());
890
891 let contains_exclusive = visible_row_rect.min.x <= pos.x
893 && pos.x < visible_row_rect.max.x
894 && visible_row_rect.min.y <= pos.y
895 && pos.y < visible_row_rect.max.y;
896
897 if contains_exclusive {
898 ui.ctx()
899 .data_mut(|d| d.insert_temp(hovered_row_id, (current_frame, row_nr)));
900 }
901 }
902
903 let row_hovered = if let Some((frame, hovered_row)) =
905 ui.ctx().data(|d| d.get_temp::<(u64, u64)>(hovered_row_id))
906 {
907 hovered_row == row_nr
908 && (frame == current_frame || frame == current_frame.saturating_sub(1))
909 } else {
910 false
911 };
912
913 let mut row_ui = ui.new_child(
914 UiBuilder::new()
915 .max_rect(row_rect)
916 .id_salt(("row", row_nr))
917 .layout(egui::Layout::left_to_right(egui::Align::Center)),
918 );
919 row_ui.set_min_size(row_rect.size());
920
921 self.table_delegate.row_ui(&mut row_ui, row_nr);
922
923 for col_nr in col_range.clone() {
924 let column = &self.table.columns[col_nr];
925 let mut cell_rect =
926 Rect::from_x_y_ranges(self.col_x[col_nr]..=self.col_x[col_nr + 1], y_range)
927 .translate(-scroll_offset);
928 let clip_rect = cell_rect;
929 let auto_size_this_frame = column.is_auto_size_this_frame();
930 if auto_size_this_frame {
931 cell_rect.max.x = cell_rect.min.x + column.range.min;
932 }
933
934 let mut ui_builder = UiBuilder::new()
935 .max_rect(cell_rect)
936 .id_salt((row_nr, col_nr))
937 .layout(egui::Layout::left_to_right(egui::Align::Center));
938 if auto_size_this_frame {
939 ui_builder = ui_builder.sizing_pass();
940 }
941 let mut cell_ui = row_ui.new_child(ui_builder);
942 cell_ui.shrink_clip_rect(clip_rect);
943
944 self.table_delegate.cell_ui(
945 &mut cell_ui,
946 &CellInfo {
947 col_nr,
948 row_nr,
949 table_id: self.id,
950 row_hovered,
951 },
952 );
953
954 let width = &mut self.max_column_widths[col_nr];
955 *width = width.max(cell_ui.min_size().x);
956 }
957 }
958
959 for col_nr in col_range {
961 let column = &self.table.columns[col_nr];
962 if column.is_resizable() {
963 update(
964 &mut self.visible_column_lines,
965 col_nr,
966 ColumnResizer {
967 scroll_offset,
968 top: last_header_row_y,
969 },
970 );
971 }
972 }
973 }
974}
975
976impl SplitScrollDelegate for TableSplitScrollDelegate<'_> {
977 fn right_bottom_ui(&mut self, ui: &mut Ui, scroll_offset: Vec2) {
979 if self.table.scroll_to_columns.is_some() || self.table.scroll_to_rows.is_some() {
980 let mut target_rect = ui.clip_rect(); let mut target_align = None;
982
983 if let Some((column_range, align)) = &self.table.scroll_to_columns {
984 let scrollable_col_x_base = self.col_x[self.table.num_sticky_cols];
988 let x_from_column_nr = |col_nr: usize| -> f32 {
989 ui.min_rect().left() + (self.col_x[col_nr] - scrollable_col_x_base)
990 };
991
992 let sticky_width = scrollable_col_x_base - self.col_x[0];
993
994 target_rect.min.x = x_from_column_nr(*column_range.start()) - sticky_width;
998 target_rect.max.x = x_from_column_nr(*column_range.end() + 1);
999 target_align = target_align.or(*align);
1000 }
1001
1002 if let Some((row_range, align)) = &self.table.scroll_to_rows {
1003 let y_from_row_nr =
1004 |row_nr: u64| -> f32 { ui.min_rect().top() + self.get_row_top_offset(row_nr) };
1005
1006 let last_header_row_y = self.header_row_y.last().copied().unwrap_or(0.0);
1007 let sticky_height = last_header_row_y - self.header_row_y[0];
1008
1009 target_rect.min.y = y_from_row_nr(*row_range.start()) - sticky_height;
1013 target_rect.max.y = y_from_row_nr(*row_range.end() + 1);
1014 target_align = target_align.or(*align);
1015 }
1016
1017 ui.scroll_to_rect(target_rect, target_align);
1018 }
1019
1020 self.region_ui(ui, scroll_offset, true);
1021 }
1022
1023 fn left_top_ui(&mut self, ui: &mut Ui) {
1024 self.header_ui(ui, Vec2::ZERO);
1025 }
1026
1027 fn right_top_ui(&mut self, ui: &mut Ui, scroll_offset: Vec2) {
1028 let horizontal_scroll_offset = vec2(scroll_offset.x, 0.0);
1029 self.header_ui(ui, horizontal_scroll_offset);
1030 }
1031
1032 fn left_bottom_ui(&mut self, ui: &mut Ui, scroll_offset: Vec2) {
1033 let vertical_scroll_offset = vec2(0.0, scroll_offset.y);
1034 self.region_ui(ui, vertical_scroll_offset, false);
1035 }
1036
1037 fn paint_overlays(&mut self, ui: &mut Ui) {
1038 let total_rows_height = self.get_row_top_offset(self.table.num_rows);
1039 let header_top = self.header_row_y[0];
1040 let header_bottom = self.header_row_y.last().copied().unwrap_or(0.0);
1041 let clip_bottom = ui.clip_rect().bottom();
1042
1043 for (
1046 col_nr,
1047 ColumnResizer {
1048 scroll_offset,
1049 top: _,
1050 },
1051 ) in &self.visible_column_lines
1052 {
1053 let col_nr = *col_nr;
1054 if self.col_interaction.contains_key(&col_nr) {
1055 continue; }
1057
1058 let Some(column) = self.table.columns.get(col_nr) else {
1059 continue;
1060 };
1061 if !column.is_resizable() {
1062 continue;
1063 }
1064
1065 let column_id = column.id_for(col_nr);
1066 let range = column.range;
1067 let current = column.current;
1068 let column_width = self
1069 .state
1070 .col_widths
1071 .get(&column_id)
1072 .copied()
1073 .unwrap_or(current);
1074
1075 let x = self.col_x[col_nr + 1] - scroll_offset.x + (column_width - current);
1076 let content_bottom = header_bottom + total_rows_height - scroll_offset.y;
1077 let line_bottom = clip_bottom.min(content_bottom);
1078
1079 let line_rect = egui::Rect::from_x_y_ranges(x..=x, header_top..=line_bottom)
1081 .expand(ui.style().interaction.resize_grab_radius_side);
1082
1083 let column_resize_id = self.id.with(column_id).with("resize");
1084 let resize_response =
1085 ui.interact(line_rect, column_resize_id, egui::Sense::click_and_drag());
1086
1087 let hovered = resize_response.hovered();
1088 let dragged = resize_response.dragged();
1089
1090 if dragged && let Some(pointer) = ui.pointer_latest_pos() {
1091 let new_width = column_width + pointer.x - x;
1092 let clamped_width = range.clamp(new_width);
1093 self.state.col_widths.insert(column_id, clamped_width);
1094 self.dragging_col = Some(col_nr);
1095 }
1096
1097 self.col_interaction.insert(col_nr, (hovered, dragged));
1098 }
1099
1100 for (col_nr, ColumnResizer { scroll_offset, top }) in &self.visible_column_lines {
1102 let col_nr = *col_nr;
1103 let Some(column) = self.table.columns.get(col_nr) else {
1104 continue;
1105 };
1106 if !column.is_resizable() {
1107 continue;
1108 }
1109
1110 let column_id = column.id_for(col_nr);
1111 let current = column.current;
1112 let column_width = self
1113 .state
1114 .col_widths
1115 .get(&column_id)
1116 .copied()
1117 .unwrap_or(current);
1118 let x = self.col_x[col_nr + 1] - scroll_offset.x + (column_width - current);
1119
1120 let content_bottom = header_bottom + total_rows_height - scroll_offset.y;
1121 let line_bottom = clip_bottom.min(content_bottom);
1122 let yrange = Rangef::new(*top, line_bottom);
1123
1124 let (hovered, dragged) = self
1125 .col_interaction
1126 .get(&col_nr)
1127 .copied()
1128 .unwrap_or((false, false));
1129
1130 if hovered || dragged {
1131 ui.set_cursor_icon(egui::CursorIcon::ResizeColumn);
1132 }
1133
1134 let stroke = if dragged {
1135 ui.style().visuals.widgets.active.bg_stroke
1136 } else if hovered {
1137 ui.style().visuals.widgets.hovered.bg_stroke
1138 } else {
1139 ui.visuals().widgets.noninteractive.bg_stroke
1140 };
1141
1142 ui.painter().vline(x, yrange, stroke);
1143 }
1144 }
1145
1146 fn update_col_widths(&mut self, ui: &mut Ui) {
1147 for col_nr in 0..self.table.columns.len() {
1148 if self.dragging_col == Some(col_nr) {
1150 continue;
1151 }
1152
1153 let column = self.table.columns.get(col_nr);
1154 let Some(column) = column else {
1155 continue;
1156 };
1157 if !column.is_resizable() {
1158 continue;
1159 }
1160
1161 let column_id = column.id_for(col_nr);
1162 let used_width = column.range.clamp(self.max_column_widths[col_nr]);
1163 let old_width = self
1164 .state
1165 .col_widths
1166 .get(&column_id)
1167 .copied()
1168 .unwrap_or(column.current);
1169
1170 let auto_size_this_frame = column.is_auto_size_this_frame();
1172 let auto_fit = column.is_auto_fit();
1173
1174 if auto_size_this_frame {
1175 self.table.columns[col_nr]
1176 .flags
1177 .set(ColumnFlags::AUTO_SIZE_THIS_FRAME, false);
1178 }
1179
1180 let mut new_width = old_width;
1181 if auto_size_this_frame || (ui.is_sizing_pass() && auto_fit) {
1182 new_width = used_width;
1183 } else if auto_fit {
1184 new_width = old_width.max(used_width);
1185 }
1186
1187 self.state.col_widths.insert(column_id, new_width);
1188 }
1189
1190 self.col_interaction.clear();
1192
1193 if !ui.input(|i| i.pointer.primary_down()) {
1195 self.dragging_col = None;
1196 }
1197 }
1198}
1199
1200fn partition_point(range: RangeInclusive<u64>, second_partition: impl Fn(u64) -> bool) -> u64 {
1202 let mut min = *range.start();
1203 let mut max = *range.end();
1204
1205 debug_assert!(min < max, "Bad call to partition_point");
1206
1207 while min < max {
1208 let mid = min + (max - min) / 2;
1209
1210 if second_partition(mid) {
1211 max = mid;
1212 } else {
1213 min = mid + 1;
1214 }
1215 }
1216
1217 min
1218}
1219
1220#[cfg(test)]
1221mod tests {
1222 use super::partition_point;
1223
1224 #[test]
1225 fn test_partition_point() {
1226 assert_eq!(partition_point(0..=17, |i| 8 <= i), 8);
1227 assert_eq!(partition_point(0..=17, |i| 9 <= i), 9);
1228 assert_eq!(partition_point(10..=17, |_| true), 10);
1229 assert_eq!(partition_point(10..=17, |_| false), 17);
1230 }
1231}