1use emath::GuiRounding as _;
2
3use crate::{
4 vec2, Align2, Color32, Context, Id, InnerResponse, NumExt, Painter, Rect, Region, Style, Ui,
5 UiBuilder, Vec2,
6};
7
8#[cfg(debug_assertions)]
9use crate::Stroke;
10
11#[derive(Clone, Debug, Default, PartialEq)]
12pub(crate) struct State {
13 col_widths: Vec<f32>,
14 row_heights: Vec<f32>,
15}
16
17impl State {
18 pub fn load(ctx: &Context, id: Id) -> Option<Self> {
19 ctx.data_mut(|d| d.get_temp(id))
20 }
21
22 pub fn store(self, ctx: &Context, id: Id) {
23 ctx.data_mut(|d| d.insert_temp(id, self));
27 }
28
29 fn set_min_col_width(&mut self, col: usize, width: f32) {
30 self.col_widths
31 .resize(self.col_widths.len().max(col + 1), 0.0);
32 self.col_widths[col] = self.col_widths[col].max(width);
33 }
34
35 fn set_min_row_height(&mut self, row: usize, height: f32) {
36 self.row_heights
37 .resize(self.row_heights.len().max(row + 1), 0.0);
38 self.row_heights[row] = self.row_heights[row].max(height);
39 }
40
41 fn col_width(&self, col: usize) -> Option<f32> {
42 self.col_widths.get(col).copied()
43 }
44
45 fn row_height(&self, row: usize) -> Option<f32> {
46 self.row_heights.get(row).copied()
47 }
48
49 fn full_width(&self, x_spacing: f32) -> f32 {
50 self.col_widths.iter().sum::<f32>()
51 + (self.col_widths.len().at_least(1) - 1) as f32 * x_spacing
52 }
53}
54
55type ColorPickerFn = Box<dyn Send + Sync + Fn(usize, &Style) -> Option<Color32>>;
59
60pub(crate) struct GridLayout {
61 ctx: Context,
62 style: std::sync::Arc<Style>,
63 id: Id,
64
65 is_first_frame: bool,
67
68 prev_state: State,
71
72 curr_state: State,
74 initial_available: Rect,
75
76 num_columns: Option<usize>,
78 spacing: Vec2,
79 min_cell_size: Vec2,
80 max_cell_size: Vec2,
81 color_picker: Option<ColorPickerFn>,
82
83 col: usize,
85 row: usize,
86}
87
88impl GridLayout {
89 pub(crate) fn new(ui: &Ui, id: Id, prev_state: Option<State>) -> Self {
90 let is_first_frame = prev_state.is_none();
91 let prev_state = prev_state.unwrap_or_default();
92
93 let initial_available = ui.placer().max_rect().intersect(ui.cursor());
96 debug_assert!(
97 initial_available.min.x.is_finite(),
98 "Grid not yet available for right-to-left layouts"
99 );
100
101 ui.ctx().check_for_id_clash(id, initial_available, "Grid");
102
103 Self {
104 ctx: ui.ctx().clone(),
105 style: ui.style().clone(),
106 id,
107 is_first_frame,
108 prev_state,
109 curr_state: State::default(),
110 initial_available,
111
112 num_columns: None,
113 spacing: ui.spacing().item_spacing,
114 min_cell_size: ui.spacing().interact_size,
115 max_cell_size: Vec2::INFINITY,
116 color_picker: None,
117
118 col: 0,
119 row: 0,
120 }
121 }
122}
123
124impl GridLayout {
125 fn prev_col_width(&self, col: usize) -> f32 {
126 self.prev_state
127 .col_width(col)
128 .unwrap_or(self.min_cell_size.x)
129 }
130
131 fn prev_row_height(&self, row: usize) -> f32 {
132 self.prev_state
133 .row_height(row)
134 .unwrap_or(self.min_cell_size.y)
135 }
136
137 pub(crate) fn wrap_text(&self) -> bool {
138 self.max_cell_size.x.is_finite()
139 }
140
141 pub(crate) fn available_rect(&self, region: &Region) -> Rect {
142 let is_last_column = Some(self.col + 1) == self.num_columns;
143
144 let width = if is_last_column {
145 if self.is_first_frame {
148 self.curr_state
149 .col_width(self.col)
150 .unwrap_or(self.min_cell_size.x)
151 } else {
152 (self.initial_available.right() - region.cursor.left())
153 .at_most(self.max_cell_size.x)
154 }
155 } else if self.max_cell_size.x.is_finite() {
156 self.max_cell_size.x
158 } else {
159 self.prev_state
162 .col_width(self.col)
163 .or_else(|| self.curr_state.col_width(self.col))
164 .unwrap_or(self.min_cell_size.x)
165 };
166
167 let width = width.max(self.curr_state.col_width(self.col).unwrap_or(0.0));
169
170 let available = region.max_rect.intersect(region.cursor);
171
172 let height = region.max_rect.max.y - available.top();
173 let height = height
174 .at_least(self.min_cell_size.y)
175 .at_most(self.max_cell_size.y);
176
177 Rect::from_min_size(available.min, vec2(width, height))
178 }
179
180 pub(crate) fn next_cell(&self, cursor: Rect, child_size: Vec2) -> Rect {
181 let width = self.prev_state.col_width(self.col).unwrap_or(0.0);
182 let height = self.prev_row_height(self.row);
183 let size = child_size.max(vec2(width, height));
184 Rect::from_min_size(cursor.min, size).round_ui()
185 }
186
187 #[allow(clippy::unused_self)]
188 pub(crate) fn align_size_within_rect(&self, size: Vec2, frame: Rect) -> Rect {
189 Align2::LEFT_CENTER
191 .align_size_within_rect(size, frame)
192 .round_ui()
193 }
194
195 pub(crate) fn justify_and_align(&self, frame: Rect, size: Vec2) -> Rect {
196 self.align_size_within_rect(size, frame)
197 }
198
199 pub(crate) fn advance(&mut self, cursor: &mut Rect, _frame_rect: Rect, widget_rect: Rect) {
200 #[cfg(debug_assertions)]
201 {
202 let debug_expand_width = self.style.debug.show_expand_width;
203 let debug_expand_height = self.style.debug.show_expand_height;
204 if debug_expand_width || debug_expand_height {
205 let rect = widget_rect;
206 let too_wide = rect.width() > self.prev_col_width(self.col);
207 let too_high = rect.height() > self.prev_row_height(self.row);
208
209 if (debug_expand_width && too_wide) || (debug_expand_height && too_high) {
210 let painter = self.ctx.debug_painter();
211 painter.rect_stroke(
212 rect,
213 0.0,
214 (1.0, Color32::LIGHT_BLUE),
215 crate::StrokeKind::Inside,
216 );
217
218 let stroke = Stroke::new(2.5, Color32::from_rgb(200, 0, 0));
219 let paint_line_seg = |a, b| painter.line_segment([a, b], stroke);
220
221 if debug_expand_width && too_wide {
222 paint_line_seg(rect.left_top(), rect.left_bottom());
223 paint_line_seg(rect.left_center(), rect.right_center());
224 paint_line_seg(rect.right_top(), rect.right_bottom());
225 }
226 }
227 }
228 }
229
230 self.curr_state
231 .set_min_col_width(self.col, widget_rect.width().max(self.min_cell_size.x));
232 self.curr_state
233 .set_min_row_height(self.row, widget_rect.height().max(self.min_cell_size.y));
234
235 cursor.min.x += self.prev_col_width(self.col) + self.spacing.x;
236 self.col += 1;
237 }
238
239 fn paint_row(&self, cursor: &Rect, painter: &Painter) {
240 let Some(color_picker) = self.color_picker.as_ref() else {
242 return;
243 };
244 let Some(row_color) = color_picker(self.row, &self.style) else {
245 return;
246 };
247 let Some(height) = self.prev_state.row_height(self.row) else {
248 return;
249 };
250 let size = Vec2::new(self.prev_state.full_width(self.spacing.x), height);
252 let rect = Rect::from_min_size(cursor.min, size);
253 let rect = rect.expand2(0.5 * self.spacing.y * Vec2::Y);
254 let rect = rect.expand2(2.0 * Vec2::X); painter.rect_filled(rect, 2.0, row_color);
257 }
258
259 pub(crate) fn end_row(&mut self, cursor: &mut Rect, painter: &Painter) {
260 cursor.min.x = self.initial_available.min.x;
261 cursor.min.y += self.spacing.y;
262 cursor.min.y += self
263 .curr_state
264 .row_height(self.row)
265 .unwrap_or(self.min_cell_size.y);
266
267 self.col = 0;
268 self.row += 1;
269
270 self.paint_row(cursor, painter);
271 }
272
273 pub(crate) fn save(&self) {
274 if self.curr_state != self.prev_state || self.is_first_frame {
277 self.curr_state.clone().store(&self.ctx, self.id);
278 self.ctx.request_repaint();
279 }
280 }
281}
282
283#[must_use = "You should call .show()"]
312pub struct Grid {
313 id_salt: Id,
314 num_columns: Option<usize>,
315 min_col_width: Option<f32>,
316 min_row_height: Option<f32>,
317 max_cell_size: Vec2,
318 spacing: Option<Vec2>,
319 start_row: usize,
320 color_picker: Option<ColorPickerFn>,
321}
322
323impl Grid {
324 pub fn new(id_salt: impl std::hash::Hash) -> Self {
326 Self {
327 id_salt: Id::new(id_salt),
328 num_columns: None,
329 min_col_width: None,
330 min_row_height: None,
331 max_cell_size: Vec2::INFINITY,
332 spacing: None,
333 start_row: 0,
334 color_picker: None,
335 }
336 }
337
338 #[inline]
340 pub fn with_row_color<F>(mut self, color_picker: F) -> Self
341 where
342 F: Send + Sync + Fn(usize, &Style) -> Option<Color32> + 'static,
343 {
344 self.color_picker = Some(Box::new(color_picker));
345 self
346 }
347
348 #[inline]
350 pub fn num_columns(mut self, num_columns: usize) -> Self {
351 self.num_columns = Some(num_columns);
352 self
353 }
354
355 pub fn striped(self, striped: bool) -> Self {
360 if striped {
361 self.with_row_color(striped_row_color)
362 } else {
363 self.with_row_color(|_row: usize, _style: &Style| None)
367 }
368 }
369
370 #[inline]
373 pub fn min_col_width(mut self, min_col_width: f32) -> Self {
374 self.min_col_width = Some(min_col_width);
375 self
376 }
377
378 #[inline]
381 pub fn min_row_height(mut self, min_row_height: f32) -> Self {
382 self.min_row_height = Some(min_row_height);
383 self
384 }
385
386 #[inline]
388 pub fn max_col_width(mut self, max_col_width: f32) -> Self {
389 self.max_cell_size.x = max_col_width;
390 self
391 }
392
393 #[inline]
396 pub fn spacing(mut self, spacing: impl Into<Vec2>) -> Self {
397 self.spacing = Some(spacing.into());
398 self
399 }
400
401 #[inline]
404 pub fn start_row(mut self, start_row: usize) -> Self {
405 self.start_row = start_row;
406 self
407 }
408}
409
410impl Grid {
411 pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
412 self.show_dyn(ui, Box::new(add_contents))
413 }
414
415 fn show_dyn<'c, R>(
416 self,
417 ui: &mut Ui,
418 add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
419 ) -> InnerResponse<R> {
420 let Self {
421 id_salt,
422 num_columns,
423 min_col_width,
424 min_row_height,
425 max_cell_size,
426 spacing,
427 start_row,
428 mut color_picker,
429 } = self;
430 let min_col_width = min_col_width.unwrap_or_else(|| ui.spacing().interact_size.x);
431 let min_row_height = min_row_height.unwrap_or_else(|| ui.spacing().interact_size.y);
432 let spacing = spacing.unwrap_or_else(|| ui.spacing().item_spacing);
433 if color_picker.is_none() && ui.visuals().striped {
434 color_picker = Some(Box::new(striped_row_color));
435 }
436
437 let id = ui.make_persistent_id(id_salt);
438 let prev_state = State::load(ui.ctx(), id);
439
440 let max_rect = ui.cursor().intersect(ui.max_rect());
445
446 let mut ui_builder = UiBuilder::new().max_rect(max_rect);
447 if prev_state.is_none() {
448 if ui.is_visible() {
451 ui.ctx().request_discard("new Grid");
453 }
454
455 ui_builder = ui_builder.sizing_pass().invisible();
457 }
458
459 ui.allocate_new_ui(ui_builder, |ui| {
460 ui.horizontal(|ui| {
461 let is_color = color_picker.is_some();
462 let grid = GridLayout {
463 num_columns,
464 color_picker,
465 min_cell_size: vec2(min_col_width, min_row_height),
466 max_cell_size,
467 spacing,
468 row: start_row,
469 ..GridLayout::new(ui, id, prev_state)
470 };
471
472 if is_color {
474 let cursor = ui.cursor();
475 let painter = ui.painter();
476 grid.paint_row(&cursor, painter);
477 }
478
479 ui.set_grid(grid);
480 let r = add_contents(ui);
481 ui.save_grid();
482 r
483 })
484 .inner
485 })
486 }
487}
488
489fn striped_row_color(row: usize, style: &Style) -> Option<Color32> {
490 if row % 2 == 1 {
491 return Some(style.visuals.faint_bg_color);
492 }
493 None
494}