Skip to main content

sequoia_layout/
flex.rs

1//! Flex grow, shrink, and justification along a container's main axis.
2
3use crate::container::Justify;
4
5/// Flex parameters for one item along a container's main axis.
6#[derive(Clone, Copy, Debug, PartialEq)]
7pub struct FlexItem {
8    /// Base size before growing or shrinking.
9    pub base: f32,
10    /// Relative weight used to claim free space.
11    pub grow: f32,
12    /// Relative weight used to absorb a deficit.
13    pub shrink: f32,
14    /// Minimum size the item may shrink to.
15    pub min: f32,
16    /// Maximum size the item may grow to.
17    pub max: f32,
18}
19
20impl FlexItem {
21    /// Creates a sanitized flex item.
22    ///
23    /// Non-finite or negative values are clamped: `base` and `min` to zero,
24    /// `grow` to zero, `shrink` to one, and `max` becomes infinity when it is
25    /// NaN or positive infinity.
26    pub fn new(base: f32, grow: f32, shrink: f32, min: f32, max: f32) -> Self {
27        Self {
28            base: if base.is_finite() { base.max(0.0) } else { 0.0 },
29            grow: if grow.is_finite() && grow > 0.0 {
30                grow
31            } else {
32                0.0
33            },
34            shrink: if shrink.is_finite() && shrink >= 0.0 {
35                shrink
36            } else {
37                1.0
38            },
39            min: if min.is_finite() { min.max(0.0) } else { 0.0 },
40            max: if max.is_nan() {
41                f32::INFINITY
42            } else if max.is_infinite() {
43                if max > 0.0 { f32::INFINITY } else { 0.0 }
44            } else {
45                max.max(0.0)
46            },
47        }
48    }
49
50    fn clamp(self, v: f32) -> f32 {
51        let (mn, mx) = if self.min > self.max {
52            (self.max, self.max)
53        } else {
54            (self.min, self.max)
55        };
56        v.clamp(mn, mx)
57    }
58}
59
60/// Distributes `avail_main` among `sizes` according to the flex items.
61///
62/// `sizes` initially holds the base sizes and is updated in place. `gaps` is
63/// the total space consumed by gaps between items. Items grow until their max
64/// and shrink until their min; any remaining space is passed to siblings.
65pub fn distribute_flex(items: &[FlexItem], sizes: &mut [f32], gaps: f32, avail_main: f32) {
66    assert_eq!(items.len(), sizes.len());
67    if items.is_empty() {
68        return;
69    }
70    for (item, size) in items.iter().zip(sizes.iter_mut()) {
71        if !size.is_finite() {
72            *size = 0.0;
73        }
74        *size = item.clamp(*size);
75    }
76    if !avail_main.is_finite() || avail_main.is_nan() {
77        return;
78    }
79    let gaps = if gaps.is_finite() { gaps.max(0.0) } else { 0.0 };
80    const EPS: f32 = 0.001;
81
82    for _ in 0..items.len() + 1 {
83        let used: f32 = sizes.iter().sum::<f32>() + gaps;
84        let free = avail_main - used;
85        if free.abs() <= EPS || free.is_nan() {
86            break;
87        }
88        if free > 0.0 {
89            let mut sum_grow = 0.0;
90            for (item, size) in items.iter().zip(sizes.iter()) {
91                if item.grow > 0.0 && *size < item.max - EPS {
92                    sum_grow += item.grow;
93                }
94            }
95            if !(sum_grow > 0.0) {
96                break;
97            }
98            for (item, size) in items.iter().zip(sizes.iter_mut()) {
99                if item.grow > 0.0 && *size < item.max - EPS {
100                    *size = item.clamp(*size + free * item.grow / sum_grow);
101                }
102            }
103        } else {
104            let mut deficit = -free;
105            let mut changed = false;
106            for pass in [true, false] {
107                if deficit <= EPS {
108                    break;
109                }
110                let mut pool = 0.0;
111                for (item, size) in items.iter().zip(sizes.iter()) {
112                    let is_flex = item.grow > 0.0;
113                    if is_flex == pass && *size > item.min + EPS {
114                        pool += item.shrink * *size;
115                    }
116                }
117
118                if !pass {
119                    let flex_ready = items
120                        .iter()
121                        .zip(sizes.iter())
122                        .any(|(it, s)| it.grow > 0.0 && *s > it.min + EPS);
123                    if flex_ready {
124                        continue;
125                    }
126                }
127                if !(pool > 0.0) {
128                    continue;
129                }
130                let take = pool.min(deficit * items_scale(items, sizes, pass));
131
132                for (item, size) in items.iter().zip(sizes.iter_mut()) {
133                    let is_flex = item.grow > 0.0;
134                    if is_flex == pass && *size > item.min + EPS {
135                        let w = item.shrink * *size;
136                        let cut = take * w / pool;
137                        let before = *size;
138                        *size = item.clamp(*size - cut);
139                        changed |= (*size - before).abs() > f32::EPSILON;
140                    }
141                }
142                let used2: f32 = sizes.iter().sum::<f32>() + gaps;
143                deficit = (used2 - avail_main).max(0.0);
144            }
145            // A clamped item changed the pool, so the next outer pass can still
146            // absorb a remaining deficit on siblings that are above their min.
147            // Without this break, a container whose children are already at
148            // their minimum (for example 3000 fixed text rows that do not fit)
149            // keeps re-scanning every child until `items.len() + 1` passes,
150            // which turns one layout pass into O(n^2).
151            if !changed {
152                break;
153            }
154        }
155    }
156}
157
158fn items_scale(_items: &[FlexItem], _sizes: &[f32], _flex_pass: bool) -> f32 {
159    1.0
160}
161
162/// Computes the leading offset and inter-item gap for a justified line.
163///
164/// Returns `(start_offset, added_gap)`; `used` is the space already occupied
165/// by items and `avail` is the available main-axis space.
166pub fn justify_layout(
167    justify: Justify,
168    count: usize,
169    base_gap: f32,
170    used: f32,
171    avail: f32,
172) -> (f32, f32) {
173    let extra = if avail.is_finite() {
174        (avail - used).max(0.0)
175    } else {
176        0.0
177    };
178    let _ = base_gap;
179    justify.distribute(count, base_gap, extra)
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    fn approx(a: f32, b: f32) -> bool {
187        (a - b).abs() < 0.05
188    }
189
190    #[test]
191    fn grow_splits_free_space() {
192        let items = [
193            FlexItem::new(100.0, 1.0, 1.0, 0.0, f32::INFINITY),
194            FlexItem::new(50.0, 3.0, 1.0, 0.0, f32::INFINITY),
195        ];
196        let mut sizes = [100.0, 50.0];
197        distribute_flex(&items, &mut sizes, 0.0, 400.0);
198        assert!(approx(sizes[0] + sizes[1], 400.0));
199        assert!(approx(sizes[0], 162.5));
200        assert!(approx(sizes[1], 237.5));
201    }
202
203    #[test]
204    fn max_cap_overflow_goes_to_sibling() {
205        let items = [
206            FlexItem::new(0.0, 1.0, 1.0, 0.0, 200.0),
207            FlexItem::new(0.0, 1.0, 1.0, 0.0, f32::INFINITY),
208        ];
209        let mut sizes = [0.0, 0.0];
210        distribute_flex(&items, &mut sizes, 0.0, 400.0);
211        assert!(approx(sizes[0], 200.0), "{sizes:?}");
212        assert!(approx(sizes[1], 200.0), "{sizes:?}");
213    }
214
215    #[test]
216    fn shrink_prefers_scaled_weight() {
217        let items = [
218            FlexItem::new(100.0, 1.0, 1.0, 0.0, f32::INFINITY),
219            FlexItem::new(100.0, 1.0, 1.0, 0.0, f32::INFINITY),
220        ];
221        let mut sizes = [100.0, 100.0];
222        distribute_flex(&items, &mut sizes, 0.0, 120.0);
223        assert!(approx(sizes[0] + sizes[1], 120.0), "{sizes:?}");
224    }
225
226    #[test]
227    fn zero_shrink_item_keeps_size() {
228        let items = [
229            FlexItem::new(100.0, 0.0, 0.0, 0.0, f32::INFINITY),
230            FlexItem::new(100.0, 1.0, 1.0, 0.0, f32::INFINITY),
231        ];
232        let mut sizes = [100.0, 100.0];
233        distribute_flex(&items, &mut sizes, 0.0, 120.0);
234        assert!(approx(sizes[0], 100.0), "{sizes:?}");
235        assert!(approx(sizes[1], 20.0), "{sizes:?}");
236    }
237}