1use alloc::{vec, vec::Vec};
4use core::num::NonZeroUsize;
5use waterui_core::{AnyView, Environment, View, view::TupleViews};
6
7use crate::{
8 Layout, Point, ProposalSize, Rect, Size, SubView,
9 container::FixedContainer,
10 stack::{Alignment, HorizontalAlignment, VerticalAlignment},
11};
12
13struct ChildMeasurement {
15 size: Size,
16}
17
18#[derive(Debug, Clone, PartialEq, PartialOrd)]
20pub struct GridLayout {
21 columns: NonZeroUsize,
22 spacing: Size, alignment: Alignment,
24}
25
26impl GridLayout {
27 #[must_use]
29 pub const fn new(columns: NonZeroUsize, spacing: Size, alignment: Alignment) -> Self {
30 Self {
31 columns,
32 spacing,
33 alignment,
34 }
35 }
36}
37
38#[allow(clippy::cast_precision_loss)]
39impl Layout for GridLayout {
40 fn size_that_fits(&self, proposal: ProposalSize, children: &[&dyn SubView]) -> Size {
41 if children.is_empty() {
42 return Size::zero();
43 }
44
45 let num_columns = self.columns.get();
46 let num_rows = children.len().div_ceil(num_columns);
47
48 let child_width = proposal.width.map(|w| {
51 let total_spacing = self.spacing.width * (num_columns - 1) as f32;
52 ((w - total_spacing) / num_columns as f32).max(0.0)
53 });
54
55 let child_proposal = ProposalSize::new(child_width, None);
58
59 let measurements: Vec<ChildMeasurement> = children
60 .iter()
61 .map(|child| ChildMeasurement {
62 size: child.size_that_fits(child_proposal),
63 })
64 .collect();
65
66 let mut total_height = 0.0;
68 for row_children in measurements.chunks(num_columns) {
69 let row_height = row_children
70 .iter()
71 .map(|m| m.size.height)
72 .filter(|h| h.is_finite())
73 .fold(0.0, f32::max);
74 total_height += row_height;
75 }
76
77 total_height += self.spacing.height * (num_rows.saturating_sub(1) as f32);
78
79 let final_width = proposal.width.unwrap_or(0.0);
81
82 Size::new(final_width, total_height)
83 }
84
85 fn place(&self, bounds: Rect, children: &[&dyn SubView]) -> Vec<Rect> {
86 if children.is_empty() || !bounds.width().is_finite() {
87 return vec![Rect::new(Point::zero(), Size::zero()); children.len()];
89 }
90
91 let num_columns = self.columns.get();
92
93 let total_h_spacing = self.spacing.width * (num_columns - 1) as f32;
95 let column_width = ((bounds.width() - total_h_spacing) / num_columns as f32).max(0.0);
96
97 let child_proposal = ProposalSize::new(Some(column_width), None);
99
100 let measurements: Vec<ChildMeasurement> = children
101 .iter()
102 .map(|child| ChildMeasurement {
103 size: child.size_that_fits(child_proposal),
104 })
105 .collect();
106
107 let row_heights: Vec<f32> = measurements
109 .chunks(num_columns)
110 .map(|row_children| {
111 row_children
112 .iter()
113 .map(|m| m.size.height)
114 .filter(|h| h.is_finite())
115 .fold(0.0, f32::max)
116 })
117 .collect();
118
119 let mut placements = Vec::with_capacity(children.len());
120 let mut cursor_y = bounds.y();
121
122 for (row_index, row_measurements) in measurements.chunks(num_columns).enumerate() {
123 let row_height = row_heights.get(row_index).copied().unwrap_or(0.0);
124 let mut cursor_x = bounds.x();
125
126 for measurement in row_measurements {
127 let cell_frame = Rect::new(
128 Point::new(cursor_x, cursor_y),
129 Size::new(column_width, row_height),
130 );
131
132 let child_width = if measurement.size.width.is_infinite() {
134 column_width
135 } else {
136 measurement.size.width
137 };
138
139 let child_height = if measurement.size.height.is_infinite() {
140 row_height
141 } else {
142 measurement.size.height
143 };
144
145 let child_size = Size::new(child_width, child_height);
146
147 let child_x = match self.alignment.horizontal() {
149 HorizontalAlignment::Leading => cell_frame.x(),
150 HorizontalAlignment::Center => {
151 cell_frame.x() + (cell_frame.width() - child_size.width) / 2.0
152 }
153 HorizontalAlignment::Trailing => cell_frame.max_x() - child_size.width,
154 };
155
156 let child_y = match self.alignment.vertical() {
157 VerticalAlignment::Top => cell_frame.y(),
158 VerticalAlignment::Center => {
159 cell_frame.y() + (cell_frame.height() - child_size.height) / 2.0
160 }
161 VerticalAlignment::Bottom => cell_frame.max_y() - child_size.height,
162 };
163
164 placements.push(Rect::new(Point::new(child_x, child_y), child_size));
165
166 cursor_x += column_width + self.spacing.width;
167 }
168
169 cursor_y += row_height + self.spacing.height;
170 }
171
172 placements
173 }
174}
175
176#[derive(Debug)]
183pub struct GridRow {
184 pub(crate) contents: Vec<AnyView>,
185}
186
187impl GridRow {
188 pub fn new(contents: impl TupleViews) -> Self {
190 Self {
191 contents: contents.into_views(),
192 }
193 }
194}
195
196#[derive(Debug)]
221pub struct Grid {
222 layout: GridLayout,
223 rows: Vec<GridRow>,
224}
225
226impl Grid {
227 pub fn new(columns: usize, rows: impl IntoIterator<Item = GridRow>) -> Self {
236 Self {
237 layout: GridLayout::new(
238 NonZeroUsize::new(columns).expect("Grid columns must be greater than 0"),
239 Size::new(8.0, 8.0), Alignment::Center, ),
242 rows: rows.into_iter().collect(),
243 }
244 }
245
246 #[must_use]
248 pub const fn spacing(mut self, spacing: f32) -> Self {
249 self.layout.spacing = Size::new(spacing, spacing);
250 self
251 }
252
253 #[must_use]
255 pub const fn alignment(mut self, alignment: Alignment) -> Self {
256 self.layout.alignment = alignment;
257 self
258 }
259}
260
261impl View for Grid {
262 fn body(self, _env: &Environment) -> impl View {
263 let flattened_children = self
266 .rows
267 .into_iter()
268 .flat_map(|row| row.contents)
269 .collect::<Vec<AnyView>>();
270
271 FixedContainer::new(self.layout, flattened_children)
272 }
273}
274
275pub fn grid(columns: usize, rows: impl IntoIterator<Item = GridRow>) -> Grid {
283 Grid::new(columns, rows)
284}
285
286pub fn row(contents: impl TupleViews) -> GridRow {
290 GridRow::new(contents)
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296 use crate::StretchAxis;
297 use core::num::NonZeroUsize;
298
299 struct MockSubView {
300 size: Size,
301 }
302
303 impl SubView for MockSubView {
304 fn size_that_fits(&self, _proposal: ProposalSize) -> Size {
305 self.size
306 }
307 fn stretch_axis(&self) -> StretchAxis {
308 StretchAxis::None
309 }
310 fn priority(&self) -> i32 {
311 0
312 }
313 }
314
315 #[test]
316 fn test_grid_size_2x2() {
317 let layout = GridLayout::new(
318 NonZeroUsize::new(2).unwrap(),
319 Size::new(10.0, 10.0),
320 Alignment::Center,
321 );
322
323 let mut child1 = MockSubView {
324 size: Size::new(50.0, 30.0),
325 };
326 let mut child2 = MockSubView {
327 size: Size::new(50.0, 40.0),
328 };
329 let mut child3 = MockSubView {
330 size: Size::new(50.0, 20.0),
331 };
332 let mut child4 = MockSubView {
333 size: Size::new(50.0, 50.0),
334 };
335
336 let children: Vec<&dyn SubView> = vec![&mut child1, &mut child2, &mut child3, &mut child4];
337
338 let size = layout.size_that_fits(ProposalSize::new(Some(200.0), None), &children);
339
340 assert!((size.width - 200.0).abs() < f32::EPSILON);
342 assert!((size.height - 100.0).abs() < f32::EPSILON);
344 }
345
346 #[test]
347 fn test_grid_placement() {
348 let layout = GridLayout::new(
349 NonZeroUsize::new(2).unwrap(),
350 Size::new(10.0, 10.0),
351 Alignment::TopLeading,
352 );
353
354 let mut child1 = MockSubView {
355 size: Size::new(40.0, 30.0),
356 };
357 let mut child2 = MockSubView {
358 size: Size::new(40.0, 30.0),
359 };
360
361 let children: Vec<&dyn SubView> = vec![&mut child1, &mut child2];
362
363 let bounds = Rect::new(Point::new(0.0, 0.0), Size::new(100.0, 100.0));
364 let rects = layout.place(bounds, &children);
365
366 assert!((rects[0].x() - 0.0).abs() < f32::EPSILON);
369 assert!((rects[0].y() - 0.0).abs() < f32::EPSILON);
370
371 assert!((rects[1].x() - 55.0).abs() < f32::EPSILON);
373 assert!((rects[1].y() - 0.0).abs() < f32::EPSILON);
374 }
375}