Skip to main content

egui/
grid.rs

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