sequoia-layout 0.1.1

Sequoia GUI framework — layout engine
Documentation
//! Length resolution and main-axis space distribution.

use sequoia_core::{Constraints, Length, Size};

/// Resolves a length along the main axis.
///
/// Returns `(size, fill_weight)`, where `fill_weight` is non-zero only for
/// [`Length::Fill`] and describes how much of the free space to claim.
pub fn resolve_main(len: Length, intrinsic: f32, available: f32) -> (f32, f32) {
    match len {
        Length::Px(v) => (v, 0.0),
        Length::Percent(p) => {
            if available.is_finite() && p.is_finite() {
                ((available * p / 100.0).max(0.0), 0.0)
            } else {
                (intrinsic, 0.0)
            }
        }
        Length::Fill(w) => (intrinsic, w.max(0.0)),
        Length::Fit => (intrinsic, 0.0),
    }
}

/// Resolves a length along the cross axis to a concrete size.
///
/// Percentages and [`Length::Fill`] fall back to the intrinsic size when the
/// available space is unbounded.
pub fn resolve_cross(len: Length, intrinsic: f32, available: f32) -> f32 {
    match len {
        Length::Px(v) => v,
        Length::Percent(p) => {
            if available.is_finite() && p.is_finite() {
                (available * p / 100.0).max(0.0)
            } else {
                intrinsic
            }
        }
        Length::Fill(_) => {
            if available.is_finite() {
                available
            } else {
                intrinsic
            }
        }
        Length::Fit => intrinsic,
    }
}

/// Resolves a flex basis, preferring `basis` over the `fallback` result.
///
/// Returns `(size, fill_weight)` like [`resolve_main`]. An unset basis, or a
/// percentage with unbounded space, keeps the fallback.
pub fn resolve_flex_basis(
    basis: Option<Length>,
    fallback: (f32, f32),
    intrinsic: f32,
    available: f32,
) -> (f32, f32) {
    let Some(b) = basis else {
        return fallback;
    };
    match b {
        Length::Px(v) => (v.max(0.0), 0.0),
        Length::Percent(p) => {
            if available.is_finite() && p.is_finite() {
                ((available * p / 100.0).max(0.0), 0.0)
            } else {
                fallback
            }
        }
        Length::Fill(w) => (intrinsic, w.max(0.0)),
        Length::Fit => (intrinsic, 0.0),
    }
}

/// Constrains `size` to the given minimum and maximum constraints.
pub fn clamp_to(size: Size, constraints: Constraints) -> Size {
    constraints.constrain(size)
}

/// Distributes free space or a deficit across main-axis bases.
///
/// Positive `free` space is shared by items with a positive grow weight. A
/// deficit first shrinks flexible items, then fixed items, without applying
/// per-item limits.
pub fn distribute_main(bases: &mut [f32], grows: &[f32], free: f32) {
    debug_assert_eq!(bases.len(), grows.len());

    if bases.is_empty() || free == 0.0 || free.is_nan() {
        return;
    }

    let sum_grow: f32 = grows.iter().copied().sum();

    if free > 0.0 {
        if sum_grow > 0.0 {
            for (i, base) in bases.iter_mut().enumerate() {
                if grows[i] > 0.0 {
                    *base += free * grows[i] / sum_grow;
                }
            }
        }
        return;
    }

    let mut deficit = -free;

    let flex_sum: f32 = bases
        .iter()
        .zip(grows)
        .filter(|(_, g)| **g > 0.0)
        .map(|(b, _)| *b)
        .sum();
    if flex_sum > 0.0 {
        let take = flex_sum.min(deficit);
        let k = 1.0 - take / flex_sum;
        for (i, base) in bases.iter_mut().enumerate() {
            if grows[i] > 0.0 {
                *base *= k;
            }
        }
        deficit -= take;
    }

    if deficit > 0.0 {
        let fixed_sum: f32 = bases
            .iter()
            .zip(grows)
            .filter(|(_, g)| **g <= 0.0)
            .map(|(b, _)| *b)
            .sum();
        if fixed_sum > 0.0 {
            let k = ((fixed_sum - deficit) / fixed_sum).max(0.0);
            for (i, base) in bases.iter_mut().enumerate() {
                if grows[i] <= 0.0 {
                    *base *= k;
                }
            }
        }
    }
}

const CLAMP_EPS: f32 = 0.001;

