Skip to main content

egui_table/
table.rs

1use std::{
2    collections::{BTreeMap, btree_map::Entry},
3    ops::{Range, RangeInclusive},
4};
5
6use egui::{
7    Align, Context, Id, IdMap, Layout, NumExt as _, Rangef, Rect, Response, Ui, UiBuilder, Vec2,
8    Vec2b, vec2,
9};
10use vec1::Vec1;
11
12use crate::{SplitScroll, SplitScrollDelegate, columns::Column};
13
14// TODO: fix the functionality of this
15#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
16pub enum AutoSizeMode {
17    /// Never auto-size the columns.
18    #[default]
19    Never,
20
21    /// Always auto-size the columns
22    Always,
23
24    /// Auto-size the columns if the parents' width changes
25    OnParentResize,
26}
27
28#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
29pub struct TableState {
30    // Maps columns ids to their widths.
31    pub col_widths: IdMap<f32>,
32
33    pub parent_width: Option<f32>,
34}
35
36impl TableState {
37    pub fn load(ctx: &egui::Context, id: Id) -> Option<Self> {
38        ctx.data_mut(|d| d.get_persisted(id))
39    }
40
41    pub fn store(self, ctx: &egui::Context, id: Id) {
42        ctx.data_mut(|d| d.insert_persisted(id, self));
43    }
44
45    pub fn id(ui: &Ui, id_salt: Id) -> Id {
46        ui.make_persistent_id(id_salt)
47    }
48
49    pub fn reset(ctx: &egui::Context, id: Id) {
50        ctx.data_mut(|d| {
51            d.remove::<Self>(id);
52        });
53    }
54}
55
56/// Describes one of potentially many header rows.
57///
58/// Each header row has a fixed height.
59#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
60pub struct HeaderRow {
61    pub height: f32,
62
63    /// If empty, it is ignored.
64    ///
65    /// Contains non-overlapping ranges of column indices to group together.
66    /// For instance: `vec![(0..3), (3..5), (5..6)]`.
67    pub groups: Vec<Range<usize>>,
68}
69
70impl HeaderRow {
71    pub fn new(height: f32) -> Self {
72        Self {
73            height,
74            groups: Default::default(),
75        }
76    }
77}
78
79/// A table viewer.
80///
81/// Designed to be fast when there are millions of rows, but only hundreds of columns.
82///
83/// ## Sticky columns and rows
84/// You can designate a certain number of column and rows as being "sticky".
85/// These won't scroll with the rest of the table.
86///
87/// The sticky rows are always the first ones at the top, and are usually used for the column headers.
88/// The sticky columns are always the first ones on the left, useful for special columns like
89/// table row number or similar.
90/// A sticky column is sometimes called a "gutter".
91///
92/// ## Batteries not included
93/// * You need to specify the `Table` size beforehand
94/// * Does not add any margins to cells. Add it yourself with [`egui::Frame`].
95/// * Does not wrap cells in scroll areas. Do that yourself.
96/// * Doesn't paint any guide-lines for the rows. Paint them yourself.
97pub struct Table {
98    /// The columns of the table.
99    columns: Vec<Column>,
100
101    /// Salt added to the parent [`Ui::id`] to produce an [`Id`] that is unique
102    /// within the parent [`Ui`].
103    ///
104    /// You need to set this to something unique if you have multiple tables in the same ui.
105    id_salt: Id,
106
107    /// Which columns are sticky (non-scrolling)?
108    num_sticky_cols: usize,
109
110    /// The count and parameters of the sticky (non-scrolling) header rows.
111    headers: Vec<HeaderRow>,
112
113    /// Total number of rows (sticky + non-sticky).
114    num_rows: u64,
115
116    /// How to do auto-sizing of columns, if at all.
117    auto_size_mode: AutoSizeMode,
118
119    scroll_to_columns: Option<(RangeInclusive<usize>, Option<Align>)>,
120    scroll_to_rows: Option<(RangeInclusive<u64>, Option<Align>)>,
121
122    /// If true, the vertical scrollbar will stick to the bottom as the content grows.
123    ///
124    /// Useful for log views or terminal emulation.
125    stick_to_bottom: bool,
126}
127
128impl Default for Table {
129    fn default() -> Self {
130        Self {
131            columns: vec![],
132            id_salt: Id::new("table"),
133            num_sticky_cols: 0,
134            headers: vec![HeaderRow::new(16.0)],
135            num_rows: 0,
136            auto_size_mode: AutoSizeMode::default(),
137            scroll_to_columns: None,
138            scroll_to_rows: None,
139            stick_to_bottom: false,
140        }
141    }
142}
143
144#[derive(Clone, Debug)]
145#[non_exhaustive]
146pub struct CellInfo {
147    pub col_nr: usize,
148
149    pub row_nr: u64,
150
151    /// The unique [`Id`] of this table.
152    pub table_id: Id,
153    // We could add more stuff here, like a reference to the column
154}
155
156#[derive(Clone, Debug)]
157#[non_exhaustive]
158pub struct HeaderCellInfo {
159    pub group_index: usize,
160
161    pub col_range: Range<usize>,
162
163    /// Header row
164    pub row_nr: usize,
165
166    /// The unique [`Id`] of this table.
167    pub table_id: Id,
168}
169
170/// Data given to the delegate containing information about what is about to be rendered.
171#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
172#[non_exhaustive]
173pub struct PrefetchInfo {
174    /// The sticky columns are always visible.
175    pub num_sticky_columns: usize,
176
177    /// This range of columns are currently visible, in addition to the sticky ones.
178    pub visible_columns: Range<usize>,
179
180    /// These rows are currently visible.
181    pub visible_rows: Range<u64>,
182
183    /// The unique [`Id`] of this table.
184    pub table_id: Id,
185}
186
187/// The interface that the user needs to implement to display a table.
188///
189/// The [`Table`] calls functions on the delegate to render the table.
190pub trait TableDelegate {
191    /// Called before any call to [`Self::cell_ui`] to communicate the range of visible columns and rows.
192    ///
193    /// You can use this to only load the data required to be viewed.
194    fn prepare(&mut self, _info: &PrefetchInfo) {}
195
196    /// The contents of a header cell in the table.
197    ///
198    /// The [`CellInfo::row_nr`] is which header row (usually 0).
199    fn header_cell_ui(&mut self, ui: &mut Ui, cell: &HeaderCellInfo);
200
201    /// The contents of a row.
202    ///
203    /// Individual cell [`Ui`]s will be children of the ui passed to this fn, so you can e.g. use
204    /// [`Ui::style_mut`] to style the whole row.
205    ///
206    /// This might be called multiple times per row (e.g. for sticky and non-sticky columns).
207    fn row_ui(&mut self, _ui: &mut Ui, _row_nr: u64) {}
208
209    /// The contents of a cell in the table.
210    ///
211    /// The [`CellInfo::row_nr`] is ignoring header rows.
212    fn cell_ui(&mut self, ui: &mut Ui, cell: &CellInfo);
213
214    /// Compute the offset for the top of the given row.
215    ///
216    /// Implement this for arbitrary row heights. The default implementation uses
217    /// [`Self::default_row_height`].
218    ///
219    /// Note: must always return 0.0 for `row_nr = 0`.
220    fn row_top_offset(&self, _ctx: &Context, _table_id: Id, row_nr: u64) -> f32 {
221        row_nr as f32 * self.default_row_height()
222    }
223
224    /// Default row height.
225    ///
226    /// This is used by the default implementation of [`Self::row_top_offset`].
227    fn default_row_height(&self) -> f32 {
228        20.0
229    }
230}
231
232impl Table {
233    /// Create a new table, with no columns and no headers, and zero rows.
234    #[inline]
235    pub fn new() -> Self {
236        Self::default()
237    }
238
239    /// Salt added to the parent [`Ui::id`] to produce an [`Id`] that is unique
240    /// within the parent [`Ui`].
241    ///
242    /// You need to set this to something unique if you have multiple tables in the same ui.
243    #[inline]
244    pub fn id_salt(mut self, id_salt: impl std::hash::Hash) -> Self {
245        self.id_salt = Id::new(id_salt);
246        self
247    }
248
249    /// Total number of rows (sticky + non-sticky).
250    #[inline]
251    pub fn num_rows(mut self, num_rows: u64) -> Self {
252        self.num_rows = num_rows;
253        self
254    }
255
256    /// The columns of the table.
257    #[inline]
258    pub fn columns(mut self, columns: impl Into<Vec<Column>>) -> Self {
259        self.columns = columns.into();
260        self
261    }
262
263    /// How many columns are sticky (non-scrolling)?
264    ///
265    /// Default is 0.
266    #[inline]
267    pub fn num_sticky_cols(mut self, num_sticky_cols: usize) -> Self {
268        self.num_sticky_cols = num_sticky_cols;
269        self
270    }
271
272    /// The count and parameters of the sticky (non-scrolling) header rows.
273    #[inline]
274    pub fn headers(mut self, headers: impl Into<Vec<HeaderRow>>) -> Self {
275        self.headers = headers.into();
276        self
277    }
278
279    /// How to do auto-sizing of columns, if at all.
280    #[inline]
281    pub fn auto_size_mode(mut self, auto_size_mode: AutoSizeMode) -> Self {
282        self.auto_size_mode = auto_size_mode;
283        self
284    }
285
286    /// The scroll handle will stick to the bottom position even while the content size
287    /// changes dynamically.
288    ///
289    /// This can be useful to simulate terminal UIs or log/info scrollers.
290    /// The scroll handle remains stuck until user manually changes position. Once "unstuck"
291    /// it will remain focused on whatever content viewport the user left it on.
292    #[inline]
293    pub fn stick_to_bottom(mut self, stick: bool) -> Self {
294        self.stick_to_bottom = stick;
295        self
296    }
297
298    /// Read the globally unique id, based on the current [`Self::id_salt`]
299    /// and the parent id.
300    #[inline]
301    pub fn get_id(&self, ui: &Ui) -> Id {
302        TableState::id(ui, self.id_salt)
303    }
304
305    /// Set a row to scroll to.
306    ///
307    /// `align` specifies if the row should be positioned in the top, center, or bottom of the view
308    /// (using [`Align::TOP`], [`Align::Center`] or [`Align::BOTTOM`]).
309    /// If `align` is `None`, the table will scroll just enough to bring the cursor into view.
310    ///
311    /// See also: [`Self::scroll_to_column`].
312    #[inline]
313    pub fn scroll_to_row(self, row: u64, align: Option<Align>) -> Self {
314        self.scroll_to_rows(row..=row, align)
315    }
316
317    /// Scroll to a range of rows.
318    ///
319    /// See [`Self::scroll_to_row`] for details.
320    #[inline]
321    pub fn scroll_to_rows(mut self, rows: RangeInclusive<u64>, align: Option<Align>) -> Self {
322        self.scroll_to_rows = Some((rows, align));
323        self
324    }
325
326    /// Set a column to scroll to.
327    ///
328    /// `align` specifies if the column should be positioned in the left, center, or right of the view
329    /// (using [`Align::LEFT`], [`Align::Center`] or [`Align::RIGHT`]).
330    /// If `align` is `None`, the table will scroll just enough to bring the cursor into view.
331    ///
332    /// See also: [`Self::scroll_to_row`].
333    #[inline]
334    pub fn scroll_to_column(self, column: usize, align: Option<Align>) -> Self {
335        self.scroll_to_columns(column..=column, align)
336    }
337
338    /// Scroll to a range of columns.
339    ///
340    /// See [`Self::scroll_to_column`] for details.
341    #[inline]
342    pub fn scroll_to_columns(
343        mut self,
344        columns: RangeInclusive<usize>,
345        align: Option<Align>,
346    ) -> Self {
347        self.scroll_to_columns = Some((columns, align));
348        self
349    }
350
351    /// The top y coordinate offset of a specific row nr.
352    ///
353    /// `get_row_top_offset(0)` should always return 0.0.
354    #[expect(clippy::unused_self)] // for uniformity
355    fn get_row_top_offset(
356        &self,
357        ctx: &Context,
358        table_id: Id,
359        table_delegate: &dyn TableDelegate,
360        row_nr: u64,
361    ) -> f32 {
362        table_delegate.row_top_offset(ctx, table_id, row_nr)
363    }
364
365    /// Which row contains the given y offset (from the top)?
366    fn get_row_nr_at_y_offset(
367        &self,
368        ctx: &Context,
369        table_id: Id,
370        table_delegate: &dyn TableDelegate,
371        y_offset: f32,
372    ) -> u64 {
373        partition_point(0..=self.num_rows, |row_nr| {
374            y_offset <= self.get_row_top_offset(ctx, table_id, table_delegate, row_nr)
375        })
376        .saturating_sub(1)
377    }
378
379    pub fn show(mut self, ui: &mut Ui, table_delegate: &mut dyn TableDelegate) -> Response {
380        self.num_sticky_cols = self.num_sticky_cols.at_most(self.columns.len());
381
382        let id = TableState::id(ui, self.id_salt);
383        let state = TableState::load(ui, id);
384        let is_new = state.is_none();
385        let do_full_sizing_pass = is_new;
386        let mut state = state.unwrap_or_default();
387
388        for (i, column) in self.columns.iter_mut().enumerate() {
389            let column_id = column.id_for(i);
390            if let Some(existing_width) = state.col_widths.get(&column_id) {
391                column.current = *existing_width;
392            }
393            column.current = column.range.clamp(column.current);
394
395            if do_full_sizing_pass {
396                column.auto_size_this_frame = true;
397            }
398        }
399
400        let parent_width = ui.available_width();
401        let auto_size = match self.auto_size_mode {
402            AutoSizeMode::Never => false,
403            AutoSizeMode::Always => true,
404            AutoSizeMode::OnParentResize => state.parent_width != Some(parent_width),
405        };
406        if auto_size {
407            Column::auto_size(&mut self.columns, parent_width);
408        }
409        state.parent_width = Some(parent_width);
410
411        let col_x = {
412            let mut x = ui.cursor().min.x;
413            let mut col_x = Vec1::with_capacity(x, self.columns.len() + 1);
414            for column in &self.columns {
415                x += column.current;
416                col_x.push(x);
417            }
418            col_x
419        };
420
421        let header_row_y = {
422            let mut y = ui.cursor().min.y;
423            let mut sticky_row_y = Vec1::with_capacity(y, self.headers.len() + 1);
424            for header in &self.headers {
425                y += header.height;
426                sticky_row_y.push(y);
427            }
428            sticky_row_y
429        };
430
431        let sticky_size = Vec2::new(
432            self.columns[..self.num_sticky_cols]
433                .iter()
434                .map(|c| c.current)
435                .sum(),
436            self.headers.iter().map(|h| h.height).sum(),
437        );
438
439        let mut ui_builder = UiBuilder::new().layout(Layout::top_down(Align::Min));
440        if do_full_sizing_pass {
441            ui_builder = ui_builder.sizing_pass().invisible();
442            ui.request_discard("Full egui_table sizing");
443        }
444        let response = ui
445            .scope_builder(ui_builder, |ui| {
446                // Don't wrap text in the table cells.
447                ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); // TODO: I think this is default for horizontal layouts anyway?
448
449                let num_columns = self.columns.len();
450
451                for (col_nr, column) in self.columns.iter_mut().enumerate() {
452                    if column.resizable {
453                        let column_resize_id = id.with(column.id_for(col_nr)).with("resize");
454                        if let Some(response) = ui.read_response(column_resize_id)
455                            && response.double_clicked()
456                        {
457                            column.auto_size_this_frame = true;
458                        }
459                    }
460                    if column.auto_size_this_frame {
461                        ui.request_discard("egui_table column sizing");
462                    }
463                }
464
465                SplitScroll {
466                    scroll_enabled: Vec2b::new(true, true),
467                    fixed_size: sticky_size,
468                    scroll_outer_size: (ui.available_size() - sticky_size).at_least(Vec2::ZERO),
469                    scroll_content_size: Vec2::new(
470                        self.columns[self.num_sticky_cols..]
471                            .iter()
472                            .map(|c| c.current)
473                            .sum(),
474                        self.get_row_top_offset(ui, id, table_delegate, self.num_rows),
475                    ),
476                    stick_to_bottom: self.stick_to_bottom,
477                }
478                .show(
479                    ui,
480                    &mut TableSplitScrollDelegate {
481                        id,
482                        table_delegate,
483                        state: &mut state,
484                        table: &mut self,
485                        col_x,
486                        header_row_y,
487                        max_column_widths: vec![0.0; num_columns],
488                        visible_column_lines: Default::default(),
489                        do_full_sizing_pass,
490                        has_prefetched: false,
491                        egui_ctx: ui.clone(),
492                    },
493                );
494            })
495            .response;
496
497        state.store(ui, id);
498        response
499    }
500}
501
502#[derive(Clone, Copy, Debug)]
503struct ColumnResizer {
504    scroll_offset: Vec2,
505
506    top: f32,
507}
508
509fn update(map: &mut BTreeMap<usize, ColumnResizer>, key: usize, value: ColumnResizer) {
510    match map.entry(key) {
511        Entry::Vacant(entry) => {
512            entry.insert(value);
513        }
514        Entry::Occupied(mut entry) => {
515            entry.get_mut().top = entry.get_mut().top.min(value.top);
516        }
517    }
518}
519
520struct TableSplitScrollDelegate<'a> {
521    id: Id,
522    table_delegate: &'a mut dyn TableDelegate,
523    table: &'a mut Table,
524    state: &'a mut TableState,
525
526    /// The x coordinate for the start of each column, plus the end of the last column.
527    col_x: Vec1<f32>,
528
529    /// The y coordinate for the start of each header row, plus the end of the last header row.
530    header_row_y: Vec1<f32>,
531
532    /// Actual width of the widest element in each column
533    max_column_widths: Vec<f32>,
534
535    /// Key is column number. The resizer is to the right of the column.
536    visible_column_lines: BTreeMap<usize, ColumnResizer>,
537
538    do_full_sizing_pass: bool,
539
540    has_prefetched: bool,
541
542    egui_ctx: Context,
543}
544
545impl TableSplitScrollDelegate<'_> {
546    /// Helper wrapper around [`Table::get_row_top_offset`].
547    fn get_row_top_offset(&self, row_nr: u64) -> f32 {
548        self.table
549            .get_row_top_offset(&self.egui_ctx, self.id, self.table_delegate, row_nr)
550    }
551
552    /// Helper wrapper around [`Table::get_row_nr_at_y_offset`].
553    fn get_row_nr_at_y_offset(&self, y_offset: f32) -> u64 {
554        self.table
555            .get_row_nr_at_y_offset(&self.egui_ctx, self.id, self.table_delegate, y_offset)
556    }
557
558    fn header_ui(&mut self, ui: &mut Ui, scroll_offset: Vec2) {
559        for (row_nr, header_row) in self.table.headers.iter().enumerate() {
560            let groups = if header_row.groups.is_empty() {
561                (0..self.table.columns.len()).map(|i| i..i + 1).collect()
562            } else {
563                header_row.groups.clone()
564            };
565
566            let y_range = Rangef::new(self.header_row_y[row_nr], self.header_row_y[row_nr + 1]);
567
568            for (group_index, col_range) in groups.into_iter().enumerate() {
569                let start = col_range.start;
570                let end = col_range.end;
571
572                let mut header_rect =
573                    Rect::from_x_y_ranges(self.col_x[start]..=self.col_x[end], y_range)
574                        .translate(-scroll_offset);
575
576                if 0 < start
577                    && self.table.columns[start - 1].resizable
578                    && ui.clip_rect().x_range().contains(header_rect.left())
579                {
580                    // The previous column is resizable, so make sure the resize line goes to above this heading:
581                    update(
582                        &mut self.visible_column_lines,
583                        start - 1,
584                        ColumnResizer {
585                            scroll_offset,
586                            top: header_rect.top(),
587                        },
588                    );
589                }
590
591                let clip_rect = header_rect;
592
593                let last_column = &self.table.columns[end - 1];
594                let auto_size_this_frame = last_column.auto_size_this_frame; // TODO: correct?
595
596                if auto_size_this_frame {
597                    // Note: we shrink the cell rect when auto-sizing, but not the clip rect! This is to avoid flicker.
598                    header_rect.max.x = header_rect.min.x
599                        + self.table.columns[start..end]
600                            .iter()
601                            .map(|column| column.range.min)
602                            .sum::<f32>();
603                }
604
605                let mut ui_builder = UiBuilder::new()
606                    .max_rect(header_rect)
607                    .id_salt(("header", row_nr, group_index))
608                    .layout(egui::Layout::left_to_right(egui::Align::Center));
609                if auto_size_this_frame {
610                    ui_builder = ui_builder.sizing_pass();
611                }
612                let mut cell_ui = ui.new_child(ui_builder);
613                cell_ui.shrink_clip_rect(clip_rect);
614
615                self.table_delegate.header_cell_ui(
616                    &mut cell_ui,
617                    &HeaderCellInfo {
618                        group_index,
619                        col_range,
620                        row_nr,
621                        table_id: self.id,
622                    },
623                );
624
625                if start + 1 == end {
626                    // normal single-column group
627                    let col_nr = start;
628                    let column = &self.table.columns[start];
629                    let width = &mut self.max_column_widths[col_nr];
630                    *width = width.max(cell_ui.min_size().x);
631
632                    // Save column lines for later interaction:
633                    if column.resizable && ui.clip_rect().x_range().contains(header_rect.right()) {
634                        update(
635                            &mut self.visible_column_lines,
636                            col_nr,
637                            ColumnResizer {
638                                scroll_offset,
639                                top: header_rect.top(),
640                            },
641                        );
642                    }
643                }
644            }
645        }
646    }
647
648    fn region_ui(&mut self, ui: &mut Ui, scroll_offset: Vec2, do_prefetch: bool) {
649        // Used to find the visible range of columns and rows:
650        let viewport = ui.clip_rect().translate(scroll_offset);
651
652        let col_range = if self.table.columns.is_empty() || viewport.left() == viewport.right() {
653            0..0
654        } else if self.do_full_sizing_pass {
655            // We do the UI for all columns during a sizing pass, so we can auto-size ALL columns
656            0..self.table.columns.len()
657        } else {
658            // Only paint the visible columns:
659            let col_idx_at = |x: f32| -> usize {
660                self.col_x
661                    .partition_point(|&col_x| col_x < x)
662                    .saturating_sub(1)
663                    .at_most(self.table.columns.len() - 1)
664            };
665
666            col_idx_at(viewport.min.x)..col_idx_at(viewport.max.x) + 1
667        };
668
669        let row_range = if self.table.num_rows == 0 || viewport.top() == viewport.bottom() {
670            0..0
671        } else {
672            // Only paint the visible rows:
673            let row_idx_at = |y: f32| -> u64 {
674                let row_nr = self.get_row_nr_at_y_offset(y - self.header_row_y.last());
675                row_nr.at_most(self.table.num_rows.saturating_sub(1))
676            };
677
678            let margin = if do_prefetch {
679                1.0 // Handle possible rounding errors in the syncing of the scroll offsets
680            } else {
681                0.0
682            };
683
684            row_idx_at(viewport.min.y - margin)..row_idx_at(viewport.max.y + margin) + 1
685        };
686
687        if do_prefetch {
688            self.table_delegate.prepare(&PrefetchInfo {
689                num_sticky_columns: self.table.num_sticky_cols,
690                visible_columns: col_range.clone(),
691                visible_rows: row_range.clone(),
692                table_id: self.id,
693            });
694            self.has_prefetched = true;
695        } else {
696            debug_assert!(
697                self.has_prefetched,
698                "SplitScroll delegate methods called in unexpected order"
699            );
700        }
701
702        for row_nr in row_range {
703            let y_range = Rangef::new(
704                self.header_row_y.last() + self.get_row_top_offset(row_nr),
705                self.header_row_y.last() + self.get_row_top_offset(row_nr + 1),
706            );
707
708            let row_x_range = self.col_x[0]..=self.col_x[self.col_x.len() - 1];
709            let row_rect = Rect::from_x_y_ranges(row_x_range, y_range).translate(-scroll_offset);
710
711            let mut row_ui = ui.new_child(
712                UiBuilder::new()
713                    .max_rect(row_rect)
714                    .id_salt(("row", row_nr))
715                    .layout(egui::Layout::left_to_right(egui::Align::Center)),
716            );
717            row_ui.set_min_size(row_rect.size());
718
719            self.table_delegate.row_ui(&mut row_ui, row_nr);
720
721            for col_nr in col_range.clone() {
722                let column = &self.table.columns[col_nr];
723                let mut cell_rect =
724                    Rect::from_x_y_ranges(self.col_x[col_nr]..=self.col_x[col_nr + 1], y_range)
725                        .translate(-scroll_offset);
726                let clip_rect = cell_rect;
727                if column.auto_size_this_frame {
728                    // Note: we shrink the cell rect when auto-sizing, but not the clip rect! This is to avoid flicker.
729                    cell_rect.max.x = cell_rect.min.x + column.range.min;
730                }
731
732                let mut ui_builder = UiBuilder::new()
733                    .max_rect(cell_rect)
734                    .id_salt((row_nr, col_nr))
735                    .layout(egui::Layout::left_to_right(egui::Align::Center));
736                if column.auto_size_this_frame {
737                    ui_builder = ui_builder.sizing_pass();
738                }
739                let mut cell_ui = row_ui.new_child(ui_builder);
740                cell_ui.shrink_clip_rect(clip_rect);
741
742                self.table_delegate.cell_ui(
743                    &mut cell_ui,
744                    &CellInfo {
745                        col_nr,
746                        row_nr,
747                        table_id: self.id,
748                    },
749                );
750
751                let width = &mut self.max_column_widths[col_nr];
752                *width = width.max(cell_ui.min_size().x);
753            }
754        }
755
756        // Save column lines for later interaction:
757        for col_nr in col_range.clone() {
758            let column = &self.table.columns[col_nr];
759            if column.resizable {
760                update(
761                    &mut self.visible_column_lines,
762                    col_nr,
763                    ColumnResizer {
764                        scroll_offset,
765                        top: *self.header_row_y.last(),
766                    },
767                );
768            }
769        }
770    }
771}
772
773impl SplitScrollDelegate for TableSplitScrollDelegate<'_> {
774    // First to be called
775    fn right_bottom_ui(&mut self, ui: &mut Ui) {
776        if self.table.scroll_to_columns.is_some() || self.table.scroll_to_rows.is_some() {
777            let mut target_rect = ui.clip_rect(); // no scrolling
778            let mut target_align = None;
779
780            if let Some((column_range, align)) = &self.table.scroll_to_columns {
781                // Use the first scrollable column as the base, so that offsets start
782                // at 0 for the first non-sticky column — mirroring how row_top_offset
783                // starts at 0 for the first data row.
784                let scrollable_col_x_base = self.col_x[self.table.num_sticky_cols];
785                let x_from_column_nr = |col_nr: usize| -> f32 {
786                    ui.min_rect().left() + (self.col_x[col_nr] - scrollable_col_x_base)
787                };
788
789                let sticky_width = scrollable_col_x_base - self.col_x.first();
790
791                // Subtract sticky_width from the left of the target rect so that when
792                // scroll_to_rect aligns the left of the target to the viewport left, the
793                // actual column lands just right of the sticky columns (not behind them).
794                target_rect.min.x = x_from_column_nr(*column_range.start()) - sticky_width;
795                target_rect.max.x = x_from_column_nr(*column_range.end() + 1);
796                target_align = target_align.or(*align);
797            }
798
799            if let Some((row_range, align)) = &self.table.scroll_to_rows {
800                let y_from_row_nr =
801                    |row_nr: u64| -> f32 { ui.min_rect().top() + self.get_row_top_offset(row_nr) };
802
803                let sticky_height = self.header_row_y.last() - self.header_row_y.first();
804
805                // Subtract sticky_height from the top of the target rect so that when
806                // scroll_to_rect aligns the top of the target to the viewport top, the
807                // actual row lands just below the sticky header (not behind it).
808                target_rect.min.y = y_from_row_nr(*row_range.start()) - sticky_height;
809                target_rect.max.y = y_from_row_nr(*row_range.end() + 1);
810                target_align = target_align.or(*align);
811            }
812
813            ui.scroll_to_rect(target_rect, target_align);
814        }
815
816        let scroll_offset = ui.clip_rect().min - ui.min_rect().min;
817        self.region_ui(ui, scroll_offset, true);
818    }
819
820    fn left_top_ui(&mut self, ui: &mut Ui) {
821        self.header_ui(ui, Vec2::ZERO);
822    }
823
824    fn right_top_ui(&mut self, ui: &mut Ui) {
825        let scroll_offset = vec2(ui.clip_rect().min.x - ui.min_rect().min.x, 0.0);
826        self.header_ui(ui, scroll_offset);
827    }
828
829    fn left_bottom_ui(&mut self, ui: &mut Ui) {
830        self.region_ui(
831            ui,
832            vec2(0.0, ui.clip_rect().min.y - ui.min_rect().min.y),
833            false,
834        );
835    }
836
837    fn finish(&mut self, ui: &mut Ui) {
838        // Paint column resize lines
839
840        for (col_nr, ColumnResizer { scroll_offset, top }) in &self.visible_column_lines {
841            let col_nr = *col_nr;
842            let Some(column) = self.table.columns.get(col_nr) else {
843                continue;
844            };
845            if !column.resizable {
846                continue;
847            }
848
849            let column_id = column.id_for(col_nr);
850            let used_width = column.range.clamp(self.max_column_widths[col_nr]);
851
852            let column_width = self
853                .state
854                .col_widths
855                .entry(column_id)
856                .or_insert(column.current);
857
858            let layout_width = *column_width; // Width used when computing col_x
859
860            if ui.is_sizing_pass() || column.auto_size_this_frame {
861                // Shrink to fit the widest element in the column:
862                *column_width = used_width;
863            } else {
864                // Grow to fit the widest element in the column:
865                *column_width = column_width.max(used_width);
866            }
867
868            let column_resize_id = self.id.with(column.id_for(col_nr)).with("resize");
869
870            // Right side of the column, adjusted for any width change since layout:
871            let mut x = self.col_x[col_nr + 1] - scroll_offset.x + (*column_width - layout_width);
872            let yrange = Rangef::new(*top, ui.clip_rect().bottom());
873            let line_rect = egui::Rect::from_x_y_ranges(x..=x, yrange)
874                .expand(ui.style().interaction.resize_grab_radius_side);
875
876            let resize_response =
877                ui.interact(line_rect, column_resize_id, egui::Sense::click_and_drag());
878
879            if resize_response.dragged()
880                && let Some(pointer) = ui.pointer_latest_pos()
881            {
882                // Drag-to-resize.
883                // TODO: use `ui.intrinsic_size` (once it exist) to prevent
884                // resizing below what the content can fit within.
885                let new_width = *column_width + pointer.x - x;
886                let new_width = column.range.clamp(new_width);
887                x += new_width - *column_width;
888                *column_width = new_width;
889            }
890
891            let dragging_something_else =
892                ui.input(|i| i.pointer.any_down() || i.pointer.any_pressed());
893            let resize_hover = resize_response.hovered() && !dragging_something_else;
894
895            if resize_hover || resize_response.dragged() {
896                ui.set_cursor_icon(egui::CursorIcon::ResizeColumn);
897            }
898
899            let stroke = if resize_response.dragged() {
900                ui.style().visuals.widgets.active.bg_stroke
901            } else if resize_hover {
902                ui.style().visuals.widgets.hovered.bg_stroke
903            } else {
904                // ui.visuals().widgets.inactive.bg_stroke
905                ui.visuals().widgets.noninteractive.bg_stroke
906            };
907
908            ui.painter().vline(x, yrange, stroke);
909        }
910    }
911}
912
913/// Returns the index of the first element that returns `true` using binary search.
914fn partition_point(range: RangeInclusive<u64>, second_partition: impl Fn(u64) -> bool) -> u64 {
915    let mut min = *range.start();
916    let mut max = *range.end();
917
918    debug_assert!(min < max, "Bad call to partition_point");
919
920    while min < max {
921        let mid = min + (max - min) / 2;
922
923        if second_partition(mid) {
924            max = mid;
925        } else {
926            min = mid + 1;
927        }
928    }
929
930    min
931}
932
933#[cfg(test)]
934mod tests {
935    use crate::table::partition_point;
936
937    #[test]
938    fn test_partition_point() {
939        assert_eq!(partition_point(0..=17, |i| 8 <= i), 8);
940        assert_eq!(partition_point(0..=17, |i| 9 <= i), 9);
941        assert_eq!(partition_point(10..=17, |_| true), 10);
942        assert_eq!(partition_point(10..=17, |_| false), 17);
943    }
944}