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            for pass in [true, false] {
106                if deficit <= EPS {
107                    break;
108                }
109                let mut pool = 0.0;
110                for (item, size) in items.iter().zip(sizes.iter()) {
111                    let is_flex = item.grow > 0.0;
112                    if is_flex == pass && *size > item.min + EPS {
113                        pool += item.shrink * *size;
114                    }
115                }
116
117                if !pass {
118                    let flex_ready = items
119                        .iter()
120                        .zip(sizes.iter())
121                        .any(|(it, s)| it.grow > 0.0 && *s > it.min + EPS);
122                    if flex_ready {
123                        continue;
124                    }
125                }
126                if !(pool > 0.0) {
127                    continue;
128                }
129                let take = pool.min(deficit * items_scale(items, sizes, pass));
130
131                for (item, size) in items.iter().zip(sizes.iter_mut()) {
132                    let is_flex = item.grow > 0.0;
133                    if is_flex == pass && *size > item.min + EPS {
134                        let w = item.shrink * *size;
135                        let cut = take * w / pool;
136                        *size = item.clamp(*size - cut);
137                    }
138                }
139                let used2: f32 = sizes.iter().sum::<f32>() + gaps;
140                deficit = (used2 - avail_main).max(0.0);
141            }
142            // A clamped item changed the pool, so the next outer pass can still
143            // absorb a remaining deficit on siblings that are above their min.
144        }
145    }
146}
147
148fn items_scale(_items: &[FlexItem], _sizes: &[f32], _flex_pass: bool) -> f32 {
149    1.0
150}
151
152/// Computes the leading offset and inter-item gap for a justified line.
153///
154/// Returns `(start_offset, added_gap)`; `used` is the space already occupied
155/// by items and `avail` is the available main-axis space.
156pub fn justify_layout(
157    justify: Justify,
158    count: usize,
159    base_gap: f32,
160    used: f32,
161    avail: f32,
162) -> (f32, f32) {
163    let extra = if avail.is_finite() {
164        (avail - used).max(0.0)
165    } else {
166        0.0
167    };
168    let _ = base_gap;
169    justify.distribute(count, base_gap, extra)
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    fn approx(a: f32, b: f32) -> bool {
177        (a - b).abs() < 0.05
178    }
179
180    #[test]
181    fn grow_splits_free_space() {
182        let items = [
183            FlexItem::new(100.0, 1.0, 1.0, 0.0, f32::INFINITY),
184            FlexItem::new(50.0, 3.0, 1.0, 0.0, f32::INFINITY),
185        ];
186        let mut sizes = [100.0, 50.0];
187        distribute_flex(&items, &mut sizes, 0.0, 400.0);
188        assert!(approx(sizes[0] + sizes[1], 400.0));
189        assert!(approx(sizes[0], 162.5));
190        assert!(approx(sizes[1], 237.5));
191    }
192
193    #[test]
194    fn max_cap_overflow_goes_to_sibling() {
195        let items = [
196            FlexItem::new(0.0, 1.0, 1.0, 0.0, 200.0),
197            FlexItem::new(0.0, 1.0, 1.0, 0.0, f32::INFINITY),
198        ];
199        let mut sizes = [0.0, 0.0];
200        distribute_flex(&items, &mut sizes, 0.0, 400.0);
201        assert!(approx(sizes[0], 200.0), "{sizes:?}");
202        assert!(approx(sizes[1], 200.0), "{sizes:?}");
203    }
204
205    #[test]
206    fn shrink_prefers_scaled_weight() {
207        let items = [
208            FlexItem::new(100.0, 1.0, 1.0, 0.0, f32::INFINITY),
209            FlexItem::new(100.0, 1.0, 1.0, 0.0, f32::INFINITY),
210        ];
211        let mut sizes = [100.0, 100.0];
212        distribute_flex(&items, &mut sizes, 0.0, 120.0);
213        assert!(approx(sizes[0] + sizes[1], 120.0), "{sizes:?}");
214    }
215
216    #[test]
217    fn zero_shrink_item_keeps_size() {
218        let items = [
219            FlexItem::new(100.0, 0.0, 0.0, 0.0, f32::INFINITY),
220            FlexItem::new(100.0, 1.0, 1.0, 0.0, f32::INFINITY),
221        ];
222        let mut sizes = [100.0, 100.0];
223        distribute_flex(&items, &mut sizes, 0.0, 120.0);
224        assert!(approx(sizes[0], 100.0), "{sizes:?}");
225        assert!(approx(sizes[1], 20.0), "{sizes:?}");
226    }
227}