Skip to main content

rich/
ratio.rs

1//! Ratio-based size resolution.
2//!
3//! Port of `rich/_ratio.py`'s `ratio_resolve` — distributes a total span among
4//! a set of edges, each of which may pin a fixed `size`, or flex by `ratio`
5//! down to a `minimum_size`. Used by [`Layout`](crate::layout::Layout) to size
6//! its split regions. (`Table` has its own `_ratio` helpers inline.)
7
8/// One participant in a [`ratio_resolve`] distribution.
9#[derive(Debug, Clone, Copy)]
10pub struct Edge {
11    /// A fixed size, if pinned.
12    pub size: Option<usize>,
13    /// Flex weight when `size` is `None` (defaults to 1 upstream).
14    pub ratio: usize,
15    /// The smallest size a flexible edge may shrink to.
16    pub minimum_size: usize,
17}
18
19impl Edge {
20    pub fn new(size: Option<usize>, ratio: usize, minimum_size: usize) -> Self {
21        Edge {
22            size,
23            ratio,
24            minimum_size,
25        }
26    }
27}
28
29/// Distribute `total` across `edges`, returning a concrete size per edge.
30///
31/// Direct port of `rich._ratio.ratio_resolve`.
32pub fn ratio_resolve(total: usize, edges: &[Edge]) -> Vec<usize> {
33    let total = total as f64;
34    let mut sizes: Vec<Option<usize>> = edges.iter().map(|e| e.size).collect();
35
36    // Resolve one flexible edge per pass until all are fixed.
37    while sizes.iter().any(Option::is_none) {
38        let flexible: Vec<usize> = sizes
39            .iter()
40            .enumerate()
41            .filter(|(_, s)| s.is_none())
42            .map(|(i, _)| i)
43            .collect();
44
45        let fixed_sum: f64 = sizes.iter().flatten().map(|&s| s as f64).sum();
46        let remaining = total - fixed_sum;
47        if remaining <= 0.0 {
48            // No room for flexible edges: give each its minimum (or its size).
49            return sizes
50                .iter()
51                .zip(edges)
52                .map(|(size, edge)| match size {
53                    Some(s) => *s,
54                    None => edge.minimum_size.max(1),
55                })
56                .collect();
57        }
58
59        let ratio_sum: f64 = flexible.iter().map(|&i| edges[i].ratio.max(1) as f64).sum();
60        let portion = remaining / ratio_sum;
61
62        // If any flexible edge would fall below its minimum, pin it and retry —
63        // a newly fixed size changes the remaining distribution.
64        let mut pinned = false;
65        for &i in &flexible {
66            if portion * edges[i].ratio.max(1) as f64 <= edges[i].minimum_size as f64 {
67                sizes[i] = Some(edges[i].minimum_size);
68                pinned = true;
69                break;
70            }
71        }
72        if !pinned {
73            // Distribute the flexible space, carrying the rounding remainder
74            // forward so the totals stay exact (upstream's `divmod` loop).
75            let mut remainder = 0.0;
76            for &i in &flexible {
77                let value = portion * edges[i].ratio.max(1) as f64 + remainder;
78                let size = value.floor();
79                remainder = value - size;
80                sizes[i] = Some(size as usize);
81            }
82            break;
83        }
84    }
85
86    sizes.into_iter().map(|s| s.unwrap_or(0)).collect()
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    fn edges(specs: &[(Option<usize>, usize)]) -> Vec<Edge> {
94        specs
95            .iter()
96            .map(|&(size, ratio)| Edge::new(size, ratio, 1))
97            .collect()
98    }
99
100    #[test]
101    fn even_split_carries_remainder() {
102        // 23 across two ratio-1 edges → 11, 12 (matches upstream's divmod).
103        assert_eq!(
104            ratio_resolve(23, &edges(&[(None, 1), (None, 1)])),
105            vec![11, 12]
106        );
107    }
108
109    #[test]
110    fn even_split_exact() {
111        assert_eq!(
112            ratio_resolve(24, &edges(&[(None, 1), (None, 1)])),
113            vec![12, 12]
114        );
115    }
116
117    #[test]
118    fn fixed_and_flex() {
119        // One flexible (ratio 3) + one fixed size 5, total 24 → 19, 5.
120        assert_eq!(
121            ratio_resolve(24, &edges(&[(None, 3), (Some(5), 1)])),
122            vec![19, 5]
123        );
124    }
125
126    #[test]
127    fn ratio_weighting() {
128        // 3:1 across 24 → 18, 6.
129        assert_eq!(
130            ratio_resolve(24, &edges(&[(None, 3), (None, 1)])),
131            vec![18, 6]
132        );
133    }
134}