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