egui_extras/
table.rs

1//! Table view with (optional) fixed header and scrolling body.
2//! Cell widths are precalculated with given size hints so we can have tables like this:
3//! | fixed size | all available space/minimum | 30% of available width | fixed size |
4//! Takes all available height, so if you want something below the table, put it in a strip.
5
6use 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// -----------------------------------------------------------------=----------
17
18#[derive(Clone, Copy, Debug, PartialEq)]
19enum InitialColumnSize {
20    /// Absolute size in points
21    Absolute(f32),
22
23    /// Base on content
24    Automatic(f32),
25
26    /// Take all available space
27    Remainder,
28}
29
30/// Specifies the properties of a column, like its width range.
31#[derive(Clone, Copy, Debug, PartialEq)]
32pub struct Column {
33    initial_width: InitialColumnSize,
34
35    width_range: Rangef,
36
37    /// Clip contents if too narrow?
38    clip: bool,
39
40    resizable: Option<bool>,
41
42    /// If set, we should acurately measure the size of this column this frame
43    /// so that we can correctly auto-size it. This is done as a `sizing_pass`.
44    auto_size_this_frame: bool,
45}
46
47impl Column {
48    /// Automatically sized based on content.
49    ///
50    /// If you have many thousands of rows and are therefore using [`TableBody::rows`]
51    /// or [`TableBody::heterogeneous_rows`], then the automatic size will only be based
52    /// on the currently visible rows.
53    pub fn auto() -> Self {
54        Self::auto_with_initial_suggestion(100.0)
55    }
56
57    /// Automatically sized.
58    ///
59    /// The given fallback is a loose suggestion, that may be used to wrap
60    /// cell contents, if they contain a wrapping layout.
61    /// In most cases though, the given value is ignored.
62    pub fn auto_with_initial_suggestion(suggested_width: f32) -> Self {
63        Self::new(InitialColumnSize::Automatic(suggested_width))
64    }
65
66    /// With this initial width.
67    pub fn initial(width: f32) -> Self {
68        Self::new(InitialColumnSize::Absolute(width))
69    }
70
71    /// Always this exact width, never shrink or grow.
72    pub fn exact(width: f32) -> Self {
73        Self::new(InitialColumnSize::Absolute(width))
74            .range(width..=width)
75            .clip(true)
76    }
77
78    /// Take all the space remaining after the other columns have
79    /// been sized.
80    ///
81    /// If you have multiple [`Column::remainder`] they all
82    /// share the remaining space equally.
83    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    /// Can this column be resized by dragging the column separator?
98    ///
99    /// If you don't call this, the fallback value of
100    /// [`TableBuilder::resizable`] is used (which by default is `false`).
101    #[inline]
102    pub fn resizable(mut self, resizable: bool) -> Self {
103        self.resizable = Some(resizable);
104        self
105    }
106
107    /// If `true`: Allow the column to shrink enough to clip the contents.
108    /// If `false`: The column will always be wide enough to contain all its content.
109    ///
110    /// Clipping can make sense if you expect a column to contain a lot of things,
111    /// and you don't want it too take up too much space.
112    /// If you turn on clipping you should also consider calling [`Self::at_least`].
113    ///
114    /// Default: `false`.
115    #[inline]
116    pub fn clip(mut self, clip: bool) -> Self {
117        self.clip = clip;
118        self
119    }
120
121    /// Won't shrink below this width (in points).
122    ///
123    /// Default: 0.0
124    #[inline]
125    pub fn at_least(mut self, minimum: f32) -> Self {
126        self.width_range.min = minimum;
127        self
128    }
129
130    /// Won't grow above this width (in points).
131    ///
132    /// Default: [`f32::INFINITY`]
133    #[inline]
134    pub fn at_most(mut self, maximum: f32) -> Self {
135        self.width_range.max = maximum;
136        self
137    }
138
139    /// Allowed range of movement (in points), if in a resizable [`Table`].
140    #[inline]
141    pub fn range(mut self, range: impl Into<Rangef>) -> Self {
142        self.width_range = range.into();
143        self
144    }
145
146    /// If set, the column will be automatically sized based on the content this frame.
147    ///
148    /// Do not set this every frame, just on a specific action.
149    #[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
179// -----------------------------------------------------------------=----------
180
181struct 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
211// -----------------------------------------------------------------=----------
212
213/// Builder for a [`Table`] with (optional) fixed header and scrolling body.
214///
215/// You must pre-allocate all columns with [`Self::column`]/[`Self::columns`].
216///
217/// If you have multiple [`Table`]:s in the same [`Ui`]
218/// you will need to give them unique id:s by with [`Self::id_salt`].
219///
220/// ### Example
221/// ```
222/// # egui::__run_test_ui(|ui| {
223/// use egui_extras::{TableBuilder, Column};
224/// TableBuilder::new(ui)
225///     .column(Column::auto().resizable(true))
226///     .column(Column::remainder())
227///     .header(20.0, |mut header| {
228///         header.col(|ui| {
229///             ui.heading("First column");
230///         });
231///         header.col(|ui| {
232///             ui.heading("Second column");
233///         });
234///     })
235///     .body(|mut body| {
236///         body.row(30.0, |mut row| {
237///             row.col(|ui| {
238///                 ui.label("Hello");
239///             });
240///             row.col(|ui| {
241///                 ui.button("world!");
242///             });
243///         });
244///     });
245/// # });
246/// ```
247pub 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    /// Give this table a unique id within the parent [`Ui`].
274    ///
275    /// This is required if you have multiple tables in the same [`Ui`].
276    #[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    /// Give this table a unique id within the parent [`Ui`].
283    ///
284    /// This is required if you have multiple tables in the same [`Ui`].
285    #[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    /// Enable striped row background for improved readability.
292    ///
293    /// Default is whatever is in [`egui::Visuals::striped`].
294    #[inline]
295    pub fn striped(mut self, striped: bool) -> Self {
296        self.striped = Some(striped);
297        self
298    }
299
300    /// What should table cells sense for? (default: [`egui::Sense::hover()`]).
301    #[inline]
302    pub fn sense(mut self, sense: egui::Sense) -> Self {
303        self.sense = sense;
304        self
305    }
306
307    /// Make the columns resizable by dragging.
308    ///
309    /// You can set this for individual columns with [`Column::resizable`].
310    /// [`Self::resizable`] is used as a fallback for any column for which you don't call
311    /// [`Column::resizable`].
312    ///
313    /// If the _last_ column is [`Column::remainder`], then it won't be resizable
314    /// (and instead use up the remainder).
315    ///
316    /// Default is `false`.
317    #[inline]
318    pub fn resizable(mut self, resizable: bool) -> Self {
319        self.resizable = resizable;
320        self
321    }
322
323    /// Enable vertical scrolling in body (default: `true`)
324    #[inline]
325    pub fn vscroll(mut self, vscroll: bool) -> Self {
326        self.scroll_options.vscroll = vscroll;
327        self
328    }
329
330    /// Enables scrolling the table's contents using mouse drag (default: `true`).
331    ///
332    /// See [`ScrollArea::drag_to_scroll`] for more.
333    #[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    /// Should the scroll handle stick to the bottom position even as the content size changes
340    /// dynamically? The scroll handle remains stuck until manually changed, and will become stuck
341    /// once again when repositioned to the bottom. Default: `false`.
342    #[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    /// Set a row to scroll to.
349    ///
350    /// `align` specifies if the row should be positioned in the top, center, or bottom of the view
351    /// (using [`Align::TOP`], [`Align::Center`] or [`Align::BOTTOM`]).
352    /// If `align` is `None`, the table will scroll just enough to bring the cursor into view.
353    ///
354    /// See also: [`Self::vertical_scroll_offset`].
355    #[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    /// Set the vertical scroll offset position, in points.
362    ///
363    /// See also: [`Self::scroll_to_row`].
364    #[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    /// The minimum height of a vertical scroll area which requires scroll bars.
371    ///
372    /// The scroll area will only become smaller than this if the content is smaller than this
373    /// (and so we don't require scroll bars).
374    ///
375    /// Default: `200.0`.
376    #[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    /// Don't make the scroll area higher than this (add scroll-bars instead!).
383    ///
384    /// In other words: add scroll-bars when this height is reached.
385    /// Default: `800.0`.
386    #[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    /// For each axis (x,y):
393    /// * If true, add blank space outside the table, keeping the table small.
394    /// * If false, add blank space inside the table, expanding the table to fit the containing ui.
395    ///
396    /// Default: `true`.
397    ///
398    /// See [`ScrollArea::auto_shrink`] for more.
399    #[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    /// Set the visibility of both horizontal and vertical scroll bars.
406    ///
407    /// With `ScrollBarVisibility::VisibleWhenNeeded` (default), the scroll bar will be visible only when needed.
408    #[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    /// Should the scroll area animate `scroll_to_*` functions?
415    ///
416    /// Default: `true`.
417    #[inline]
418    pub fn animate_scrolling(mut self, animated: bool) -> Self {
419        self.scroll_options.animated = animated;
420        self
421    }
422
423    /// What layout should we use for the individual cells?
424    #[inline]
425    pub fn cell_layout(mut self, cell_layout: egui::Layout) -> Self {
426        self.cell_layout = cell_layout;
427        self
428    }
429
430    /// Allocate space for one column.
431    #[inline]
432    pub fn column(mut self, column: Column) -> Self {
433        self.columns.push(column);
434        self
435    }
436
437    /// Allocate space for several columns at once.
438    #[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    /// Reset all column widths.
453    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    /// Create a header row which always stays visible and at the top
459    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    /// Create table body without a header row
533    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// ----------------------------------------------------------------------------
580
581#[derive(Clone, Debug)]
582#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
583struct TableState {
584    column_widths: Vec<f32>,
585
586    /// If known from previous frame
587    #[cfg_attr(feature = "serde", serde(skip))]
588    max_used_widths: Vec<f32>,
589}
590
591impl TableState {
592    /// Return true if we should do a sizing pass.
593    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        // Make sure that the stored widths aren't out-dated:
609        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            // Make sure any non-resizable `remainder` columns are updated
625            // to take up the remainder of the current available width.
626            // Also handles changing item spacing.
627            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                    // Resiable columns keep their width:
639                    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
674// ----------------------------------------------------------------------------
675
676/// Table struct which can construct a [`TableBody`].
677///
678/// Is created by [`TableBuilder`] by either calling [`TableBuilder::body`] or after creating a header row with [`TableBuilder::header`].
679pub 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    /// Accumulated maximum used widths for each column.
688    max_used_widths: Vec<f32>,
689
690    /// During the sizing pass we calculate the width of columns with [`Column::auto`].
691    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    /// Access the contained [`egui::Ui`].
703    ///
704    /// You can use this to e.g. modify the [`egui::Style`] with [`egui::Ui::style_mut`].
705    pub fn ui_mut(&mut self) -> &mut egui::Ui {
706        self.ui
707    }
708
709    /// Create table body after adding a header row
710    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                    // TableBody::row didn't find the correct row, so scroll to the bottom:
795                    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; // ignored, we only have vertical scrolling
801                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                // If the last column is 'remainder', then let it fill the remainder!
822                let eps = 0.1; // just to avoid some rounding errors.
823                *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                    // If we clip, we don't need to be as wide as the max used width
834                    *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                // Unless we clip we don't want to shrink below the
840                // size that was actually used:
841                *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                    // Auto-size: resize to what is needed.
862                    *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                            // Unless we clip we don't want to shrink below the
868                            // size that was actually used.
869                            // However, we still want to allow content that shrinks when you try
870                            // to make the column less wide, so we allow some small shrinkage each frame:
871                            // big enough to allow shrinking over time, small enough not to look ugly when
872                            // shrinking fails. This is a bit of a HACK around immediate mode.
873                            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.inactive.bg_stroke
900                    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
916/// The body of a table.
917///
918/// Is created by calling `body` on a [`Table`] (after adding a header row) or [`TableBuilder`] (without a header row).
919pub struct TableBody<'a> {
920    layout: StripLayout<'a>,
921
922    columns: &'a [Column],
923
924    /// Current column widths.
925    widths: &'a [f32],
926
927    /// Accumulated maximum used widths for each column.
928    max_used_widths: &'a mut [f32],
929
930    striped: bool,
931    row_index: usize,
932    y_range: Rangef,
933
934    /// Look for this row to scroll to.
935    scroll_to_row: Option<usize>,
936
937    /// If we find the correct row to scroll to,
938    /// this is set to the y-range of the row.
939    scroll_to_y_range: &'a mut Option<Rangef>,
940
941    hovered_row_index: Option<usize>,
942
943    /// Used to store the hovered row index between frames.
944    hovered_row_index_id: egui::Id,
945}
946
947impl<'a> TableBody<'a> {
948    /// Access the contained [`egui::Ui`].
949    ///
950    /// You can use this to e.g. modify the [`egui::Style`] with [`egui::Ui::style_mut`].
951    pub fn ui_mut(&mut self) -> &mut egui::Ui {
952        self.layout.ui
953    }
954
955    /// Where in screen-space is the table body?
956    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    /// Return a vector containing all column widths for this table body.
967    ///
968    /// This is primarily meant for use with [`TableBody::heterogeneous_rows`] in cases where row
969    /// heights are expected to according to the width of one or more cells -- for example, if text
970    /// is wrapped rather than clipped within the cell.
971    pub fn widths(&self) -> &[f32] {
972        self.widths
973    }
974
975    /// Add a single row with the given height.
976    ///
977    /// ⚠️ It is much more performant to use [`Self::rows`] or [`Self::heterogeneous_rows`],
978    /// as those functions will only render the visible rows.
979    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    /// Add many rows with same height.
1006    ///
1007    /// Is a lot more performant than adding each individual row as non visible rows must not be rendered.
1008    ///
1009    /// If you need many rows with different heights, use [`Self::heterogeneous_rows`] instead.
1010    ///
1011    /// ### Example
1012    /// ```
1013    /// # egui::__run_test_ui(|ui| {
1014    /// use egui_extras::{TableBuilder, Column};
1015    /// TableBuilder::new(ui)
1016    ///     .column(Column::remainder().at_least(100.0))
1017    ///     .body(|mut body| {
1018    ///         let row_height = 18.0;
1019    ///         let num_rows = 10_000;
1020    ///         body.rows(row_height, num_rows, |mut row| {
1021    ///             let row_index = row.index();
1022    ///             row.col(|ui| {
1023    ///                 ui.label(format!("First column of row {row_index}"));
1024    ///             });
1025    ///         });
1026    ///     });
1027    /// # });
1028    /// ```
1029    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    /// Add rows with varying heights.
1086    ///
1087    /// This takes a very slight performance hit compared to [`TableBody::rows`] due to the need to
1088    /// iterate over all row heights in order to calculate the virtual table height above and below the
1089    /// visible region, but it is many orders of magnitude more performant than adding individual
1090    /// heterogeneously-sized rows using [`TableBody::row`] at the cost of the additional complexity
1091    /// that comes with pre-calculating row heights and representing them as an iterator.
1092    ///
1093    /// ### Example
1094    /// ```
1095    /// # egui::__run_test_ui(|ui| {
1096    /// use egui_extras::{TableBuilder, Column};
1097    /// TableBuilder::new(ui)
1098    ///     .column(Column::remainder().at_least(100.0))
1099    ///     .body(|mut body| {
1100    ///         let row_heights: Vec<f32> = vec![60.0, 18.0, 31.0, 240.0];
1101    ///         body.heterogeneous_rows(row_heights.into_iter(), |mut row| {
1102    ///             let row_index = row.index();
1103    ///             let thick = row_index % 6 == 0;
1104    ///             row.col(|ui| {
1105    ///                 ui.centered_and_justified(|ui| {
1106    ///                     ui.label(row_index.to_string());
1107    ///                 });
1108    ///             });
1109    ///         });
1110    ///     });
1111    /// # });
1112    /// ```
1113    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        // Skip the invisible rows, and populate the first non-virtual row.
1129        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                // This row is visible:
1142                self.add_buffer(old_cursor_y as f32); // skip all the invisible rows
1143                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        // populate visible rows:
1163        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        // calculate height below the visible table range:
1195        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            // Catch desire to scroll past the end:
1211            *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            // we need to add a buffer to allow the table to
1217            // accurately calculate the scrollbar position
1218            self.add_buffer(height_below_visible as f32);
1219        }
1220    }
1221
1222    // Create a table row buffer of the given height to represent the non-visible portion of the
1223    // table.
1224    fn add_buffer(&mut self, height: f32) {
1225        self.layout.skip_space(egui::vec2(0.0, height));
1226    }
1227
1228    // Capture the hover information for the just created row. This is used in the next render
1229    // to ensure that the entire row is highlighted.
1230    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
1246/// The row of a table.
1247/// Is created by [`TableRow`] for each created [`TableBody::row`] or each visible row in rows created by calling [`TableBody::rows`].
1248pub struct TableRow<'a, 'b> {
1249    layout: &'b mut StripLayout<'a>,
1250    columns: &'b [Column],
1251    widths: &'b [f32],
1252
1253    /// grows during building with the maximum widths
1254    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    /// Add the contents of a column on this row (i.e. a cell).
1269    ///
1270    /// Returns the used space (`min_rect`) plus the [`Response`] of the whole cell.
1271    #[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 // anything will look wrong, so pick something that is obviously wrong
1290        };
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    /// Set the selection highlight state for cells added after a call to this function.
1325    #[inline]
1326    pub fn set_selected(&mut self, selected: bool) {
1327        self.selected = selected;
1328    }
1329
1330    /// Set the hovered highlight state for cells added after a call to this function.
1331    #[inline]
1332    pub fn set_hovered(&mut self, hovered: bool) {
1333        self.hovered = hovered;
1334    }
1335
1336    /// Returns a union of the [`Response`]s of the cells added to the row up to this point.
1337    ///
1338    /// You need to add at least one row to the table before calling this function.
1339    pub fn response(&self) -> Response {
1340        self.response
1341            .clone()
1342            .expect("Should only be called after `col`")
1343    }
1344
1345    /// Returns the index of the row.
1346    #[inline]
1347    pub fn index(&self) -> usize {
1348        self.row_index
1349    }
1350
1351    /// Returns the index of the column. Incremented after a column is added.
1352    #[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}