egui/
grid.rs

1use emath::GuiRounding as _;
2
3use crate::{
4    vec2, Align2, Color32, Context, Id, InnerResponse, NumExt, Painter, Rect, Region, Style, Ui,
5    UiBuilder, Vec2,
6};
7
8#[cfg(debug_assertions)]
9use crate::Stroke;
10
11#[derive(Clone, Debug, Default, PartialEq)]
12pub(crate) struct State {
13    col_widths: Vec<f32>,
14    row_heights: Vec<f32>,
15}
16
17impl State {
18    pub fn load(ctx: &Context, id: Id) -> Option<Self> {
19        ctx.data_mut(|d| d.get_temp(id))
20    }
21
22    pub fn store(self, ctx: &Context, id: Id) {
23        // We don't persist Grids, because
24        // A) there are potentially a lot of them, using up a lot of space (and therefore serialization time)
25        // B) if the code changes, the grid _should_ change, and not remember old sizes
26        ctx.data_mut(|d| d.insert_temp(id, self));
27    }
28
29    fn set_min_col_width(&mut self, col: usize, width: f32) {
30        self.col_widths
31            .resize(self.col_widths.len().max(col + 1), 0.0);
32        self.col_widths[col] = self.col_widths[col].max(width);
33    }
34
35    fn set_min_row_height(&mut self, row: usize, height: f32) {
36        self.row_heights
37            .resize(self.row_heights.len().max(row + 1), 0.0);
38        self.row_heights[row] = self.row_heights[row].max(height);
39    }
40
41    fn col_width(&self, col: usize) -> Option<f32> {
42        self.col_widths.get(col).copied()
43    }
44
45    fn row_height(&self, row: usize) -> Option<f32> {
46        self.row_heights.get(row).copied()
47    }
48
49    fn full_width(&self, x_spacing: f32) -> f32 {
50        self.col_widths.iter().sum::<f32>()
51            + (self.col_widths.len().at_least(1) - 1) as f32 * x_spacing
52    }
53}
54
55// ----------------------------------------------------------------------------
56
57// type alias for boxed function to determine row color during grid generation
58type ColorPickerFn = Box<dyn Send + Sync + Fn(usize, &Style) -> Option<Color32>>;
59
60pub(crate) struct GridLayout {
61    ctx: Context,
62    style: std::sync::Arc<Style>,
63    id: Id,
64
65    /// First frame (no previous know state).
66    is_first_frame: bool,
67
68    /// State previous frame (if any).
69    /// This can be used to predict future sizes of cells.
70    prev_state: State,
71
72    /// State accumulated during the current frame.
73    curr_state: State,
74    initial_available: Rect,
75
76    // Options:
77    num_columns: Option<usize>,
78    spacing: Vec2,
79    min_cell_size: Vec2,
80    max_cell_size: Vec2,
81    color_picker: Option<ColorPickerFn>,
82
83    // Cursor:
84    col: usize,
85    row: usize,
86}
87
88impl GridLayout {
89    pub(crate) fn new(ui: &Ui, id: Id, prev_state: Option<State>) -> Self {
90        let is_first_frame = prev_state.is_none();
91        let prev_state = prev_state.unwrap_or_default();
92
93        // TODO(emilk): respect current layout
94
95        let initial_available = ui.placer().max_rect().intersect(ui.cursor());
96        debug_assert!(
97            initial_available.min.x.is_finite(),
98            "Grid not yet available for right-to-left layouts"
99        );
100
101        ui.ctx().check_for_id_clash(id, initial_available, "Grid");
102
103        Self {
104            ctx: ui.ctx().clone(),
105            style: ui.style().clone(),
106            id,
107            is_first_frame,
108            prev_state,
109            curr_state: State::default(),
110            initial_available,
111
112            num_columns: None,
113            spacing: ui.spacing().item_spacing,
114            min_cell_size: ui.spacing().interact_size,
115            max_cell_size: Vec2::INFINITY,
116            color_picker: None,
117
118            col: 0,
119            row: 0,
120        }
121    }
122}
123
124impl GridLayout {
125    fn prev_col_width(&self, col: usize) -> f32 {
126        self.prev_state
127            .col_width(col)
128            .unwrap_or(self.min_cell_size.x)
129    }
130
131    fn prev_row_height(&self, row: usize) -> f32 {
132        self.prev_state
133            .row_height(row)
134            .unwrap_or(self.min_cell_size.y)
135    }
136
137    pub(crate) fn wrap_text(&self) -> bool {
138        self.max_cell_size.x.is_finite()
139    }
140
141    pub(crate) fn available_rect(&self, region: &Region) -> Rect {
142        let is_last_column = Some(self.col + 1) == self.num_columns;
143
144        let width = if is_last_column {
145            // The first frame we don't really know the widths of the previous columns,
146            // so returning a big available width here can cause trouble.
147            if self.is_first_frame {
148                self.curr_state
149                    .col_width(self.col)
150                    .unwrap_or(self.min_cell_size.x)
151            } else {
152                (self.initial_available.right() - region.cursor.left())
153                    .at_most(self.max_cell_size.x)
154            }
155        } else if self.max_cell_size.x.is_finite() {
156            // TODO(emilk): should probably heed `prev_state` here too
157            self.max_cell_size.x
158        } else {
159            // If we want to allow width-filling widgets like [`Separator`] in one of the first cells
160            // then we need to make sure they don't spill out of the first cell:
161            self.prev_state
162                .col_width(self.col)
163                .or_else(|| self.curr_state.col_width(self.col))
164                .unwrap_or(self.min_cell_size.x)
165        };
166
167        // If something above was wider, we can be wider:
168        let width = width.max(self.curr_state.col_width(self.col).unwrap_or(0.0));
169
170        let available = region.max_rect.intersect(region.cursor);
171
172        let height = region.max_rect.max.y - available.top();
173        let height = height
174            .at_least(self.min_cell_size.y)
175            .at_most(self.max_cell_size.y);
176
177        Rect::from_min_size(available.min, vec2(width, height))
178    }
179
180    pub(crate) fn next_cell(&self, cursor: Rect, child_size: Vec2) -> Rect {
181        let width = self.prev_state.col_width(self.col).unwrap_or(0.0);
182        let height = self.prev_row_height(self.row);
183        let size = child_size.max(vec2(width, height));
184        Rect::from_min_size(cursor.min, size).round_ui()
185    }
186
187    #[allow(clippy::unused_self)]
188    pub(crate) fn align_size_within_rect(&self, size: Vec2, frame: Rect) -> Rect {
189        // TODO(emilk): allow this alignment to be customized
190        Align2::LEFT_CENTER
191            .align_size_within_rect(size, frame)
192            .round_ui()
193    }
194
195    pub(crate) fn justify_and_align(&self, frame: Rect, size: Vec2) -> Rect {
196        self.align_size_within_rect(size, frame)
197    }
198
199    pub(crate) fn advance(&mut self, cursor: &mut Rect, _frame_rect: Rect, widget_rect: Rect) {
200        #[cfg(debug_assertions)]
201        {
202            let debug_expand_width = self.style.debug.show_expand_width;
203            let debug_expand_height = self.style.debug.show_expand_height;
204            if debug_expand_width || debug_expand_height {
205                let rect = widget_rect;
206                let too_wide = rect.width() > self.prev_col_width(self.col);
207                let too_high = rect.height() > self.prev_row_height(self.row);
208
209                if (debug_expand_width && too_wide) || (debug_expand_height && too_high) {
210                    let painter = self.ctx.debug_painter();
211                    painter.rect_stroke(
212                        rect,
213                        0.0,
214                        (1.0, Color32::LIGHT_BLUE),
215                        crate::StrokeKind::Inside,
216                    );
217
218                    let stroke = Stroke::new(2.5, Color32::from_rgb(200, 0, 0));
219                    let paint_line_seg = |a, b| painter.line_segment([a, b], stroke);
220
221                    if debug_expand_width && too_wide {
222                        paint_line_seg(rect.left_top(), rect.left_bottom());
223                        paint_line_seg(rect.left_center(), rect.right_center());
224                        paint_line_seg(rect.right_top(), rect.right_bottom());
225                    }
226                }
227            }
228        }
229
230        self.curr_state
231            .set_min_col_width(self.col, widget_rect.width().max(self.min_cell_size.x));
232        self.curr_state
233            .set_min_row_height(self.row, widget_rect.height().max(self.min_cell_size.y));
234
235        cursor.min.x += self.prev_col_width(self.col) + self.spacing.x;
236        self.col += 1;
237    }
238
239    fn paint_row(&self, cursor: &Rect, painter: &Painter) {
240        // handle row color painting based on color-picker function
241        let Some(color_picker) = self.color_picker.as_ref() else {
242            return;
243        };
244        let Some(row_color) = color_picker(self.row, &self.style) else {
245            return;
246        };
247        let Some(height) = self.prev_state.row_height(self.row) else {
248            return;
249        };
250        // Paint background for coming row:
251        let size = Vec2::new(self.prev_state.full_width(self.spacing.x), height);
252        let rect = Rect::from_min_size(cursor.min, size);
253        let rect = rect.expand2(0.5 * self.spacing.y * Vec2::Y);
254        let rect = rect.expand2(2.0 * Vec2::X); // HACK: just looks better with some spacing on the sides
255
256        painter.rect_filled(rect, 2.0, row_color);
257    }
258
259    pub(crate) fn end_row(&mut self, cursor: &mut Rect, painter: &Painter) {
260        cursor.min.x = self.initial_available.min.x;
261        cursor.min.y += self.spacing.y;
262        cursor.min.y += self
263            .curr_state
264            .row_height(self.row)
265            .unwrap_or(self.min_cell_size.y);
266
267        self.col = 0;
268        self.row += 1;
269
270        self.paint_row(cursor, painter);
271    }
272
273    pub(crate) fn save(&self) {
274        // We need to always save state on the first frame, otherwise request_discard
275        // would be called repeatedly (see #5132)
276        if self.curr_state != self.prev_state || self.is_first_frame {
277            self.curr_state.clone().store(&self.ctx, self.id);
278            self.ctx.request_repaint();
279        }
280    }
281}
282
283// ----------------------------------------------------------------------------
284
285/// A simple grid layout.
286///
287/// The cells are always laid out left to right, top-down.
288/// The contents of each cell will be aligned to the left and center.
289///
290/// If you want to add multiple widgets to a cell you need to group them with
291/// [`Ui::horizontal`], [`Ui::vertical`] etc.
292///
293/// ```
294/// # egui::__run_test_ui(|ui| {
295/// egui::Grid::new("some_unique_id").show(ui, |ui| {
296///     ui.label("First row, first column");
297///     ui.label("First row, second column");
298///     ui.end_row();
299///
300///     ui.label("Second row, first column");
301///     ui.label("Second row, second column");
302///     ui.label("Second row, third column");
303///     ui.end_row();
304///
305///     ui.horizontal(|ui| { ui.label("Same"); ui.label("cell"); });
306///     ui.label("Third row, second column");
307///     ui.end_row();
308/// });
309/// # });
310/// ```
311#[must_use = "You should call .show()"]
312pub struct Grid {
313    id_salt: Id,
314    num_columns: Option<usize>,
315    min_col_width: Option<f32>,
316    min_row_height: Option<f32>,
317    max_cell_size: Vec2,
318    spacing: Option<Vec2>,
319    start_row: usize,
320    color_picker: Option<ColorPickerFn>,
321}
322
323impl Grid {
324    /// Create a new [`Grid`] with a locally unique identifier.
325    pub fn new(id_salt: impl std::hash::Hash) -> Self {
326        Self {
327            id_salt: Id::new(id_salt),
328            num_columns: None,
329            min_col_width: None,
330            min_row_height: None,
331            max_cell_size: Vec2::INFINITY,
332            spacing: None,
333            start_row: 0,
334            color_picker: None,
335        }
336    }
337
338    /// Setting this will allow for dynamic coloring of rows of the grid object
339    #[inline]
340    pub fn with_row_color<F>(mut self, color_picker: F) -> Self
341    where
342        F: Send + Sync + Fn(usize, &Style) -> Option<Color32> + 'static,
343    {
344        self.color_picker = Some(Box::new(color_picker));
345        self
346    }
347
348    /// Setting this will allow the last column to expand to take up the rest of the space of the parent [`Ui`].
349    #[inline]
350    pub fn num_columns(mut self, num_columns: usize) -> Self {
351        self.num_columns = Some(num_columns);
352        self
353    }
354
355    /// If `true`, add a subtle background color to every other row.
356    ///
357    /// This can make a table easier to read.
358    /// Default is whatever is in [`crate::Visuals::striped`].
359    pub fn striped(self, striped: bool) -> Self {
360        if striped {
361            self.with_row_color(striped_row_color)
362        } else {
363            // Explicitly set the row color to nothing.
364            // Needed so that when the style.visuals.striped value is checked later on,
365            // it is clear that the user does not want stripes on this specific Grid.
366            self.with_row_color(|_row: usize, _style: &Style| None)
367        }
368    }
369
370    /// Set minimum width of each column.
371    /// Default: [`crate::style::Spacing::interact_size`]`.x`.
372    #[inline]
373    pub fn min_col_width(mut self, min_col_width: f32) -> Self {
374        self.min_col_width = Some(min_col_width);
375        self
376    }
377
378    /// Set minimum height of each row.
379    /// Default: [`crate::style::Spacing::interact_size`]`.y`.
380    #[inline]
381    pub fn min_row_height(mut self, min_row_height: f32) -> Self {
382        self.min_row_height = Some(min_row_height);
383        self
384    }
385
386    /// Set soft maximum width (wrapping width) of each column.
387    #[inline]
388    pub fn max_col_width(mut self, max_col_width: f32) -> Self {
389        self.max_cell_size.x = max_col_width;
390        self
391    }
392
393    /// Set spacing between columns/rows.
394    /// Default: [`crate::style::Spacing::item_spacing`].
395    #[inline]
396    pub fn spacing(mut self, spacing: impl Into<Vec2>) -> Self {
397        self.spacing = Some(spacing.into());
398        self
399    }
400
401    /// Change which row number the grid starts on.
402    /// This can be useful when you have a large [`crate::Grid`] inside of [`crate::ScrollArea::show_rows`].
403    #[inline]
404    pub fn start_row(mut self, start_row: usize) -> Self {
405        self.start_row = start_row;
406        self
407    }
408}
409
410impl Grid {
411    pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
412        self.show_dyn(ui, Box::new(add_contents))
413    }
414
415    fn show_dyn<'c, R>(
416        self,
417        ui: &mut Ui,
418        add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
419    ) -> InnerResponse<R> {
420        let Self {
421            id_salt,
422            num_columns,
423            min_col_width,
424            min_row_height,
425            max_cell_size,
426            spacing,
427            start_row,
428            mut color_picker,
429        } = self;
430        let min_col_width = min_col_width.unwrap_or_else(|| ui.spacing().interact_size.x);
431        let min_row_height = min_row_height.unwrap_or_else(|| ui.spacing().interact_size.y);
432        let spacing = spacing.unwrap_or_else(|| ui.spacing().item_spacing);
433        if color_picker.is_none() && ui.visuals().striped {
434            color_picker = Some(Box::new(striped_row_color));
435        }
436
437        let id = ui.make_persistent_id(id_salt);
438        let prev_state = State::load(ui.ctx(), id);
439
440        // Each grid cell is aligned LEFT_CENTER.
441        // If somebody wants to wrap more things inside a cell,
442        // then we should pick a default layout that matches that alignment,
443        // which we do here:
444        let max_rect = ui.cursor().intersect(ui.max_rect());
445
446        let mut ui_builder = UiBuilder::new().max_rect(max_rect);
447        if prev_state.is_none() {
448            // The initial frame will be glitchy, because we don't know the sizes of things to come.
449
450            if ui.is_visible() {
451                // Try to cover up the glitchy initial frame:
452                ui.ctx().request_discard("new Grid");
453            }
454
455            // Hide the ui this frame, and make things as narrow as possible:
456            ui_builder = ui_builder.sizing_pass().invisible();
457        }
458
459        ui.allocate_new_ui(ui_builder, |ui| {
460            ui.horizontal(|ui| {
461                let is_color = color_picker.is_some();
462                let grid = GridLayout {
463                    num_columns,
464                    color_picker,
465                    min_cell_size: vec2(min_col_width, min_row_height),
466                    max_cell_size,
467                    spacing,
468                    row: start_row,
469                    ..GridLayout::new(ui, id, prev_state)
470                };
471
472                // paint first incoming row
473                if is_color {
474                    let cursor = ui.cursor();
475                    let painter = ui.painter();
476                    grid.paint_row(&cursor, painter);
477                }
478
479                ui.set_grid(grid);
480                let r = add_contents(ui);
481                ui.save_grid();
482                r
483            })
484            .inner
485        })
486    }
487}
488
489fn striped_row_color(row: usize, style: &Style) -> Option<Color32> {
490    if row % 2 == 1 {
491        return Some(style.visuals.faint_bg_color);
492    }
493    None
494}