1use crate::container::Align;
9use crate::dock::sanitize_gap;
10
11#[derive(Clone, Copy, Debug, PartialEq)]
13pub struct MasonryStyle {
14 pub columns: usize,
16 pub column_gap: f32,
18 pub row_gap: f32,
20 pub align: Align,
22}
23
24impl Default for MasonryStyle {
25 fn default() -> Self {
26 Self {
27 columns: 2,
28 column_gap: 0.0,
29 row_gap: 0.0,
30 align: Align::Stretch,
31 }
32 }
33}
34
35impl MasonryStyle {
36 pub fn new(columns: usize) -> Self {
38 Self {
39 columns: columns.max(1),
40 ..Self::default()
41 }
42 }
43
44 pub fn columns(mut self, columns: usize) -> Self {
46 self.columns = columns.max(1);
47 self
48 }
49
50 pub fn gap(mut self, gap: f32) -> Self {
52 let gap = sanitize_gap(gap);
53 self.column_gap = gap;
54 self.row_gap = gap;
55 self
56 }
57
58 pub fn column_gap(mut self, gap: f32) -> Self {
60 self.column_gap = sanitize_gap(gap);
61 self
62 }
63
64 pub fn row_gap(mut self, gap: f32) -> Self {
66 self.row_gap = sanitize_gap(gap);
67 self
68 }
69
70 pub fn align_items(mut self, align: Align) -> Self {
72 self.align = align;
73 self
74 }
75}
76
77#[derive(Clone, Copy, Debug, Default, PartialEq)]
79pub struct MasonryPlacement {
80 pub column: usize,
82 pub top: f32,
84}
85
86pub fn masonry_column_width(available: f32, columns: usize, gap: f32) -> f32 {
91 let columns = columns.max(1);
92 let gap = sanitize_gap(gap);
93 if !available.is_finite() {
94 return 0.0;
95 }
96 let total_gap = gap * columns.saturating_sub(1) as f32;
97 ((available.max(0.0) - total_gap) / columns as f32).max(0.0)
98}
99
100pub fn masonry_assign(
106 heights: &[f32],
107 columns: usize,
108 row_gap: f32,
109) -> (Vec<MasonryPlacement>, f32) {
110 let columns = columns.max(1);
111 let row_gap = sanitize_gap(row_gap);
112 let mut bottoms = vec![0.0_f32; columns];
113 let mut counts = vec![0usize; columns];
114 let mut placements = Vec::with_capacity(heights.len());
115
116 for &raw_height in heights {
117 let height = if raw_height.is_finite() {
118 raw_height.max(0.0)
119 } else {
120 0.0
121 };
122 let mut column = 0usize;
123 for candidate in 1..columns {
124 if bottoms[candidate] < bottoms[column] {
125 column = candidate;
126 }
127 }
128 let top = if counts[column] == 0 {
129 0.0
130 } else {
131 bottoms[column] + row_gap
132 };
133 bottoms[column] = top + height;
134 counts[column] += 1;
135 placements.push(MasonryPlacement { column, top });
136 }
137
138 let height = bottoms.into_iter().fold(0.0_f32, f32::max);
139 (placements, height)
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 fn approx(a: f32, b: f32) -> bool {
147 (a - b).abs() < 0.01
148 }
149
150 #[test]
151 fn column_width_accounts_for_gaps() {
152 assert!(approx(masonry_column_width(110.0, 2, 10.0), 50.0));
153 assert!(approx(masonry_column_width(100.0, 3, 10.0), 80.0 / 3.0));
154 assert_eq!(masonry_column_width(100.0, 0, 10.0), 100.0);
155 }
156
157 #[test]
158 fn column_width_is_zero_when_unbounded_or_too_small() {
159 assert_eq!(masonry_column_width(f32::INFINITY, 2, 10.0), 0.0);
160 assert_eq!(masonry_column_width(10.0, 3, 10.0), 0.0);
161 }
162
163 #[test]
164 fn items_fill_the_shortest_column_in_order() {
165 let (placements, height) = masonry_assign(&[100.0, 50.0, 80.0, 120.0, 30.0], 2, 10.0);
166 assert_eq!(
167 placements,
168 vec![
169 MasonryPlacement {
170 column: 0,
171 top: 0.0
172 },
173 MasonryPlacement {
174 column: 1,
175 top: 0.0
176 },
177 MasonryPlacement {
178 column: 1,
179 top: 60.0
180 },
181 MasonryPlacement {
182 column: 0,
183 top: 110.0
184 },
185 MasonryPlacement {
186 column: 1,
187 top: 150.0
188 },
189 ]
190 );
191 assert!(approx(height, 230.0));
192 }
193
194 #[test]
195 fn ties_use_the_lowest_column_index() {
196 let (placements, height) = masonry_assign(&[10.0, 10.0, 10.0, 10.0], 3, 0.0);
197 assert_eq!(
198 placements
199 .iter()
200 .map(|placement| placement.column)
201 .collect::<Vec<_>>(),
202 vec![0, 1, 2, 0]
203 );
204 assert!(approx(height, 20.0));
205 }
206
207 #[test]
208 fn first_item_in_each_column_has_no_leading_gap() {
209 let (placements, height) = masonry_assign(&[20.0, 20.0, 20.0], 2, 6.0);
210 assert_eq!(placements[0].top, 0.0);
211 assert_eq!(placements[1].top, 0.0);
212 assert_eq!(placements[2].top, 26.0);
213 assert!(approx(height, 46.0));
214 }
215
216 #[test]
217 fn invalid_values_are_sanitized() {
218 let (placements, height) = masonry_assign(&[f32::NAN, -5.0, 10.0], 0, f32::NAN);
219 assert_eq!(placements.len(), 3);
220 assert!(placements.iter().all(|placement| placement.column == 0));
221 assert_eq!(placements[0].top, 0.0);
222 assert_eq!(placements[1].top, 0.0);
223 assert_eq!(placements[2].top, 0.0);
224 assert!(approx(height, 10.0));
225 }
226
227 #[test]
228 fn empty_input_has_no_placements_or_height() {
229 let (placements, height) = masonry_assign(&[], 3, 8.0);
230 assert!(placements.is_empty());
231 assert_eq!(height, 0.0);
232 }
233
234 #[test]
235 fn style_builders_sanitize_values() {
236 let style = MasonryStyle::new(0)
237 .gap(f32::NAN)
238 .column_gap(-1.0)
239 .row_gap(4.0)
240 .columns(3);
241 assert_eq!(style.columns, 3);
242 assert_eq!(style.column_gap, 0.0);
243 assert_eq!(style.row_gap, 4.0);
244 }
245}