Skip to main content

sequoia_layout/
flex.rs

1use crate::container::Justify;
2
3#[derive(Clone, Copy, Debug, PartialEq)]
4pub struct FlexItem {
5    pub base: f32,
6    pub grow: f32,
7    pub shrink: f32,
8    pub min: f32,
9    pub max: f32,
10}
11
12impl FlexItem {
13    pub fn new(base: f32, grow: f32, shrink: f32, min: f32, max: f32) -> Self {
14        Self {
15            base: if base.is_finite() { base.max(0.0) } else { 0.0 },
16            grow: if grow.is_finite() && grow > 0.0 {
17                grow
18            } else {
19                0.0
20            },
21            shrink: if shrink.is_finite() && shrink >= 0.0 {
22                shrink
23            } else {
24                1.0
25            },
26            min: if min.is_finite() { min.max(0.0) } else { 0.0 },
27            max: if max.is_nan() {
28                f32::INFINITY
29            } else if max.is_infinite() {
30                if max > 0.0 { f32::INFINITY } else { 0.0 }
31            } else {
32                max.max(0.0)
33            },
34        }
35    }
36
37    fn clamp(self, v: f32) -> f32 {
38        let (mn, mx) = if self.min > self.max {
39            (self.max, self.max)
40        } else {
41            (self.min, self.max)
42        };
43        v.clamp(mn, mx)
44    }
45}
46
47pub fn distribute_flex(items: &[FlexItem], sizes: &mut [f32], gaps: f32, avail_main: f32) {
48    assert_eq!(items.len(), sizes.len());
49    if items.is_empty() {
50        return;
51    }
52    for (item, size) in items.iter().zip(sizes.iter_mut()) {
53        if !size.is_finite() {
54            *size = 0.0;
55        }
56        *size = item.clamp(*size);
57    }
58    if !avail_main.is_finite() || avail_main.is_nan() {
59        return;
60    }
61    let gaps = if gaps.is_finite() { gaps.max(0.0) } else { 0.0 };
62    const EPS: f32 = 0.001;
63
64    for _ in 0..items.len() + 1 {
65        let used: f32 = sizes.iter().sum::<f32>() + gaps;
66        let free = avail_main - used;
67        if free.abs() <= EPS || free.is_nan() {
68            break;
69        }
70        if free > 0.0 {
71            let mut sum_grow = 0.0;
72            for (item, size) in items.iter().zip(sizes.iter()) {
73                if item.grow > 0.0 && *size < item.max - EPS {
74                    sum_grow += item.grow;
75                }
76            }
77            if !(sum_grow > 0.0) {
78                break;
79            }
80            for (item, size) in items.iter().zip(sizes.iter_mut()) {
81                if item.grow > 0.0 && *size < item.max - EPS {
82                    *size = item.clamp(*size + free * item.grow / sum_grow);
83                }
84            }
85        } else {
86            let mut deficit = -free;
87            for pass in [true, false] {
88                if deficit <= EPS {
89                    break;
90                }
91                let mut pool = 0.0;
92                for (item, size) in items.iter().zip(sizes.iter()) {
93                    let is_flex = item.grow > 0.0;
94                    if is_flex == pass && *size > item.min + EPS {
95                        pool += item.shrink * *size;
96                    }
97                }
98
99                if !pass {
100                    let flex_ready = items
101                        .iter()
102                        .zip(sizes.iter())
103                        .any(|(it, s)| it.grow > 0.0 && *s > it.min + EPS);
104                    if flex_ready {
105                        continue;
106                    }
107                }
108                if !(pool > 0.0) {
109                    continue;
110                }
111                let take = pool.min(deficit * items_scale(items, sizes, pass));
112
113                for (item, size) in items.iter().zip(sizes.iter_mut()) {
114                    let is_flex = item.grow > 0.0;
115                    if is_flex == pass && *size > item.min + EPS {
116                        let w = item.shrink * *size;
117                        let cut = take * w / pool;
118                        *size = item.clamp(*size - cut);
119                    }
120                }
121                let used2: f32 = sizes.iter().sum::<f32>() + gaps;
122                deficit = (used2 - avail_main).max(0.0);
123            }
124            // A clamped item changed the pool, so the next outer pass can still
125            // absorb a remaining deficit on siblings that are above their min.
126        }
127    }
128}
129
130fn items_scale(_items: &[FlexItem], _sizes: &[f32], _flex_pass: bool) -> f32 {
131    1.0
132}
133
134pub fn justify_layout(
135    justify: Justify,
136    count: usize,
137    base_gap: f32,
138    used: f32,
139    avail: f32,
140) -> (f32, f32) {
141    let extra = if avail.is_finite() {
142        (avail - used).max(0.0)
143    } else {
144        0.0
145    };
146    let _ = base_gap;
147    justify.distribute(count, base_gap, extra)
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    fn approx(a: f32, b: f32) -> bool {
155        (a - b).abs() < 0.05
156    }
157
158    #[test]
159    fn grow_splits_free_space() {
160        let items = [
161            FlexItem::new(100.0, 1.0, 1.0, 0.0, f32::INFINITY),
162            FlexItem::new(50.0, 3.0, 1.0, 0.0, f32::INFINITY),
163        ];
164        let mut sizes = [100.0, 50.0];
165        distribute_flex(&items, &mut sizes, 0.0, 400.0);
166        assert!(approx(sizes[0] + sizes[1], 400.0));
167        assert!(approx(sizes[0], 162.5));
168        assert!(approx(sizes[1], 237.5));
169    }
170
171    #[test]
172    fn max_cap_overflow_goes_to_sibling() {
173        let items = [
174            FlexItem::new(0.0, 1.0, 1.0, 0.0, 200.0),
175            FlexItem::new(0.0, 1.0, 1.0, 0.0, f32::INFINITY),
176        ];
177        let mut sizes = [0.0, 0.0];
178        distribute_flex(&items, &mut sizes, 0.0, 400.0);
179        assert!(approx(sizes[0], 200.0), "{sizes:?}");
180        assert!(approx(sizes[1], 200.0), "{sizes:?}");
181    }
182
183    #[test]
184    fn shrink_prefers_scaled_weight() {
185        let items = [
186            FlexItem::new(100.0, 1.0, 1.0, 0.0, f32::INFINITY),
187            FlexItem::new(100.0, 1.0, 1.0, 0.0, f32::INFINITY),
188        ];
189        let mut sizes = [100.0, 100.0];
190        distribute_flex(&items, &mut sizes, 0.0, 120.0);
191        assert!(approx(sizes[0] + sizes[1], 120.0), "{sizes:?}");
192    }
193
194    #[test]
195    fn zero_shrink_item_keeps_size() {
196        let items = [
197            FlexItem::new(100.0, 0.0, 0.0, 0.0, f32::INFINITY),
198            FlexItem::new(100.0, 1.0, 1.0, 0.0, f32::INFINITY),
199        ];
200        let mut sizes = [100.0, 100.0];
201        distribute_flex(&items, &mut sizes, 0.0, 120.0);
202        assert!(approx(sizes[0], 100.0), "{sizes:?}");
203        assert!(approx(sizes[1], 20.0), "{sizes:?}");
204    }
205}