Skip to main content

sequoia_layout/
resolve.rs

1//! Length resolution and main-axis space distribution.
2
3use sequoia_core::{Constraints, Length, Size};
4
5/// Resolves a length along the main axis.
6///
7/// Returns `(size, fill_weight)`, where `fill_weight` is non-zero only for
8/// [`Length::Fill`] and describes how much of the free space to claim.
9pub fn resolve_main(len: Length, intrinsic: f32, available: f32) -> (f32, f32) {
10    match len {
11        Length::Px(v) => (v, 0.0),
12        Length::Percent(p) => {
13            if available.is_finite() && p.is_finite() {
14                ((available * p / 100.0).max(0.0), 0.0)
15            } else {
16                (intrinsic, 0.0)
17            }
18        }
19        Length::Fill(w) => (intrinsic, w.max(0.0)),
20        Length::Fit => (intrinsic, 0.0),
21    }
22}
23
24/// Resolves a length along the cross axis to a concrete size.
25///
26/// Percentages and [`Length::Fill`] fall back to the intrinsic size when the
27/// available space is unbounded.
28pub fn resolve_cross(len: Length, intrinsic: f32, available: f32) -> f32 {
29    match len {
30        Length::Px(v) => v,
31        Length::Percent(p) => {
32            if available.is_finite() && p.is_finite() {
33                (available * p / 100.0).max(0.0)
34            } else {
35                intrinsic
36            }
37        }
38        Length::Fill(_) => {
39            if available.is_finite() {
40                available
41            } else {
42                intrinsic
43            }
44        }
45        Length::Fit => intrinsic,
46    }
47}
48
49/// Resolves a flex basis, preferring `basis` over the `fallback` result.
50///
51/// Returns `(size, fill_weight)` like [`resolve_main`]. An unset basis, or a
52/// percentage with unbounded space, keeps the fallback.
53pub fn resolve_flex_basis(
54    basis: Option<Length>,
55    fallback: (f32, f32),
56    intrinsic: f32,
57    available: f32,
58) -> (f32, f32) {
59    let Some(b) = basis else {
60        return fallback;
61    };
62    match b {
63        Length::Px(v) => (v.max(0.0), 0.0),
64        Length::Percent(p) => {
65            if available.is_finite() && p.is_finite() {
66                ((available * p / 100.0).max(0.0), 0.0)
67            } else {
68                fallback
69            }
70        }
71        Length::Fill(w) => (intrinsic, w.max(0.0)),
72        Length::Fit => (intrinsic, 0.0),
73    }
74}
75
76/// Constrains `size` to the given minimum and maximum constraints.
77pub fn clamp_to(size: Size, constraints: Constraints) -> Size {
78    constraints.constrain(size)
79}
80
81/// Distributes free space or a deficit across main-axis bases.
82///
83/// Positive `free` space is shared by items with a positive grow weight. A
84/// deficit first shrinks flexible items, then fixed items, without applying
85/// per-item limits.
86pub fn distribute_main(bases: &mut [f32], grows: &[f32], free: f32) {
87    debug_assert_eq!(bases.len(), grows.len());
88
89    if bases.is_empty() || free == 0.0 || free.is_nan() {
90        return;
91    }
92
93    let sum_grow: f32 = grows.iter().copied().sum();
94
95    if free > 0.0 {
96        if sum_grow > 0.0 {
97            for (i, base) in bases.iter_mut().enumerate() {
98                if grows[i] > 0.0 {
99                    *base += free * grows[i] / sum_grow;
100                }
101            }
102        }
103        return;
104    }
105
106    let mut deficit = -free;
107
108    let flex_sum: f32 = bases
109        .iter()
110        .zip(grows)
111        .filter(|(_, g)| **g > 0.0)
112        .map(|(b, _)| *b)
113        .sum();
114    if flex_sum > 0.0 {
115        let take = flex_sum.min(deficit);
116        let k = 1.0 - take / flex_sum;
117        for (i, base) in bases.iter_mut().enumerate() {
118            if grows[i] > 0.0 {
119                *base *= k;
120            }
121        }
122        deficit -= take;
123    }
124
125    if deficit > 0.0 {
126        let fixed_sum: f32 = bases
127            .iter()
128            .zip(grows)
129            .filter(|(_, g)| **g <= 0.0)
130            .map(|(b, _)| *b)
131            .sum();
132        if fixed_sum > 0.0 {
133            let k = ((fixed_sum - deficit) / fixed_sum).max(0.0);
134            for (i, base) in bases.iter_mut().enumerate() {
135                if grows[i] <= 0.0 {
136                    *base *= k;
137                }
138            }
139        }
140    }
141}
142
143const CLAMP_EPS: f32 = 0.001;
144
145/// Distributes main-axis space while respecting per-item min/max limits.
146///
147/// Similar to [`distribute_main`], but items stop growing at their max and
148/// stop shrinking at their min; the remaining space is passed to siblings.
149/// `gaps` is the total space consumed by gaps between items.
150pub fn distribute_main_clamped(
151    bases: &mut [f32],
152    grows: &[f32],
153    mins: &[f32],
154    maxs: &[f32],
155    gaps: f32,
156    avail_main: f32,
157) {
158    debug_assert_eq!(bases.len(), grows.len());
159    if bases.is_empty() {
160        return;
161    }
162
163    let limit = |i: usize| -> (f32, f32) {
164        let mn = mins.get(i).copied().unwrap_or(0.0);
165        let mx = maxs.get(i).copied().unwrap_or(f32::INFINITY);
166        let mn = if mn.is_finite() { mn.max(0.0) } else { 0.0 };
167        let mx = if mx.is_nan() {
168            f32::INFINITY
169        } else if mx.is_infinite() {
170            if mx > 0.0 { f32::INFINITY } else { 0.0 }
171        } else {
172            mx.max(0.0)
173        };
174        if mn > mx { (mx, mx) } else { (mn, mx) }
175    };
176    let clamp_one = |i: usize, v: f32| -> f32 {
177        let (mn, mx) = limit(i);
178        v.clamp(mn, mx)
179    };
180    for (i, base) in bases.iter_mut().enumerate() {
181        *base = clamp_one(i, *base);
182    }
183    if !avail_main.is_finite() || avail_main.is_nan() {
184        return;
185    }
186    let gaps = if gaps.is_finite() { gaps.max(0.0) } else { 0.0 };
187
188    for _ in 0..bases.len() + 1 {
189        let used: f32 = bases.iter().sum::<f32>() + gaps;
190        let free = avail_main - used;
191        if free.abs() <= CLAMP_EPS || free.is_nan() {
192            break;
193        }
194        if free > 0.0 {
195            let grow_of = |i: usize| grows.get(i).copied().unwrap_or(0.0);
196            let mut sum_grow = 0.0;
197            for (i, base) in bases.iter().enumerate() {
198                let (_, mx) = limit(i);
199                if is_finite_positive(grow_of(i)) && *base < mx - CLAMP_EPS {
200                    sum_grow += grow_of(i);
201                }
202            }
203            if !(sum_grow > 0.0) {
204                break;
205            }
206            for (i, base) in bases.iter_mut().enumerate() {
207                let (_, mx) = limit(i);
208                if is_finite_positive(grow_of(i)) && *base < mx - CLAMP_EPS {
209                    *base = clamp_one(i, *base + free * grow_of(i) / sum_grow);
210                }
211            }
212        } else {
213            let grow_of = |i: usize| grows.get(i).copied().unwrap_or(0.0);
214            let flex_ready = bases
215                .iter()
216                .enumerate()
217                .any(|(i, b)| grow_of(i) > 0.0 && *b > limit(i).0 + CLAMP_EPS);
218
219            let mut pool_sum = 0.0;
220            for (i, base) in bases.iter().enumerate() {
221                let (mn, _) = limit(i);
222                let is_flex = grow_of(i) > 0.0;
223                if *base > mn + CLAMP_EPS && (is_flex || !flex_ready) {
224                    pool_sum += *base;
225                }
226            }
227            if !(pool_sum > 0.0) {
228                break;
229            }
230            let mut deficit = -free;
231            let take = pool_sum.min(deficit);
232            let k = 1.0 - take / pool_sum;
233            for (i, base) in bases.iter_mut().enumerate() {
234                let (mn, _) = limit(i);
235                let is_flex = grow_of(i) > 0.0;
236                if *base > mn + CLAMP_EPS && (is_flex || !flex_ready) {
237                    *base = clamp_one(i, *base * k);
238                }
239            }
240            deficit -= take;
241            if deficit <= CLAMP_EPS {
242                continue;
243            }
244        }
245    }
246}
247
248fn is_finite_positive(v: f32) -> bool {
249    v.is_finite() && v > 0.0
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255
256    fn approx(a: f32, b: f32) -> bool {
257        (a - b).abs() < 0.01
258    }
259
260    #[test]
261    fn clamped_matches_plain_without_limits() {
262        let mut a = vec![100.0, 50.0];
263        let mut b = a.clone();
264        distribute_main(&mut a, &[1.0, 3.0], 250.0);
265        distribute_main_clamped(&mut b, &[1.0, 3.0], &[], &[], 0.0, 400.0);
266        assert!(approx(a[0], b[0]) && approx(a[1], b[1]));
267        assert!(approx(b[0] + b[1], 400.0));
268    }
269
270    #[test]
271    fn leftover_after_max_goes_to_siblings() {
272        let mut bases = vec![0.0, 0.0];
273        distribute_main_clamped(
274            &mut bases,
275            &[1.0, 1.0],
276            &[],
277            &[200.0, f32::INFINITY],
278            0.0,
279            400.0,
280        );
281        assert!(approx(bases[0], 200.0), "{bases:?}");
282        assert!(approx(bases[1], 200.0), "{bases:?}");
283    }
284
285    #[test]
286    fn deficit_stops_at_min_and_overflows() {
287        let mut bases = vec![60.0, 60.0];
288        distribute_main_clamped(&mut bases, &[0.0, 0.0], &[60.0, 0.0], &[], 0.0, 100.0);
289        assert!(approx(bases[0], 60.0), "{bases:?}");
290        assert!(approx(bases[1], 40.0), "{bases:?}");
291
292        let mut bases = vec![60.0, 60.0];
293        distribute_main_clamped(&mut bases, &[0.0, 0.0], &[60.0, 60.0], &[], 0.0, 100.0);
294        assert!(approx(bases[0], 60.0), "{bases:?}");
295        assert!(approx(bases[1], 60.0), "{bases:?}");
296    }
297
298    #[test]
299    fn unbounded_only_clamps() {
300        let mut bases = vec![500.0, 10.0];
301        distribute_main_clamped(
302            &mut bases,
303            &[1.0, 1.0],
304            &[],
305            &[200.0, f32::INFINITY],
306            0.0,
307            f32::INFINITY,
308        );
309        assert!(approx(bases[0], 200.0), "{bases:?}");
310        assert!(approx(bases[1], 10.0), "{bases:?}");
311    }
312}