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 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
47type 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 is_first_frame: bool,
59
60 prev_state: State,
63
64 curr_state: State,
66 initial_available: Rect,
67
68 num_columns: Option<usize>,
70 spacing: Vec2,
71 min_cell_size: Vec2,
72 max_cell_size: Vec2,
73 color_picker: Option<ColorPickerFn>,
74
75 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 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 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 self.max_cell_size.x
150 } else {
151 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 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 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 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 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); 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#[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 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 #[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 #[inline]
333 pub fn num_columns(mut self, num_columns: usize) -> Self {
334 self.num_columns = Some(num_columns);
335 self
336 }
337
338 pub fn striped(self, striped: bool) -> Self {
343 if striped {
344 self.with_row_color(striped_row_color)
345 } else {
346 self.with_row_color(|_row: usize, _style: &Style| None)
350 }
351 }
352
353 #[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 #[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 #[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 #[inline]
379 pub fn spacing(mut self, spacing: impl Into<Vec2>) -> Self {
380 self.spacing = Some(spacing.into());
381 self
382 }
383
384 #[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 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 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 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}