egui/
grid.rs

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