Skip to main content

sequoia_layout/
wrap.rs

1//! Line wrapping for flow containers.
2
3use crate::container::{Align, Axis};
4use crate::dock::sanitize_gap;
5
6/// Wrapping configuration for children along a main axis.
7#[derive(Clone, Copy, Debug, PartialEq)]
8pub struct WrapStyle {
9    /// Main axis along which children are placed.
10    pub axis: Axis,
11    /// Cross-axis alignment of the items within each line.
12    pub align: Align,
13    /// Gap between items on the same line.
14    pub column_gap: f32,
15    /// Gap between wrapped lines.
16    pub row_gap: f32,
17    /// Explicit number of columns, overriding width-based wrapping.
18    pub columns: Option<usize>,
19    /// Explicit number of rows, overriding width-based wrapping.
20    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    /// Creates a wrapping style for the given main axis.
38    pub fn new(axis: Axis) -> Self {
39        Self {
40            axis,
41            ..Self::default()
42        }
43    }
44
45    /// Returns the gap used between items along the main axis.
46    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    /// Returns the gap used between wrapped lines.
54    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    /// Sets both the column and row gap.
62    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    /// Sets the gap between items on the same line.
69    pub fn column_gap(mut self, gap: f32) -> Self {
70        self.column_gap = sanitize_gap(gap);
71        self
72    }
73
74    /// Sets the gap between wrapped lines.
75    pub fn row_gap(mut self, gap: f32) -> Self {
76        self.row_gap = sanitize_gap(gap);
77        self
78    }
79
80    /// Sets the cross-axis alignment of items within a line.
81    pub fn align(mut self, align: Align) -> Self {
82        self.align = align;
83        self
84    }
85
86    /// Sets the main axis and reinterprets the main and cross gaps.
87    pub fn axis(mut self, axis: Axis) -> Self {
88        self.axis = axis;
89        self
90    }
91
92    /// Forces the layout to use exactly `columns` columns.
93    ///
94    /// Values below one are clamped to one. For a horizontal axis this is the
95    /// number of items per line; for a vertical axis it is the number of
96    /// lines.
97    pub fn columns(mut self, columns: usize) -> Self {
98        self.columns = Some(columns.max(1));
99        self
100    }
101
102    /// Forces the layout to use exactly `rows` rows.
103    ///
104    /// Values below one are clamped to one. For a vertical axis this is the
105    /// number of items per line; for a horizontal axis it is the number of
106    /// lines.
107    pub fn rows(mut self, rows: usize) -> Self {
108        self.rows = Some(rows.max(1));
109        self
110    }
111
112    /// Removes the explicit column count and restores width-based wrapping.
113    pub fn clear_columns(mut self) -> Self {
114        self.columns = None;
115        self
116    }
117
118    /// Removes the explicit row count and restores width-based wrapping.
119    pub fn clear_rows(mut self) -> Self {
120        self.rows = None;
121        self
122    }
123
124    /// Returns how many items fit on one line when an explicit count is set.
125    ///
126    /// The count matching the main axis wins: `columns` for a horizontal
127    /// axis and `rows` for a vertical axis. The other count is treated as the
128    /// desired number of lines.
129    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    /// Splits `main_sizes` into lines using the explicit counts when present.
142    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
152/// Splits main-axis sizes into lines that fit within `avail_main`.
153///
154/// Returns half-open index ranges `(start, end)` for each line. `gap` is
155/// counted between adjacent items. An unbounded axis keeps every item on a
156/// single line.
157pub 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
190/// Splits items into lines with an optional fixed number of items per line.
191///
192/// When `per_line` is `None` or zero this falls back to [`wrap_lines`].
193pub 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}