egui/
grid.rs

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