1use std::sync::Arc;
2
3use emath::GuiRounding as _;
4
5use crate::{
6 Align2, AsIdSalt, Color32, Context, Id, IdSalt, InnerResponse, NumExt as _, Painter, Rect,
7 Region, Style, Ui, 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 sizing_pass: bool,
82
83 num_columns: Option<usize>,
85 spacing: Vec2,
86 min_cell_size: Vec2,
87 max_cell_size: Vec2,
88 color_picker: Option<ColorPickerFn>,
89
90 col: usize,
92 row: usize,
93}
94
95impl GridLayout {
96 pub(crate) fn new(ui: &Ui, id: Id, prev_state: Option<State>) -> Self {
97 let is_first_frame = prev_state.is_none();
98
99 let sizing_pass = ui.is_sizing_pass();
101
102 let prev_state = prev_state.unwrap_or_default();
103
104 let initial_available = ui.placer().max_rect().intersect(ui.cursor());
107 debug_assert!(
108 initial_available.min.x.is_finite(),
109 "Grid not yet available for right-to-left layouts"
110 );
111
112 ui.ctx().check_for_id_clash(id, initial_available, "Grid");
113
114 Self {
115 ctx: ui.ctx().clone(),
116 style: Arc::clone(ui.style()),
117 id,
118 is_first_frame,
119 prev_state,
120 curr_state: State::default(),
121 initial_available,
122 sizing_pass,
123
124 num_columns: None,
125 spacing: ui.spacing().item_spacing,
126 min_cell_size: ui.spacing().interact_size,
127 max_cell_size: Vec2::INFINITY,
128 color_picker: None,
129
130 col: 0,
131 row: 0,
132 }
133 }
134}
135
136impl GridLayout {
137 fn prev_col_width(&self, col: usize) -> f32 {
138 self.prev_state
139 .col_width(col)
140 .unwrap_or(self.min_cell_size.x)
141 }
142
143 fn prev_row_height(&self, row: usize) -> f32 {
144 self.prev_state
145 .row_height(row)
146 .unwrap_or(self.min_cell_size.y)
147 }
148
149 pub(crate) fn wrap_text(&self) -> bool {
150 self.max_cell_size.x.is_finite()
151 }
152
153 pub(crate) fn available_rect(&self, region: &Region) -> Rect {
154 let is_last_column = Some(self.col + 1) == self.num_columns;
155
156 let width = if is_last_column {
157 if self.is_first_frame {
160 self.curr_state
161 .col_width(self.col)
162 .unwrap_or(self.min_cell_size.x)
163 } else {
164 (self.initial_available.right() - region.cursor.left())
165 .at_most(self.max_cell_size.x)
166 }
167 } else if self.max_cell_size.x.is_finite() {
168 self.max_cell_size.x
170 } else {
171 self.prev_state
174 .col_width(self.col)
175 .or_else(|| self.curr_state.col_width(self.col))
176 .unwrap_or(self.min_cell_size.x)
177 };
178
179 let width = width.max(self.curr_state.col_width(self.col).unwrap_or(0.0));
181
182 let available = region.max_rect.intersect(region.cursor);
183
184 let height = region.max_rect.max.y - available.top();
185 let height = height
186 .at_least(self.min_cell_size.y)
187 .at_most(self.max_cell_size.y);
188
189 Rect::from_min_size(available.min, vec2(width, height))
190 }
191
192 pub(crate) fn next_cell(&self, cursor: Rect, child_size: Vec2) -> Rect {
193 let width = if self.sizing_pass {
194 0.0
195 } else {
196 self.prev_state.col_width(self.col).unwrap_or(0.0)
197 };
198 let height = self.prev_row_height(self.row);
199 let size = child_size.max(vec2(width, height));
200 Rect::from_min_size(cursor.min, size).round_ui()
201 }
202
203 #[expect(clippy::unused_self)]
204 pub(crate) fn align_size_within_rect(&self, size: Vec2, frame: Rect) -> Rect {
205 Align2::LEFT_CENTER
207 .align_size_within_rect(size, frame)
208 .round_ui()
209 }
210
211 pub(crate) fn justify_and_align(&self, frame: Rect, size: Vec2) -> Rect {
212 self.align_size_within_rect(size, frame)
213 }
214
215 pub(crate) fn advance(&mut self, cursor: &mut Rect, _frame_rect: Rect, widget_rect: Rect) {
216 #[cfg(debug_assertions)]
217 {
218 let debug_expand_width = self.style.debug.show_expand_width;
219 let debug_expand_height = self.style.debug.show_expand_height;
220 if debug_expand_width || debug_expand_height {
221 let rect = widget_rect;
222 let too_wide = rect.width() > self.prev_col_width(self.col);
223 let too_high = rect.height() > self.prev_row_height(self.row);
224
225 if (debug_expand_width && too_wide) || (debug_expand_height && too_high) {
226 let painter = self.ctx.debug_painter();
227 painter.rect_stroke(
228 rect,
229 0.0,
230 (1.0, Color32::LIGHT_BLUE),
231 crate::StrokeKind::Inside,
232 );
233
234 let stroke = Stroke::new(2.5, Color32::from_rgb(200, 0, 0));
235 let paint_line_seg = |a, b| painter.line_segment([a, b], stroke);
236
237 if debug_expand_width && too_wide {
238 paint_line_seg(rect.left_top(), rect.left_bottom());
239 paint_line_seg(rect.left_center(), rect.right_center());
240 paint_line_seg(rect.right_top(), rect.right_bottom());
241 }
242 }
243 }
244 }
245
246 self.curr_state
247 .set_min_col_width(self.col, widget_rect.width().max(self.min_cell_size.x));
248 self.curr_state
249 .set_min_row_height(self.row, widget_rect.height().max(self.min_cell_size.y));
250
251 cursor.min.x += self.prev_col_width(self.col) + self.spacing.x;
252 self.col += 1;
253 }
254
255 fn paint_row(&self, cursor: &Rect, painter: &Painter) {
256 let Some(color_picker) = self.color_picker.as_ref() else {
258 return;
259 };
260 let Some(row_color) = color_picker(self.row, &self.style) else {
261 return;
262 };
263 let Some(height) = self.prev_state.row_height(self.row) else {
264 return;
265 };
266 let size = Vec2::new(self.prev_state.full_width(self.spacing.x), height);
268 let rect = Rect::from_min_size(cursor.min, size);
269 let rect = rect.expand2(0.5 * self.spacing.y * Vec2::Y);
270 let rect = rect.expand2(2.0 * Vec2::X); painter.rect_filled(rect, 2.0, row_color);
273 }
274
275 pub(crate) fn end_row(&mut self, cursor: &mut Rect, painter: &Painter) {
276 cursor.min.x = self.initial_available.min.x;
277 cursor.min.y += self.spacing.y;
278 cursor.min.y += self
279 .curr_state
280 .row_height(self.row)
281 .unwrap_or(self.min_cell_size.y);
282
283 self.col = 0;
284 self.row += 1;
285
286 self.paint_row(cursor, painter);
287 }
288
289 pub(crate) fn save(&self) {
290 if self.curr_state != self.prev_state || self.is_first_frame {
293 self.curr_state.clone().store(&self.ctx, self.id);
294 self.ctx.request_repaint();
295 }
296 }
297}
298
299#[must_use = "You should call .show()"]
328pub struct Grid {
329 id_salt: IdSalt,
330 num_columns: Option<usize>,
331 min_col_width: Option<f32>,
332 min_row_height: Option<f32>,
333 max_cell_size: Vec2,
334 spacing: Option<Vec2>,
335 start_row: usize,
336 color_picker: Option<ColorPickerFn>,
337}
338
339impl Grid {
340 pub fn new(id_salt: impl AsIdSalt) -> Self {
342 Self {
343 id_salt: IdSalt::new(id_salt),
344 num_columns: None,
345 min_col_width: None,
346 min_row_height: None,
347 max_cell_size: Vec2::INFINITY,
348 spacing: None,
349 start_row: 0,
350 color_picker: None,
351 }
352 }
353
354 #[inline]
356 pub fn with_row_color<F>(mut self, color_picker: F) -> Self
357 where
358 F: Send + Sync + Fn(usize, &Style) -> Option<Color32> + 'static,
359 {
360 self.color_picker = Some(Box::new(color_picker));
361 self
362 }
363
364 #[inline]
366 pub fn num_columns(mut self, num_columns: usize) -> Self {
367 self.num_columns = Some(num_columns);
368 self
369 }
370
371 pub fn striped(self, striped: bool) -> Self {
376 if striped {
377 self.with_row_color(striped_row_color)
378 } else {
379 self.with_row_color(|_row: usize, _style: &Style| None)
383 }
384 }
385
386 #[inline]
389 pub fn min_col_width(mut self, min_col_width: f32) -> Self {
390 self.min_col_width = Some(min_col_width);
391 self
392 }
393
394 #[inline]
397 pub fn min_row_height(mut self, min_row_height: f32) -> Self {
398 self.min_row_height = Some(min_row_height);
399 self
400 }
401
402 #[inline]
404 pub fn max_col_width(mut self, max_col_width: f32) -> Self {
405 self.max_cell_size.x = max_col_width;
406 self
407 }
408
409 #[inline]
412 pub fn spacing(mut self, spacing: impl Into<Vec2>) -> Self {
413 self.spacing = Some(spacing.into());
414 self
415 }
416
417 #[inline]
420 pub fn start_row(mut self, start_row: usize) -> Self {
421 self.start_row = start_row;
422 self
423 }
424}
425
426impl Grid {
427 pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
428 self.show_dyn(ui, Box::new(add_contents))
429 }
430
431 fn show_dyn<'c, R>(
432 self,
433 ui: &mut Ui,
434 add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
435 ) -> InnerResponse<R> {
436 let Self {
437 id_salt,
438 num_columns,
439 min_col_width,
440 min_row_height,
441 max_cell_size,
442 spacing,
443 start_row,
444 mut color_picker,
445 } = self;
446 let min_col_width = min_col_width.unwrap_or_else(|| ui.spacing().interact_size.x);
447 let min_row_height = min_row_height.unwrap_or_else(|| ui.spacing().interact_size.y);
448 let spacing = spacing.unwrap_or_else(|| ui.spacing().item_spacing);
449 if color_picker.is_none() && ui.visuals().striped {
450 color_picker = Some(Box::new(striped_row_color));
451 }
452
453 let id = ui.make_persistent_id(id_salt);
454 let prev_state = State::load(ui.ctx(), id);
455
456 let max_rect = ui.cursor().intersect(ui.max_rect());
461
462 let mut ui_builder = UiBuilder::new().max_rect(max_rect);
463 if prev_state.is_none() {
464 if ui.is_visible() {
467 ui.request_discard("new Grid");
469 }
470
471 ui_builder = ui_builder.sizing_pass().invisible();
473 }
474
475 ui.scope_builder(ui_builder, |ui| {
476 ui.horizontal(|ui| {
477 let is_color = color_picker.is_some();
478 let grid = GridLayout {
479 num_columns,
480 color_picker,
481 min_cell_size: vec2(min_col_width, min_row_height),
482 max_cell_size,
483 spacing,
484 row: start_row,
485 ..GridLayout::new(ui, id, prev_state)
486 };
487
488 if is_color {
490 let cursor = ui.cursor();
491 let painter = ui.painter();
492 grid.paint_row(&cursor, painter);
493 }
494
495 ui.set_grid(grid);
496 let r = add_contents(ui);
497 ui.save_grid();
498 r
499 })
500 .inner
501 })
502 }
503}
504
505fn striped_row_color(row: usize, style: &Style) -> Option<Color32> {
506 if row % 2 == 1 {
507 return Some(style.visuals.faint_bg_color);
508 }
509 None
510}