1use alloc::{vec, vec::Vec};
4use core::num::NonZeroUsize;
5use nami::{Computed, SignalExt};
6use num_traits::ToPrimitive;
7use waterui_core::{
8 AnyView, Environment, IntoSignalF32, View, layout::LayoutInvalidationCallback, view::TupleViews,
9};
10
11use crate::{
12 Layout, PlacedSubview, Point, ProposalSize, Rect, Size, SubView, ViewDimensions,
13 container::FixedContainer,
14 stack::{Alignment, HorizontalAlignment, VerticalAlignment},
15};
16
17struct ChildMeasurement {
19 dimensions: ViewDimensions,
20}
21
22struct GridMeasurement {
23 measurements: Vec<ChildMeasurement>,
24 column_widths: Vec<f32>,
25 row_heights: Vec<f32>,
26 total_width: f32,
27 total_height: f32,
28}
29
30#[derive(Debug, Clone)]
32pub struct GridLayout {
33 columns: NonZeroUsize,
34 spacing: Computed<Size>,
35 alignment: Alignment,
36}
37
38impl GridLayout {
39 #[must_use]
41 pub fn new(columns: NonZeroUsize, spacing: Size, alignment: Alignment) -> Self {
42 Self {
43 columns,
44 spacing: Computed::constant(spacing),
45 alignment,
46 }
47 }
48
49 fn measure_grid(
50 &self,
51 proposed_width: Option<f32>,
52 children: &[&dyn SubView],
53 ) -> GridMeasurement {
54 let num_columns = self.columns.get();
55 let num_rows = children.len().div_ceil(num_columns);
56 let spacing = nami::Signal::get(&self.spacing);
57
58 let constrained_width = proposed_width
59 .filter(|width| width.is_finite())
60 .map(|width| width.max(0.0));
61
62 let mut column_widths = constrained_width.map_or_else(
63 || vec![0.0; num_columns],
64 |width| {
65 let total_spacing = spacing.width * usize_to_f32(num_columns.saturating_sub(1));
66 let width_per_column =
67 ((width - total_spacing) / usize_to_f32(num_columns)).max(0.0);
68 vec![width_per_column; num_columns]
69 },
70 );
71
72 if constrained_width.is_none() {
73 for (index, child) in children.iter().enumerate() {
74 let intrinsic_size = child.measure(ProposalSize::new(None, None)).size;
75 if intrinsic_size.width.is_finite() {
76 let column_index = index % num_columns;
77 column_widths[column_index] =
78 column_widths[column_index].max(intrinsic_size.width.max(0.0));
79 }
80 }
81 }
82
83 let measurements: Vec<ChildMeasurement> = children
84 .iter()
85 .enumerate()
86 .map(|(index, child)| {
87 let column_index = index % num_columns;
88 let child_proposal = ProposalSize::new(Some(column_widths[column_index]), None);
89 ChildMeasurement {
90 dimensions: child.measure(child_proposal),
91 }
92 })
93 .collect();
94
95 let row_heights: Vec<f32> = measurements
96 .chunks(num_columns)
97 .map(|row_children| {
98 row_children
99 .iter()
100 .map(|measurement| measurement.dimensions.size.height)
101 .filter(|height| height.is_finite())
102 .fold(0.0, f32::max)
103 })
104 .collect();
105
106 let total_height = row_heights.iter().sum::<f32>()
107 + spacing.height * usize_to_f32(num_rows.saturating_sub(1));
108
109 let total_width = constrained_width.map_or_else(
110 || {
111 let used_columns = children.len().min(num_columns);
112 let total_spacing = spacing.width * usize_to_f32(used_columns.saturating_sub(1));
113 column_widths.iter().take(used_columns).sum::<f32>() + total_spacing
114 },
115 |width| width.max(0.0),
116 );
117
118 GridMeasurement {
119 measurements,
120 column_widths,
121 row_heights,
122 total_width,
123 total_height,
124 }
125 }
126}
127
128#[allow(clippy::cast_precision_loss)]
129impl Layout for GridLayout {
130 fn size_that_fits(&self, proposal: ProposalSize, children: &[&dyn SubView]) -> Size {
131 if children.is_empty() {
132 return Size::zero();
133 }
134
135 let measurement = self.measure_grid(proposal.width, children);
136 Size::new(measurement.total_width, measurement.total_height)
137 }
138
139 fn place(&self, bounds: Rect, children: &[&dyn SubView]) -> Vec<Rect> {
140 if children.is_empty() {
141 return vec![];
142 }
143
144 let num_columns = self.columns.get();
145 let spacing = nami::Signal::get(&self.spacing);
146 let measurement = self.measure_grid(Some(bounds.width()), children);
147
148 let mut placements = Vec::with_capacity(children.len());
149 let mut cursor_y = bounds.y();
150
151 for (row_index, row_measurements) in
152 measurement.measurements.chunks(num_columns).enumerate()
153 {
154 let row_height = measurement
155 .row_heights
156 .get(row_index)
157 .copied()
158 .unwrap_or(0.0);
159 let mut cursor_x = bounds.x();
160
161 for (column_index, child_measurement) in row_measurements.iter().enumerate() {
162 let column_width = measurement
163 .column_widths
164 .get(column_index)
165 .copied()
166 .unwrap_or(0.0);
167 let cell_frame = Rect::new(
168 Point::new(cursor_x, cursor_y),
169 Size::new(column_width, row_height),
170 );
171
172 let child_width = if child_measurement.dimensions.size.width.is_infinite() {
174 column_width
175 } else {
176 child_measurement.dimensions.size.width
177 };
178
179 let child_height = if child_measurement.dimensions.size.height.is_infinite() {
180 row_height
181 } else {
182 child_measurement.dimensions.size.height
183 };
184
185 let child_size = Size::new(child_width, child_height);
186 let mut adjusted_dimensions = child_measurement.dimensions.clone();
187 adjusted_dimensions.size = child_size;
188
189 let horizontal = self.alignment.horizontal();
191 let target_x = if horizontal == HorizontalAlignment::Leading {
192 0.0
193 } else if horizontal == HorizontalAlignment::Trailing {
194 cell_frame.width()
195 } else if horizontal == HorizontalAlignment::Center {
196 cell_frame.width() * 0.5
197 } else {
198 adjusted_dimensions
199 .horizontal(horizontal)
200 .clamp(0.0, child_size.width)
201 };
202 let child_x = cell_frame.x() + target_x
203 - adjusted_dimensions
204 .horizontal(horizontal)
205 .clamp(0.0, child_size.width);
206
207 let vertical = self.alignment.vertical();
208 let target_y = if vertical == VerticalAlignment::Top {
209 0.0
210 } else if vertical == VerticalAlignment::Bottom {
211 cell_frame.height()
212 } else if vertical == VerticalAlignment::Center {
213 cell_frame.height() * 0.5
214 } else {
215 adjusted_dimensions
216 .vertical(vertical)
217 .clamp(0.0, child_size.height)
218 };
219 let child_y = cell_frame.y() + target_y
220 - adjusted_dimensions
221 .vertical(vertical)
222 .clamp(0.0, child_size.height);
223
224 placements.push(Rect::new(Point::new(child_x, child_y), child_size));
225
226 cursor_x += column_width + spacing.width;
227 }
228
229 cursor_y += row_height + spacing.height;
230 }
231
232 placements
233 }
234
235 fn explicit_horizontal(
236 &self,
237 alignment: HorizontalAlignment,
238 _bounds: Rect,
239 children: &[PlacedSubview<'_>],
240 ) -> Option<f32> {
241 if alignment == self.alignment.horizontal() {
242 return children
243 .iter()
244 .filter_map(|child| child.explicit_horizontal(alignment))
245 .min_by(f32::total_cmp);
246 }
247 None
248 }
249
250 fn explicit_vertical(
251 &self,
252 alignment: VerticalAlignment,
253 _bounds: Rect,
254 children: &[PlacedSubview<'_>],
255 ) -> Option<f32> {
256 if alignment == VerticalAlignment::LastBaseline {
257 return children
258 .iter()
259 .filter_map(|child| child.explicit_vertical(alignment))
260 .max_by(f32::total_cmp);
261 }
262 if alignment == VerticalAlignment::FirstBaseline || alignment == self.alignment.vertical() {
263 return children
264 .iter()
265 .filter_map(|child| child.explicit_vertical(alignment))
266 .min_by(f32::total_cmp);
267 }
268 None
269 }
270
271 fn watch_invalidation(
272 &self,
273 invalidate: LayoutInvalidationCallback,
274 ) -> Vec<nami::watcher::BoxWatcherGuard> {
275 vec![nami::Signal::watch(&self.spacing, move |_| invalidate())]
276 }
277}
278
279fn usize_to_f32(value: usize) -> f32 {
280 value
281 .to_f32()
282 .expect("GridLayout: index must be representable as f32")
283}
284
285#[derive(Debug)]
292pub struct GridRow {
293 pub(crate) contents: Vec<AnyView>,
294}
295
296impl GridRow {
297 pub fn new(contents: impl TupleViews) -> Self {
299 Self {
300 contents: contents.into_views(),
301 }
302 }
303}
304
305#[derive(Debug)]
338pub struct Grid {
339 layout: GridLayout,
340 rows: Vec<GridRow>,
341}
342
343impl Grid {
344 pub fn new(columns: usize, rows: impl IntoIterator<Item = GridRow>) -> Self {
353 Self {
354 layout: GridLayout::new(
355 NonZeroUsize::new(columns).expect("Grid columns must be greater than 0"),
356 Size::new(8.0, 8.0), Alignment::Center, ),
359 rows: rows.into_iter().collect(),
360 }
361 }
362
363 #[must_use]
368 pub fn spacing(mut self, spacing: impl IntoSignalF32 + 'static) -> Self {
369 self.layout.spacing = spacing
370 .into_signal_f32()
371 .map(|spacing| Size::new(spacing, spacing))
372 .computed();
373 self
374 }
375
376 #[must_use]
378 pub const fn alignment(mut self, alignment: Alignment) -> Self {
379 self.layout.alignment = alignment;
380 self
381 }
382}
383
384impl View for Grid {
385 fn body(self, _env: &Environment) -> impl View {
386 let flattened_children = self
389 .rows
390 .into_iter()
391 .flat_map(|row| row.contents)
392 .collect::<Vec<AnyView>>();
393
394 FixedContainer::new(self.layout, flattened_children)
395 }
396}
397
398pub fn grid(columns: usize, rows: impl IntoIterator<Item = GridRow>) -> Grid {
406 Grid::new(columns, rows)
407}
408
409pub fn row(contents: impl TupleViews) -> GridRow {
413 GridRow::new(contents)
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419 use crate::StretchAxis;
420 use core::num::NonZeroUsize;
421
422 struct MockSubView {
423 size: Size,
424 }
425
426 impl SubView for MockSubView {
427 fn measure(&self, _proposal: ProposalSize) -> ViewDimensions {
428 ViewDimensions::new(self.size)
429 }
430 fn stretch_axis(&self) -> StretchAxis {
431 StretchAxis::None
432 }
433 fn priority(&self) -> i32 {
434 0
435 }
436 }
437
438 #[test]
439 fn test_grid_size_2x2() {
440 let layout = GridLayout::new(
441 NonZeroUsize::new(2).unwrap(),
442 Size::new(10.0, 10.0),
443 Alignment::Center,
444 );
445
446 let mut child1 = MockSubView {
447 size: Size::new(50.0, 30.0),
448 };
449 let mut child2 = MockSubView {
450 size: Size::new(50.0, 40.0),
451 };
452 let mut child3 = MockSubView {
453 size: Size::new(50.0, 20.0),
454 };
455 let mut child4 = MockSubView {
456 size: Size::new(50.0, 50.0),
457 };
458
459 let children: Vec<&dyn SubView> = vec![&mut child1, &mut child2, &mut child3, &mut child4];
460
461 let size = layout.size_that_fits(ProposalSize::new(Some(200.0), None), &children);
462
463 assert!((size.width - 200.0).abs() < f32::EPSILON);
465 assert!((size.height - 100.0).abs() < f32::EPSILON);
467 }
468
469 #[test]
470 fn test_grid_placement() {
471 let layout = GridLayout::new(
472 NonZeroUsize::new(2).unwrap(),
473 Size::new(10.0, 10.0),
474 Alignment::TopLeading,
475 );
476
477 let mut child1 = MockSubView {
478 size: Size::new(40.0, 30.0),
479 };
480 let mut child2 = MockSubView {
481 size: Size::new(40.0, 30.0),
482 };
483
484 let children: Vec<&dyn SubView> = vec![&mut child1, &mut child2];
485
486 let bounds = Rect::new(Point::new(0.0, 0.0), Size::new(100.0, 100.0));
487 let rects = layout.place(bounds, &children);
488
489 assert!((rects[0].x() - 0.0).abs() < f32::EPSILON);
492 assert!((rects[0].y() - 0.0).abs() < f32::EPSILON);
493
494 assert!((rects[1].x() - 55.0).abs() < f32::EPSILON);
496 assert!((rects[1].y() - 0.0).abs() < f32::EPSILON);
497 }
498
499 #[test]
500 fn test_grid_intrinsic_width_when_unconstrained() {
501 let layout = GridLayout::new(
502 NonZeroUsize::new(2).unwrap(),
503 Size::new(10.0, 10.0),
504 Alignment::Center,
505 );
506
507 let mut child1 = MockSubView {
508 size: Size::new(30.0, 20.0),
509 };
510 let mut child2 = MockSubView {
511 size: Size::new(50.0, 25.0),
512 };
513 let mut child3 = MockSubView {
514 size: Size::new(40.0, 35.0),
515 };
516 let mut child4 = MockSubView {
517 size: Size::new(20.0, 30.0),
518 };
519
520 let children: Vec<&dyn SubView> = vec![&mut child1, &mut child2, &mut child3, &mut child4];
521
522 let size = layout.size_that_fits(ProposalSize::new(None, None), &children);
523
524 assert!((size.width - 100.0).abs() < f32::EPSILON);
528 assert!((size.height - 70.0).abs() < f32::EPSILON);
530 }
531
532 #[test]
533 fn test_grid_placement_with_infinite_bounds_uses_intrinsic_width() {
534 let layout = GridLayout::new(
535 NonZeroUsize::new(2).unwrap(),
536 Size::new(10.0, 10.0),
537 Alignment::TopLeading,
538 );
539
540 let mut child1 = MockSubView {
541 size: Size::new(40.0, 20.0),
542 };
543 let mut child2 = MockSubView {
544 size: Size::new(20.0, 20.0),
545 };
546
547 let children: Vec<&dyn SubView> = vec![&mut child1, &mut child2];
548
549 let bounds = Rect::new(Point::new(5.0, 7.0), Size::new(f32::INFINITY, 100.0));
550 let rects = layout.place(bounds, &children);
551
552 assert_eq!(rects.len(), 2);
553 assert!((rects[0].x() - 5.0).abs() < f32::EPSILON);
554 assert!((rects[0].y() - 7.0).abs() < f32::EPSILON);
555 assert!((rects[0].width() - 40.0).abs() < f32::EPSILON);
556 assert!((rects[1].x() - 55.0).abs() < f32::EPSILON);
557 assert!((rects[1].y() - 7.0).abs() < f32::EPSILON);
558 assert!((rects[1].width() - 20.0).abs() < f32::EPSILON);
559 }
560}