Skip to main content

egui/
grid.rs

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