use super::model::PaneSnapshot;
const EPS: f32 = 0.01;
pub fn distribute(available: f32, panes: &[PaneSnapshot], progress: &[f32]) -> Vec<f32> {
let n = panes.len();
if n == 0 {
return Vec::new();
}
let available = available.max(0.0);
let mut sizes = vec![0.0f32; n];
let mut emin = vec![0.0f32; n];
let mut emax = vec![f32::INFINITY; n];
for i in 0..n {
let p = panes[i];
let prog = progress.get(i).copied().unwrap_or(1.0).clamp(0.0, 1.0);
let (req, lo, hi) = if p.collapsed {
let c = p.collapsed_size;
let top = p.stored_size.max(c);
(c + (top - c) * prog, 0.0, top)
} else {
(
p.stored_size,
p.min_size,
p.max_size.unwrap_or(f32::INFINITY),
)
};
debug_assert!(
lo.is_finite() && !hi.is_nan(),
"pane {i} has an invalid size bound (min={lo}, max={hi}); \
PaneDescriptor min must be finite and max must not be NaN",
);
let lo = if lo.is_finite() { lo } else { 0.0 };
let hi = if hi.is_nan() { f32::INFINITY } else { hi };
emin[i] = lo;
emax[i] = hi;
sizes[i] = if lo > hi { lo } else { req.clamp(lo, hi) };
}
let total: f32 = sizes.iter().sum();
let slack = available - total;
if slack > EPS {
grow(&mut sizes, &emax, panes, slack);
} else if slack < -EPS {
shrink(&mut sizes, &emin, panes, -slack);
}
sizes
}
fn grow(sizes: &mut [f32], emax: &[f32], panes: &[PaneSnapshot], mut surplus: f32) {
let n = sizes.len();
let mut frozen = vec![false; n];
for (i, p) in panes.iter().enumerate() {
if p.collapsed {
frozen[i] = true;
}
}
loop {
let pool: Vec<usize> = (0..n)
.filter(|&i| !frozen[i] && panes[i].stretch > 0.0 && sizes[i] < emax[i] - EPS)
.collect();
if pool.is_empty() {
break;
}
let total_stretch: f32 = pool.iter().map(|&i| panes[i].stretch).sum();
if total_stretch <= 0.0 {
break;
}
let mut absorbed = 0.0;
for &i in &pool {
let give = surplus * (panes[i].stretch / total_stretch);
let room = emax[i] - sizes[i];
let take = give.min(room);
sizes[i] += take;
absorbed += take;
if sizes[i] >= emax[i] - EPS {
frozen[i] = true;
}
}
surplus -= absorbed;
if absorbed < EPS || surplus < EPS {
break;
}
}
if surplus > EPS
&& let Some(i) = (0..n).rev().find(|&i| !panes[i].collapsed)
{
let room = (emax[i] - sizes[i]).max(0.0);
sizes[i] += surplus.min(room);
}
}
fn shrink(sizes: &mut [f32], emin: &[f32], panes: &[PaneSnapshot], mut deficit: f32) {
let n = sizes.len();
let mut frozen = vec![false; n];
for (i, p) in panes.iter().enumerate() {
if p.collapsed {
frozen[i] = true; }
}
loop {
let pool: Vec<usize> = (0..n)
.filter(|&i| !frozen[i] && sizes[i] - emin[i] > EPS)
.collect();
if pool.is_empty() {
break;
}
let total_room: f32 = pool.iter().map(|&i| sizes[i] - emin[i]).sum();
if total_room <= 0.0 {
break;
}
let mut absorbed = 0.0;
for &i in &pool {
let room = sizes[i] - emin[i];
let take = (deficit * (room / total_room)).min(room);
sizes[i] -= take;
absorbed += take;
if sizes[i] - emin[i] <= EPS {
frozen[i] = true;
}
}
deficit -= absorbed;
if absorbed < EPS || deficit < EPS {
break;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn pane(
stored: f32,
min: f32,
max: Option<f32>,
stretch: f32,
collapsed: bool,
) -> PaneSnapshot {
PaneSnapshot {
stored_size: stored,
min_size: min,
max_size: max,
stretch,
collapsed,
collapsed_size: 0.0,
visible: true,
}
}
fn ones(n: usize) -> Vec<f32> {
vec![1.0; n]
}
#[test]
fn collapsed_pane_folds_to_collapsed_size_not_zero() {
let mut p0 = pane(200.0, 50.0, None, 1.0, true);
p0.collapsed_size = 30.0;
let p1 = pane(200.0, 50.0, None, 1.0, false);
let collapsed = distribute(400.0, &[p0, p1], &[0.0, 1.0]);
assert!(
approx(collapsed[0], 30.0),
"collapsed pane folds to 30, got {}",
collapsed[0]
);
assert!(
approx(collapsed[1], 370.0),
"sibling absorbs the freed space, got {}",
collapsed[1]
);
let expanded = distribute(400.0, &[p0, p1], &[1.0, 1.0]);
assert!(
approx(expanded[0], 200.0),
"expands back to stored size, got {}",
expanded[0]
);
}
fn approx(a: f32, b: f32) -> bool {
(a - b).abs() < 0.5
}
#[test]
fn equal_share_from_zero_stored() {
let panes = vec![pane(0.0, 96.0, None, 1.0, false); 3];
let sizes = distribute(600.0, &panes, &ones(3));
for s in &sizes {
assert!(approx(*s, 200.0), "got {sizes:?}");
}
}
#[test]
fn stretch_absorbs_surplus_by_weight() {
let panes = vec![
pane(100.0, 0.0, None, 1.0, false),
pane(100.0, 0.0, None, 3.0, false),
];
let sizes = distribute(600.0, &panes, &ones(2));
assert!(approx(sizes[0], 200.0), "{sizes:?}");
assert!(approx(sizes[1], 400.0), "{sizes:?}");
}
#[test]
fn zero_stretch_surplus_goes_to_last_pane() {
let panes = vec![
pane(100.0, 0.0, None, 0.0, false),
pane(100.0, 0.0, None, 0.0, false),
];
let sizes = distribute(500.0, &panes, &ones(2));
assert!(approx(sizes[0], 100.0), "{sizes:?}");
assert!(approx(sizes[1], 400.0), "{sizes:?}");
}
#[test]
fn shrink_proportional_to_room_and_clamps_min() {
let panes = vec![
pane(300.0, 100.0, None, 1.0, false),
pane(300.0, 100.0, None, 1.0, false),
];
let sizes = distribute(400.0, &panes, &ones(2));
assert!(approx(sizes[0], 200.0), "{sizes:?}");
assert!(approx(sizes[1], 200.0), "{sizes:?}");
let tiny = distribute(150.0, &panes, &ones(2));
assert!(approx(tiny[0], 100.0) && approx(tiny[1], 100.0), "{tiny:?}");
}
#[test]
fn max_clamps_and_reroutes_surplus() {
let panes = vec![
pane(100.0, 0.0, Some(150.0), 1.0, false),
pane(100.0, 0.0, None, 1.0, false),
];
let sizes = distribute(600.0, &panes, &ones(2));
assert!(approx(sizes[0], 150.0), "{sizes:?}");
assert!(approx(sizes[1], 450.0), "{sizes:?}");
}
#[test]
fn collapse_progress_scales_effective_size() {
let panes = vec![
pane(200.0, 96.0, None, 1.0, true),
pane(200.0, 96.0, None, 1.0, false),
];
let half = distribute(600.0, &panes, &[0.5, 1.0]);
assert!(approx(half[0], 100.0), "{half:?}");
assert!(approx(half[1], 500.0), "{half:?}");
let full = distribute(600.0, &panes, &[0.0, 1.0]);
assert!(approx(full[0], 0.0), "{full:?}");
assert!(approx(full[1], 600.0), "{full:?}");
}
#[test]
fn all_collapsed_yields_zero() {
let panes = vec![pane(200.0, 96.0, None, 1.0, true); 2];
let sizes = distribute(600.0, &panes, &[0.0, 0.0]);
assert!(approx(sizes[0], 0.0) && approx(sizes[1], 0.0), "{sizes:?}");
}
#[test]
fn min_greater_than_max_honors_min() {
let panes = vec![pane(50.0, 200.0, Some(100.0), 1.0, false)];
let sizes = distribute(80.0, &panes, &ones(1));
assert!(approx(sizes[0], 200.0), "{sizes:?}");
}
#[test]
fn idempotent_for_same_input() {
let panes = vec![
pane(120.0, 50.0, None, 1.0, false),
pane(300.0, 80.0, Some(500.0), 2.0, false),
pane(0.0, 96.0, None, 0.0, true),
];
let a = distribute(700.0, &panes, &[1.0, 1.0, 0.3]);
let b = distribute(700.0, &panes, &[1.0, 1.0, 0.3]);
assert_eq!(a, b);
}
}
#[cfg(test)]
mod proptests {
use super::*;
use proptest::prelude::*;
fn arb_available() -> impl Strategy<Value = f32> {
prop_oneof![Just(0.0_f32), Just(-1.0_f32), 0.5f32..4000.0_f32,]
}
fn arb_pane() -> impl Strategy<Value = PaneSnapshot> {
(
prop_oneof![Just(0.0f32), -500.0f32..=3000.0f32],
prop_oneof![Just(0.0f32), -500.0f32..=1500.0f32],
prop_oneof![Just(None), (-500.0f32..=1500.0f32).prop_map(Some)],
prop_oneof![Just(0.0f32), -5.0f32..=10.0f32],
any::<bool>(),
prop_oneof![Just(0.0f32), -100.0f32..=500.0f32],
)
.prop_map(
|(stored_size, min_size, max_size, stretch, collapsed, collapsed_size)| {
PaneSnapshot {
stored_size,
min_size,
max_size,
stretch,
collapsed,
collapsed_size,
visible: true,
}
},
)
}
fn arb_panes_with_progress() -> impl Strategy<Value = (Vec<PaneSnapshot>, Vec<f32>)> {
prop::collection::vec(arb_pane(), 1..=8).prop_flat_map(|panes| {
let n = panes.len();
let progress = prop::collection::vec(
prop_oneof![Just(0.0f32), Just(1.0f32), 0.0f32..=1.0f32],
n..=n,
);
(Just(panes), progress)
})
}
fn arb_growable_pane() -> impl Strategy<Value = PaneSnapshot> {
(0.0f32..=2000.0f32, 0.1f32..=20.0f32).prop_map(|(stored_size, stretch)| PaneSnapshot {
stored_size,
min_size: 0.0,
max_size: None,
stretch,
collapsed: false,
collapsed_size: 0.0,
visible: true,
})
}
fn arb_growable_panes() -> impl Strategy<Value = Vec<PaneSnapshot>> {
prop::collection::vec(arb_growable_pane(), 1..=8)
}
fn arb_zero_stored_growable_pane() -> impl Strategy<Value = PaneSnapshot> {
(0.1f32..=20.0f32).prop_map(|stretch| PaneSnapshot {
stored_size: 0.0,
min_size: 0.0,
max_size: None,
stretch,
collapsed: false,
collapsed_size: 0.0,
visible: true,
})
}
fn arb_zero_stored_growable_panes() -> impl Strategy<Value = Vec<PaneSnapshot>> {
prop::collection::vec(arb_zero_stored_growable_pane(), 2..=6)
}
fn arb_zero_stretch_pane() -> impl Strategy<Value = PaneSnapshot> {
(0.0f32..=500.0f32).prop_map(|stored_size| PaneSnapshot {
stored_size,
min_size: 0.0,
max_size: None,
stretch: 0.0,
collapsed: false,
collapsed_size: 0.0,
visible: true,
})
}
fn arb_zero_stretch_panes() -> impl Strategy<Value = Vec<PaneSnapshot>> {
prop::collection::vec(arb_zero_stretch_pane(), 1..=8)
}
fn arb_min_bound_pane() -> impl Strategy<Value = PaneSnapshot> {
(1.0f32..=500.0f32, 0.0f32..=2000.0f32, 0.0f32..=5.0f32).prop_map(
|(min_size, extra, stretch)| PaneSnapshot {
stored_size: min_size + extra,
min_size,
max_size: None,
stretch,
collapsed: false,
collapsed_size: 0.0,
visible: true,
},
)
}
fn arb_min_bound_panes() -> impl Strategy<Value = Vec<PaneSnapshot>> {
prop::collection::vec(arb_min_bound_pane(), 1..=8)
}
fn arb_pane_nonneg() -> impl Strategy<Value = PaneSnapshot> {
(
0.0f32..=3000.0f32,
0.0f32..=1500.0f32,
prop_oneof![Just(None), (0.0f32..=1500.0f32).prop_map(Some)],
prop_oneof![Just(0.0f32), -5.0f32..=10.0f32],
any::<bool>(),
0.0f32..=500.0f32,
)
.prop_map(
|(stored_size, min_size, max_size, stretch, collapsed, collapsed_size)| {
PaneSnapshot {
stored_size,
min_size,
max_size,
stretch,
collapsed,
collapsed_size,
visible: true,
}
},
)
}
fn arb_nonneg_panes_with_progress() -> impl Strategy<Value = (Vec<PaneSnapshot>, Vec<f32>)> {
prop::collection::vec(arb_pane_nonneg(), 1..=8).prop_flat_map(|panes| {
let n = panes.len();
let progress = prop::collection::vec(
prop_oneof![Just(0.0f32), Just(1.0f32), 0.0f32..=1.0f32],
n..=n,
);
(Just(panes), progress)
})
}
fn arb_non_finite_max() -> impl Strategy<Value = f32> {
prop_oneof![
Just(f32::INFINITY),
Just(f32::NEG_INFINITY),
0.0f32..=1000.0f32,
]
}
fn arb_pane_with_wild_bounds() -> impl Strategy<Value = PaneSnapshot> {
(
0.0f32..=1000.0f32,
0.0f32..=1000.0f32,
prop_oneof![Just(None), arb_non_finite_max().prop_map(Some)],
0.1f32..=5.0f32,
)
.prop_map(|(stored_size, min_size, max_size, stretch)| PaneSnapshot {
stored_size,
min_size,
max_size,
stretch,
collapsed: false,
collapsed_size: 0.0,
visible: true,
})
}
fn arb_panes_with_wild_bounds() -> impl Strategy<Value = Vec<PaneSnapshot>> {
prop::collection::vec(arb_pane_with_wild_bounds(), 1..=4)
}
proptest! {
#[test]
fn grow_conservation_with_unbounded_max_and_positive_stretch(
panes in arb_growable_panes(),
extra_slack in 0.0f32..=5000.0f32,
) {
let total_initial: f32 = panes.iter().map(|p| p.stored_size).sum();
let available = total_initial + extra_slack;
let progress = vec![1.0f32; panes.len()];
let sizes = distribute(available, &panes, &progress);
let sum: f32 = sizes.iter().sum();
prop_assert!(
(sum - available).abs() < 1.0,
"growth with min=0/max=None/stretch>0 should fully absorb slack: sum={} available={} sizes={:?}",
sum, available, sizes
);
}
}
proptest! {
#[test]
fn shrink_conservation_when_every_min_is_zero(
panes in arb_zero_stretch_panes(),
available_fraction in 0.0f32..=1.0f32,
) {
let initial: Vec<f32> = panes
.iter()
.map(|p| p.stored_size.min(p.max_size.unwrap_or(f32::INFINITY)))
.collect();
let total_initial: f32 = initial.iter().sum();
let available = total_initial * available_fraction;
let progress = vec![1.0f32; panes.len()];
let sizes = distribute(available, &panes, &progress);
let sum: f32 = sizes.iter().sum();
prop_assert!(
(sum - available).abs() < 1.0,
"shrink with every min=0 should always fully reach available: sum={} available={} sizes={:?}",
sum, available, sizes
);
}
}
proptest! {
#[test]
fn shrink_floor_accepts_overflow_when_total_min_exceeds_available(
panes in arb_min_bound_panes(),
shortfall_fraction in 0.0f32..=0.9f32,
) {
let total_min: f32 = panes.iter().map(|p| p.min_size).sum();
let available = total_min * shortfall_fraction;
let progress = vec![1.0f32; panes.len()];
let sizes = distribute(available, &panes, &progress);
let sum: f32 = sizes.iter().sum();
prop_assert!(
sum >= available - 0.5,
"an unsatisfiable shrink must not undershoot available: sum={} available={}",
sum, available
);
prop_assert!(
(sum - total_min).abs() < 1.0,
"an unsatisfiable shrink floors every pane at its min, so the total should equal Sum(min)={}, got {} (sizes={:?})",
total_min, sum, sizes
);
for (i, (size, p)) in sizes.iter().zip(panes.iter()).enumerate() {
prop_assert!(
(size - p.min_size).abs() < 0.5,
"pane {} should be pinned at its min {} under an unsatisfiable shrink, got {}",
i, p.min_size, size
);
}
}
}
proptest! {
#[test]
fn every_pane_respects_its_effective_bounds_or_honors_min_when_contradictory(
panes_and_progress in arb_panes_with_progress(),
available in arb_available(),
) {
let (panes, progress) = panes_and_progress;
let sizes = distribute(available, &panes, &progress);
for (i, (size, p)) in sizes.iter().zip(panes.iter()).enumerate() {
let (emin, emax) = if p.collapsed {
(0.0f32, p.stored_size.max(p.collapsed_size))
} else {
(p.min_size, p.max_size.unwrap_or(f32::INFINITY))
};
if emin <= emax {
prop_assert!(
*size >= emin - 0.5 && *size <= emax + 0.5,
"pane {} size {} should stay within its effective bounds [{}, {}] (available={}, panes={:?})",
i, size, emin, emax, available, panes
);
} else {
prop_assert!(
(*size - emin).abs() < 0.5,
"pane {} has contradictory bounds (min {} > max {}); distribute must honor min, got {}",
i, emin, emax, size
);
}
}
}
}
proptest! {
#[test]
fn growth_splits_proportionally_to_stretch_weight(
panes in arb_zero_stored_growable_panes(),
available in 0.0f32..=5000.0f32,
) {
let progress = vec![1.0f32; panes.len()];
let sizes = distribute(available, &panes, &progress);
let total_stretch: f32 = panes.iter().map(|p| p.stretch).sum();
for (i, (size, p)) in sizes.iter().zip(panes.iter()).enumerate() {
let expected = available * (p.stretch / total_stretch);
prop_assert!(
(size - expected).abs() < 1.0,
"pane {} (stretch {}, total_stretch {}) should get {} of {}: expected {}, got {}",
i, p.stretch, total_stretch, p.stretch / total_stretch, available, expected, size
);
}
}
}
proptest! {
#[test]
fn zero_stretch_everywhere_routes_all_surplus_to_the_last_pane(
panes in arb_zero_stretch_panes(),
extra_slack in 0.0f32..=5000.0f32,
) {
let total_initial: f32 = panes.iter().map(|p| p.stored_size).sum();
let available = total_initial + extra_slack;
let progress = vec![1.0f32; panes.len()];
let sizes = distribute(available, &panes, &progress);
let n = panes.len();
for i in 0..(n - 1) {
prop_assert!(
(sizes[i] - panes[i].stored_size).abs() < 0.5,
"pane {} has stretch 0 and should keep its stored size {}, got {}",
i, panes[i].stored_size, sizes[i]
);
}
let last = n - 1;
let expected_last = panes[last].stored_size + extra_slack;
prop_assert!(
(sizes[last] - expected_last).abs() < 1.0,
"the last pane should absorb the whole surplus {}: expected {}, got {}",
extra_slack, expected_last, sizes[last]
);
}
}
proptest! {
#[test]
fn collapsed_pane_folds_to_exactly_its_collapsed_size_at_zero_progress(
pane in (0.0f32..=500.0f32, 0.0f32..=2000.0f32, 0.0f32..=10.0f32).prop_map(
|(collapsed_size, stored_size, stretch)| PaneSnapshot {
stored_size,
min_size: 0.0,
max_size: None,
stretch,
collapsed: true,
collapsed_size,
visible: true,
},
),
sibling_stretch in 0.1f32..=10.0f32,
available in 0.5f32..=4000.0f32,
) {
let sibling = PaneSnapshot {
stored_size: 0.0,
min_size: 0.0,
max_size: None,
stretch: sibling_stretch,
collapsed: false,
collapsed_size: 0.0,
visible: true,
};
let sizes = distribute(available, &[pane, sibling], &[0.0, 1.0]);
prop_assert!(
(sizes[0] - pane.collapsed_size).abs() < 0.5,
"a pane at progress 0 should fold to exactly its collapsed_size {}, got {}",
pane.collapsed_size, sizes[0]
);
}
}
proptest! {
#[test]
fn distribute_is_deterministic_for_identical_inputs(
panes_and_progress in arb_panes_with_progress(),
available in arb_available(),
) {
let (panes, progress) = panes_and_progress;
let a = distribute(available, &panes, &progress);
let b = distribute(available, &panes, &progress);
prop_assert_eq!(&a, &b, "distribute must be a pure function of its inputs: {:?} vs {:?}", a, b);
}
}
proptest! {
#![proptest_config(ProptestConfig { cases: 512, ..ProptestConfig::default() })]
#[test]
fn increasing_available_never_shrinks_any_individual_pane(
panes_and_progress in arb_panes_with_progress(),
available_lo in arb_available(),
extra in 0.0f32..=3000.0f32,
) {
let (panes, progress) = panes_and_progress;
let available_hi = available_lo.max(0.0) + extra;
let sizes_lo = distribute(available_lo, &panes, &progress);
let sizes_hi = distribute(available_hi, &panes, &progress);
for (i, (lo, hi)) in sizes_lo.iter().zip(sizes_hi.iter()).enumerate() {
prop_assert!(
*hi >= *lo - 0.5,
"pane {} shrank from {} to {} when available grew from {} to {}",
i, lo, hi, available_lo, available_hi
);
}
}
}
proptest! {
#[test]
fn negative_available_is_treated_identically_to_zero(
panes_and_progress in arb_panes_with_progress(),
negative_available in -10000.0f32..0.0f32,
) {
let (panes, progress) = panes_and_progress;
let from_negative = distribute(negative_available, &panes, &progress);
let from_zero = distribute(0.0, &panes, &progress);
prop_assert_eq!(
&from_negative, &from_zero,
"distribute clamps available to 0 internally (`available.max(0.0)`), so negative available {} must match available=0: {:?} vs {:?}",
negative_available, from_negative, from_zero
);
}
}
proptest! {
#![proptest_config(ProptestConfig { cases: 512, ..ProptestConfig::default() })]
#[test]
fn finite_nonnegative_inputs_never_produce_nan_or_negative_sizes(
panes_and_progress in arb_nonneg_panes_with_progress(),
available in arb_available(),
) {
let (panes, progress) = panes_and_progress;
let sizes = distribute(available, &panes, &progress);
for (i, size) in sizes.iter().enumerate() {
prop_assert!(
size.is_finite(),
"pane {} produced a non-finite size {} for available={}, panes={:?}",
i, size, available, panes
);
prop_assert!(
*size >= -0.05,
"pane {} produced a meaningfully negative size {} from all-non-negative pane fields (available={}, panes={:?})",
i, size, available, panes
);
}
}
}
proptest! {
#[test]
fn sizes_stay_finite_even_when_a_pane_min_or_max_is_non_finite(
panes in arb_panes_with_wild_bounds(),
available in arb_available(),
) {
let progress = vec![1.0f32; panes.len()];
let sizes = distribute(available, &panes, &progress);
for (i, size) in sizes.iter().enumerate() {
prop_assert!(
size.is_finite(),
"pane {} produced a non-finite size {} from a non-finite min/max bound (available={}, panes={:?})",
i, size, available, panes
);
}
}
}
}