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 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
53type 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 is_first_frame: bool,
65
66 prev_state: State,
69
70 curr_state: State,
72 initial_available: Rect,
73
74 num_columns: Option<usize>,
76 spacing: Vec2,
77 min_cell_size: Vec2,
78 max_cell_size: Vec2,
79 color_picker: Option<ColorPickerFn>,
80
81 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 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 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 self.max_cell_size.x
156 } else {
157 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 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 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 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 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); 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 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#[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 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 #[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 #[inline]
341 pub fn num_columns(mut self, num_columns: usize) -> Self {
342 self.num_columns = Some(num_columns);
343 self
344 }
345
346 pub fn striped(self, striped: bool) -> Self {
351 if striped {
352 self.with_row_color(striped_row_color)
353 } else {
354 self.with_row_color(|_row: usize, _style: &Style| None)
358 }
359 }
360
361 #[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 #[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 #[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 #[inline]
387 pub fn spacing(mut self, spacing: impl Into<Vec2>) -> Self {
388 self.spacing = Some(spacing.into());
389 self
390 }
391
392 #[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 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 if ui.is_visible() {
442 ui.ctx().request_discard("new Grid");
444 }
445
446 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 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}