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