1use crate::container::{Align, Axis};
4use crate::dock::sanitize_gap;
5
6#[derive(Clone, Copy, Debug, PartialEq)]
8pub struct WrapStyle {
9 pub axis: Axis,
11 pub align: Align,
13 pub column_gap: f32,
15 pub row_gap: f32,
17 pub columns: Option<usize>,
19 pub rows: Option<usize>,
21}
22
23impl Default for WrapStyle {
24 fn default() -> Self {
25 Self {
26 axis: Axis::Horizontal,
27 align: Align::Start,
28 column_gap: 0.0,
29 row_gap: 0.0,
30 columns: None,
31 rows: None,
32 }
33 }
34}
35
36impl WrapStyle {
37 pub fn new(axis: Axis) -> Self {
39 Self {
40 axis,
41 ..Self::default()
42 }
43 }
44
45 pub fn main_gap(self) -> f32 {
47 match self.axis {
48 Axis::Horizontal => self.column_gap,
49 Axis::Vertical => self.row_gap,
50 }
51 }
52
53 pub fn cross_gap(self) -> f32 {
55 match self.axis {
56 Axis::Horizontal => self.row_gap,
57 Axis::Vertical => self.column_gap,
58 }
59 }
60
61 pub fn gap(mut self, gap: f32) -> Self {
63 self.column_gap = sanitize_gap(gap);
64 self.row_gap = sanitize_gap(gap);
65 self
66 }
67
68 pub fn column_gap(mut self, gap: f32) -> Self {
70 self.column_gap = sanitize_gap(gap);
71 self
72 }
73
74 pub fn row_gap(mut self, gap: f32) -> Self {
76 self.row_gap = sanitize_gap(gap);
77 self
78 }
79
80 pub fn align(mut self, align: Align) -> Self {
82 self.align = align;
83 self
84 }
85
86 pub fn axis(mut self, axis: Axis) -> Self {
88 self.axis = axis;
89 self
90 }
91
92 pub fn columns(mut self, columns: usize) -> Self {
98 self.columns = Some(columns.max(1));
99 self
100 }
101
102 pub fn rows(mut self, rows: usize) -> Self {
108 self.rows = Some(rows.max(1));
109 self
110 }
111
112 pub fn clear_columns(mut self) -> Self {
114 self.columns = None;
115 self
116 }
117
118 pub fn clear_rows(mut self) -> Self {
120 self.rows = None;
121 self
122 }
123
124 pub fn items_per_line(self, count: usize) -> Option<usize> {
130 if count == 0 {
131 return None;
132 }
133 let columns = self.columns.filter(|&n| n > 0);
134 let rows = self.rows.filter(|&n| n > 0);
135 match self.axis {
136 Axis::Horizontal => columns.or_else(|| rows.map(|rows| count.div_ceil(rows))),
137 Axis::Vertical => rows.or_else(|| columns.map(|columns| count.div_ceil(columns))),
138 }
139 }
140
141 pub fn lines(self, main_sizes: &[f32], avail_main: f32) -> Vec<(usize, usize)> {
143 wrap_lines_counted(
144 main_sizes,
145 avail_main,
146 self.main_gap(),
147 self.items_per_line(main_sizes.len()),
148 )
149 }
150}
151
152pub fn wrap_lines(main_sizes: &[f32], avail_main: f32, gap: f32) -> Vec<(usize, usize)> {
158 let mut lines = Vec::new();
159 if main_sizes.is_empty() {
160 return lines;
161 }
162 let gap = sanitize_gap(gap);
163 let avail = if avail_main.is_finite() {
164 avail_main.max(0.0)
165 } else {
166 f32::INFINITY
167 };
168
169 let mut start = 0usize;
170 let mut used = 0.0_f32;
171 for (i, &raw) in main_sizes.iter().enumerate() {
172 let size = if raw.is_finite() { raw.max(0.0) } else { 0.0 };
173 if i == start {
174 used = size;
175 continue;
176 }
177 let next = used + gap + size;
178 if next > avail {
179 lines.push((start, i));
180 start = i;
181 used = size;
182 } else {
183 used = next;
184 }
185 }
186 lines.push((start, main_sizes.len()));
187 lines
188}
189
190pub fn wrap_lines_counted(
194 main_sizes: &[f32],
195 avail_main: f32,
196 gap: f32,
197 per_line: Option<usize>,
198) -> Vec<(usize, usize)> {
199 let Some(per_line) = per_line.filter(|&n| n > 0) else {
200 return wrap_lines(main_sizes, avail_main, gap);
201 };
202 if main_sizes.is_empty() {
203 return Vec::new();
204 }
205 (0..main_sizes.len())
206 .step_by(per_line)
207 .map(|start| (start, (start + per_line).min(main_sizes.len())))
208 .collect()
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 #[test]
216 fn empty_input_has_no_lines() {
217 assert!(wrap_lines(&[], 100.0, 0.0).is_empty());
218 }
219
220 #[test]
221 fn children_that_fit_stay_on_one_line() {
222 let lines = wrap_lines(&[30.0, 30.0, 30.0], 100.0, 0.0);
223 assert_eq!(lines, vec![(0, 3)]);
224 }
225
226 #[test]
227 fn overflow_wraps_to_the_next_line() {
228 let lines = wrap_lines(&[40.0, 40.0, 40.0], 100.0, 0.0);
229 assert_eq!(lines, vec![(0, 2), (2, 3)]);
230 }
231
232 #[test]
233 fn gap_counts_toward_the_line_width() {
234 assert_eq!(
235 wrap_lines(&[40.0, 40.0, 40.0], 100.0, 10.0),
236 vec![(0, 2), (2, 3)]
237 );
238
239 assert_eq!(
240 wrap_lines(&[40.0, 40.0, 40.0], 80.0, 10.0),
241 vec![(0, 1), (1, 2), (2, 3)]
242 );
243 }
244
245 #[test]
246 fn wider_than_the_line_gets_its_own_line() {
247 let lines = wrap_lines(&[200.0, 30.0], 100.0, 0.0);
248 assert_eq!(lines, vec![(0, 1), (1, 2)]);
249 }
250
251 #[test]
252 fn unbounded_axis_keeps_everything_on_one_line() {
253 let lines = wrap_lines(&[500.0, 500.0, 500.0], f32::INFINITY, 8.0);
254 assert_eq!(lines, vec![(0, 3)]);
255 }
256
257 #[test]
258 fn gaps_sanitize() {
259 let s = WrapStyle::new(Axis::Horizontal).gap(f32::NAN);
260 assert_eq!(s.column_gap, 0.0);
261 assert_eq!(s.row_gap, 0.0);
262 let s = s.row_gap(-5.0).column_gap(4.0);
263 assert_eq!(s.row_gap, 0.0);
264 assert_eq!(s.column_gap, 4.0);
265 }
266
267 #[test]
268 fn main_and_cross_gaps_follow_the_axis() {
269 let s = WrapStyle::new(Axis::Horizontal)
270 .column_gap(6.0)
271 .row_gap(10.0);
272 assert_eq!(s.main_gap(), 6.0);
273 assert_eq!(s.cross_gap(), 10.0);
274 let s = s.axis(Axis::Vertical);
275 assert_eq!(s.main_gap(), 10.0);
276 assert_eq!(s.cross_gap(), 6.0);
277 }
278
279 #[test]
280 fn explicit_columns_override_the_available_width() {
281 let sizes = [10.0; 7];
282 let s = WrapStyle::new(Axis::Horizontal).columns(3);
283 assert_eq!(s.lines(&sizes, 1000.0), vec![(0, 3), (3, 6), (6, 7)]);
284 }
285
286 #[test]
287 fn explicit_rows_limit_the_number_of_lines() {
288 let sizes = [10.0; 7];
289 let s = WrapStyle::new(Axis::Horizontal).rows(2);
290 assert_eq!(s.lines(&sizes, 1000.0), vec![(0, 4), (4, 7)]);
291 }
292
293 #[test]
294 fn vertical_axis_maps_rows_to_items_per_line() {
295 let sizes = [10.0; 7];
296 let s = WrapStyle::new(Axis::Vertical).rows(3);
297 assert_eq!(s.lines(&sizes, 1000.0), vec![(0, 3), (3, 6), (6, 7)]);
298 }
299
300 #[test]
301 fn counts_clamp_to_at_least_one() {
302 let s = WrapStyle::new(Axis::Horizontal).columns(0).rows(0);
303 assert_eq!(s.columns, Some(1));
304 assert_eq!(s.rows, Some(1));
305 assert_eq!(s.items_per_line(3), Some(1));
306 }
307
308 #[test]
309 fn clearing_counts_restores_width_based_wrapping() {
310 let sizes = [40.0, 40.0, 40.0];
311 let s = WrapStyle::new(Axis::Horizontal).columns(1).clear_columns();
312 assert_eq!(s.columns, None);
313 assert_eq!(s.lines(&sizes, 100.0), vec![(0, 2), (2, 3)]);
314 }
315
316 #[test]
317 fn counted_wrap_with_no_items_is_empty() {
318 assert!(wrap_lines_counted(&[], 100.0, 0.0, Some(3)).is_empty());
319 }
320}