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}
18
19impl Default for WrapStyle {
20    fn default() -> Self {
21        Self {
22            axis: Axis::Horizontal,
23            align: Align::Start,
24            column_gap: 0.0,
25            row_gap: 0.0,
26        }
27    }
28}
29
30impl WrapStyle {
31    /// Creates a wrapping style for the given main axis.
32    pub fn new(axis: Axis) -> Self {
33        Self {
34            axis,
35            ..Self::default()
36        }
37    }
38
39    /// Returns the gap used between items along the main axis.
40    pub fn main_gap(self) -> f32 {
41        match self.axis {
42            Axis::Horizontal => self.column_gap,
43            Axis::Vertical => self.row_gap,
44        }
45    }
46
47    /// Returns the gap used between wrapped lines.
48    pub fn cross_gap(self) -> f32 {
49        match self.axis {
50            Axis::Horizontal => self.row_gap,
51            Axis::Vertical => self.column_gap,
52        }
53    }
54
55    /// Sets both the column and row gap.
56    pub fn gap(mut self, gap: f32) -> Self {
57        self.column_gap = sanitize_gap(gap);
58        self.row_gap = sanitize_gap(gap);
59        self
60    }
61
62    /// Sets the gap between items on the same line.
63    pub fn column_gap(mut self, gap: f32) -> Self {
64        self.column_gap = sanitize_gap(gap);
65        self
66    }
67
68    /// Sets the gap between wrapped lines.
69    pub fn row_gap(mut self, gap: f32) -> Self {
70        self.row_gap = sanitize_gap(gap);
71        self
72    }
73
74    /// Sets the cross-axis alignment of items within a line.
75    pub fn align(mut self, align: Align) -> Self {
76        self.align = align;
77        self
78    }
79
80    /// Sets the main axis and reinterprets the main and cross gaps.
81    pub fn axis(mut self, axis: Axis) -> Self {
82        self.axis = axis;
83        self
84    }
85}
86
87/// Splits main-axis sizes into lines that fit within `avail_main`.
88///
89/// Returns half-open index ranges `(start, end)` for each line. `gap` is
90/// counted between adjacent items. An unbounded axis keeps every item on a
91/// single line.
92pub fn wrap_lines(main_sizes: &[f32], avail_main: f32, gap: f32) -> Vec<(usize, usize)> {
93    let mut lines = Vec::new();
94    if main_sizes.is_empty() {
95        return lines;
96    }
97    let gap = sanitize_gap(gap);
98    let avail = if avail_main.is_finite() {
99        avail_main.max(0.0)
100    } else {
101        f32::INFINITY
102    };
103
104    let mut start = 0usize;
105    let mut used = 0.0_f32;
106    for (i, &raw) in main_sizes.iter().enumerate() {
107        let size = if raw.is_finite() { raw.max(0.0) } else { 0.0 };
108        if i == start {
109            used = size;
110            continue;
111        }
112        let next = used + gap + size;
113        if next > avail {
114            lines.push((start, i));
115            start = i;
116            used = size;
117        } else {
118            used = next;
119        }
120    }
121    lines.push((start, main_sizes.len()));
122    lines
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn empty_input_has_no_lines() {
131        assert!(wrap_lines(&[], 100.0, 0.0).is_empty());
132    }
133
134    #[test]
135    fn children_that_fit_stay_on_one_line() {
136        let lines = wrap_lines(&[30.0, 30.0, 30.0], 100.0, 0.0);
137        assert_eq!(lines, vec![(0, 3)]);
138    }
139
140    #[test]
141    fn overflow_wraps_to_the_next_line() {
142        let lines = wrap_lines(&[40.0, 40.0, 40.0], 100.0, 0.0);
143        assert_eq!(lines, vec![(0, 2), (2, 3)]);
144    }
145
146    #[test]
147    fn gap_counts_toward_the_line_width() {
148        assert_eq!(
149            wrap_lines(&[40.0, 40.0, 40.0], 100.0, 10.0),
150            vec![(0, 2), (2, 3)]
151        );
152
153        assert_eq!(
154            wrap_lines(&[40.0, 40.0, 40.0], 80.0, 10.0),
155            vec![(0, 1), (1, 2), (2, 3)]
156        );
157    }
158
159    #[test]
160    fn wider_than_the_line_gets_its_own_line() {
161        let lines = wrap_lines(&[200.0, 30.0], 100.0, 0.0);
162        assert_eq!(lines, vec![(0, 1), (1, 2)]);
163    }
164
165    #[test]
166    fn unbounded_axis_keeps_everything_on_one_line() {
167        let lines = wrap_lines(&[500.0, 500.0, 500.0], f32::INFINITY, 8.0);
168        assert_eq!(lines, vec![(0, 3)]);
169    }
170
171    #[test]
172    fn gaps_sanitize() {
173        let s = WrapStyle::new(Axis::Horizontal).gap(f32::NAN);
174        assert_eq!(s.column_gap, 0.0);
175        assert_eq!(s.row_gap, 0.0);
176        let s = s.row_gap(-5.0).column_gap(4.0);
177        assert_eq!(s.row_gap, 0.0);
178        assert_eq!(s.column_gap, 4.0);
179    }
180
181    #[test]
182    fn main_and_cross_gaps_follow_the_axis() {
183        let s = WrapStyle::new(Axis::Horizontal)
184            .column_gap(6.0)
185            .row_gap(10.0);
186        assert_eq!(s.main_gap(), 6.0);
187        assert_eq!(s.cross_gap(), 10.0);
188        let s = s.axis(Axis::Vertical);
189        assert_eq!(s.main_gap(), 10.0);
190        assert_eq!(s.cross_gap(), 6.0);
191    }
192}