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