/// Distributes main-axis space while respecting per-item min/max limits.
///
/// Similar to [`distribute_main`], but items stop growing at their max and
/// stop shrinking at their min; the remaining space is passed to siblings.
/// `gaps` is the total space consumed by gaps between items.
pub fn distribute_main_clamped(
    bases: &mut [f32],
    grows: &[f32],
    mins: &[f32],
    maxs: &[f32],
    gaps: f32,
    avail_main: f32,
) {
    debug_assert_eq!(bases.len(), grows.len());
    if bases.is_empty() {
        return;
    }

    let limit = |i: usize| -> (f32, f32) {
        let mn = mins.get(i).copied().unwrap_or(0.0);
        let mx = maxs.get(i).copied().unwrap_or(f32::INFINITY);
        let mn = if mn.is_finite() { mn.max(0.0) } else { 0.0 };
        let mx = if mx.is_nan() {
            f32::INFINITY
        } else if mx.is_infinite() {
            if mx > 0.0 { f32::INFINITY } else { 0.0 }
        } else {
            mx.max(0.0)
        };
        if mn > mx { (mx, mx) } else { (mn, mx) }
    };
    let clamp_one = |i: usize, v: f32| -> f32 {
        let (mn, mx) = limit(i);
        v.clamp(mn, mx)
    };
    for (i, base) in bases.iter_mut().enumerate() {
        *base = clamp_one(i, *base);
    }
    if !avail_main.is_finite() || avail_main.is_nan() {
        return;
    }
    let gaps = if gaps.is_finite() { gaps.max(0.0) } else { 0.0 };

    for _ in 0..bases.len() + 1 {
        let used: f32 = bases.iter().sum::<f32>() + gaps;
        let free = avail_main - used;
        if free.abs() <= CLAMP_EPS || free.is_nan() {
            break;
        }
        if free > 0.0 {
            let grow_of = |i: usize| grows.get(i).copied().unwrap_or(0.0);
            let mut sum_grow = 0.0;
            for (i, base) in bases.iter().enumerate() {
                let (_, mx) = limit(i);
                if is_finite_positive(grow_of(i)) && *base < mx - CLAMP_EPS {
                    sum_grow += grow_of(i);
                }
            }
            if !(sum_grow > 0.0) {
                break;
            }
            for (i, base) in bases.iter_mut().enumerate() {
                let (_, mx) = limit(i);
                if is_finite_positive(grow_of(i)) && *base < mx - CLAMP_EPS {
                    *base = clamp_one(i, *base + free * grow_of(i) / sum_grow);
                }
            }
        } else {
            let grow_of = |i: usize| grows.get(i).copied().unwrap_or(0.0);
            let flex_ready = bases
                .iter()
                .enumerate()
                .any(|(i, b)| grow_of(i) > 0.0 && *b > limit(i).0 + CLAMP_EPS);

            let mut pool_sum = 0.0;
            for (i, base) in bases.iter().enumerate() {
                let (mn, _) = limit(i);
                let is_flex = grow_of(i) > 0.0;
                if *base > mn + CLAMP_EPS && (is_flex || !flex_ready) {
                    pool_sum += *base;
                }
            }
            if !(pool_sum > 0.0) {
                break;
            }
            let mut deficit = -free;
            let take = pool_sum.min(deficit);
            let k = 1.0 - take / pool_sum;
            for (i, base) in bases.iter_mut().enumerate() {
                let (mn, _) = limit(i);
                let is_flex = grow_of(i) > 0.0;
                if *base > mn + CLAMP_EPS && (is_flex || !flex_ready) {
                    *base = clamp_one(i, *base * k);
                }
            }
            deficit -= take;
            if deficit <= CLAMP_EPS {
                continue;
            }
        }
    }
}

fn is_finite_positive(v: f32) -> bool {
    v.is_finite() && v > 0.0
}

#[cfg(test)]
mod tests {
    use super::*;

    fn approx(a: f32, b: f32) -> bool {
        (a - b).abs() < 0.01
    }

    #[test]
    fn clamped_matches_plain_without_limits() {
        let mut a = vec![100.0, 50.0];
        let mut b = a.clone();
        distribute_main(&mut a, &[1.0, 3.0], 250.0);
        distribute_main_clamped(&mut b, &[1.0, 3.0], &[], &[], 0.0, 400.0);
        assert!(approx(a[0], b[0]) && approx(a[1], b[1]));
        assert!(approx(b[0] + b[1], 400.0));
    }

    #[test]
    fn leftover_after_max_goes_to_siblings() {
        let mut bases = vec![0.0, 0.0];
        distribute_main_clamped(
            &mut bases,
            &[1.0, 1.0],
            &[],
            &[200.0, f32::INFINITY],
            0.0,
            400.0,
        );
        assert!(approx(bases[0], 200.0), "{bases:?}");
        assert!(approx(bases[1], 200.0), "{bases:?}");
    }

    #[test]
    fn deficit_stops_at_min_and_overflows() {
        let mut bases = vec![60.0, 60.0];
        distribute_main_clamped(&mut bases, &[0.0, 0.0], &[60.0, 0.0], &[], 0.0, 100.0);
        assert!(approx(bases[0], 60.0), "{bases:?}");
        assert!(approx(bases[1], 40.0), "{bases:?}");

        let mut bases = vec![60.0, 60.0];
        distribute_main_clamped(&mut bases, &[0.0, 0.0], &[60.0, 60.0], &[], 0.0, 100.0);
        assert!(approx(bases[0], 60.0), "{bases:?}");
        assert!(approx(bases[1], 60.0), "{bases:?}");
    }

    #[test]
    fn unbounded_only_clamps() {
        let mut bases = vec![500.0, 10.0];
        distribute_main_clamped(
            &mut bases,
            &[1.0, 1.0],
            &[],
            &[200.0, f32::INFINITY],
            0.0,
            f32::INFINITY,
        );
        assert!(approx(bases[0], 200.0), "{bases:?}");
        assert!(approx(bases[1], 10.0), "{bases:?}");
    }
}