oxipdf-ir 0.1.0

Intermediate representation types for the oxipdf PDF engine
Documentation
//! Dimension types for layout properties.
//!
//! These types represent values that may be auto, absolute, or percentage-based.
//! Percentages are resolved relative to the containing block during layout.
//!
//! CSS math functions (`calc()`, `min()`, `max()`, `clamp()`) are represented
//! using [`CalcExpr`] — a linear combination of an absolute length and a
//! percentage. This keeps all dimension types `Copy` while supporting
//! expressions like `calc(100% - 20pt)` or `min(300pt, 50%)`.

use super::pt::Pt;

/// A linear combination of an absolute length and a percentage.
///
/// Represents the expression `length + percent × available_space`.
/// Used as the building block for `calc()`, `min()`, `max()`, `clamp()`.
///
/// # Examples
///
/// - `300pt` → `CalcExpr { length: Pt(300), percent: 0.0 }`
/// - `50%` → `CalcExpr { length: Pt(0), percent: 0.5 }`
/// - `calc(100% - 20pt)` → `CalcExpr { length: Pt(-20), percent: 1.0 }`
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct CalcExpr {
    /// Absolute length component.
    pub length: Pt,
    /// Percentage component (0.0–1.0 for 0%–100%).
    pub percent: f64,
}

impl CalcExpr {
    /// Create from a pure length.
    #[must_use]
    pub const fn from_length(pt: Pt) -> Self {
        Self {
            length: pt,
            percent: 0.0,
        }
    }

    /// Create from a pure percentage.
    #[must_use]
    pub const fn from_percent(p: f64) -> Self {
        Self {
            length: Pt::ZERO,
            percent: p,
        }
    }

    /// Resolve against the available space.
    #[must_use]
    pub fn resolve(&self, available: f64) -> f64 {
        self.length.get() + self.percent * available
    }
}

/// A dimension value that may be auto, an absolute length, a percentage,
/// or a CSS math function (`calc`, `min`, `max`, `clamp`).
///
/// Used for properties where `auto` is meaningful: width, height, margin.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum Dimension {
    /// The layout engine determines the value.
    #[default]
    Auto,
    /// An absolute length in points.
    Length(Pt),
    /// A fraction of the containing block's corresponding dimension (0.0–1.0).
    Percent(f64),
    /// `calc(length + percent)` — resolved to `length + percent × available`.
    Calc(CalcExpr),
    /// `min(a, b)` — resolved to the smaller value.
    Min(CalcExpr, CalcExpr),
    /// `max(a, b)` — resolved to the larger value.
    Max(CalcExpr, CalcExpr),
    /// `clamp(min, val, max)` — resolved to `val` clamped between `min` and `max`.
    Clamp {
        min: CalcExpr,
        val: CalcExpr,
        max: CalcExpr,
    },
}

impl Dimension {
    /// Resolve this dimension against the available space, returning the
    /// concrete value in points. `Auto` returns `None`.
    #[must_use]
    pub fn resolve(&self, available: f64) -> Option<f64> {
        match self {
            Self::Auto => None,
            Self::Length(pt) => Some(pt.get()),
            Self::Percent(p) => Some(p * available),
            Self::Calc(e) => Some(e.resolve(available)),
            Self::Min(a, b) => Some(a.resolve(available).min(b.resolve(available))),
            Self::Max(a, b) => Some(a.resolve(available).max(b.resolve(available))),
            Self::Clamp { min, val, max } => {
                let v = val.resolve(available);
                Some(v.clamp(min.resolve(available), max.resolve(available)))
            }
        }
    }

    /// Whether this dimension contains a math expression that needs resolution.
    #[must_use]
    pub fn needs_resolution(&self) -> bool {
        matches!(
            self,
            Self::Calc(_) | Self::Min(_, _) | Self::Max(_, _) | Self::Clamp { .. }
        )
    }
}

/// A dimension value that is either an absolute length, a percentage,
/// or a CSS math function.
///
/// Used for properties where `auto` is not valid: padding, gap, border-radius.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LengthPercentage {
    /// An absolute length in points.
    Length(Pt),
    /// A fraction of the containing block's corresponding dimension (0.0–1.0).
    Percent(f64),
    /// `calc(length + percent)`.
    Calc(CalcExpr),
    /// `min(a, b)`.
    Min(CalcExpr, CalcExpr),
    /// `max(a, b)`.
    Max(CalcExpr, CalcExpr),
    /// `clamp(min, val, max)`.
    Clamp {
        min: CalcExpr,
        val: CalcExpr,
        max: CalcExpr,
    },
}

impl Default for LengthPercentage {
    fn default() -> Self {
        Self::Length(Pt::ZERO)
    }
}

impl LengthPercentage {
    /// Zero length.
    pub const ZERO: Self = Self::Length(Pt::ZERO);

    /// Resolve against available space.
    #[must_use]
    pub fn resolve(&self, available: f64) -> f64 {
        match self {
            Self::Length(pt) => pt.get(),
            Self::Percent(p) => p * available,
            Self::Calc(e) => e.resolve(available),
            Self::Min(a, b) => a.resolve(available).min(b.resolve(available)),
            Self::Max(a, b) => a.resolve(available).max(b.resolve(available)),
            Self::Clamp { min, val, max } => val
                .resolve(available)
                .clamp(min.resolve(available), max.resolve(available)),
        }
    }
}

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

    #[test]
    fn calc_expr_resolve() {
        // calc(100% - 20pt) with 500pt available = 480pt
        let e = CalcExpr {
            length: Pt::new(-20.0),
            percent: 1.0,
        };
        assert!((e.resolve(500.0) - 480.0).abs() < 0.01);
    }

    #[test]
    fn dimension_calc_resolve() {
        let d = Dimension::Calc(CalcExpr {
            length: Pt::new(10.0),
            percent: 0.5,
        });
        // 10 + 0.5 * 200 = 110
        assert!((d.resolve(200.0).unwrap() - 110.0).abs() < 0.01);
    }

    #[test]
    fn dimension_min_resolve() {
        let d = Dimension::Min(
            CalcExpr::from_length(Pt::new(300.0)),
            CalcExpr::from_percent(0.5),
        );
        // min(300, 0.5 * 500) = min(300, 250) = 250
        assert!((d.resolve(500.0).unwrap() - 250.0).abs() < 0.01);
        // min(300, 0.5 * 800) = min(300, 400) = 300
        assert!((d.resolve(800.0).unwrap() - 300.0).abs() < 0.01);
    }

    #[test]
    fn dimension_max_resolve() {
        let d = Dimension::Max(
            CalcExpr::from_length(Pt::new(100.0)),
            CalcExpr::from_percent(0.5),
        );
        // max(100, 0.5 * 150) = max(100, 75) = 100
        assert!((d.resolve(150.0).unwrap() - 100.0).abs() < 0.01);
        // max(100, 0.5 * 400) = max(100, 200) = 200
        assert!((d.resolve(400.0).unwrap() - 200.0).abs() < 0.01);
    }

    #[test]
    fn dimension_clamp_resolve() {
        let d = Dimension::Clamp {
            min: CalcExpr::from_length(Pt::new(100.0)),
            val: CalcExpr::from_percent(0.5),
            max: CalcExpr::from_length(Pt::new(300.0)),
        };
        // clamp(100, 0.5 * 150, 300) = clamp(100, 75, 300) = 100
        assert!((d.resolve(150.0).unwrap() - 100.0).abs() < 0.01);
        // clamp(100, 0.5 * 400, 300) = clamp(100, 200, 300) = 200
        assert!((d.resolve(400.0).unwrap() - 200.0).abs() < 0.01);
        // clamp(100, 0.5 * 800, 300) = clamp(100, 400, 300) = 300
        assert!((d.resolve(800.0).unwrap() - 300.0).abs() < 0.01);
    }

    #[test]
    fn dimension_auto_returns_none() {
        assert!(Dimension::Auto.resolve(500.0).is_none());
    }

    #[test]
    fn length_percentage_calc() {
        let lp = LengthPercentage::Calc(CalcExpr {
            length: Pt::new(10.0),
            percent: 0.25,
        });
        assert!((lp.resolve(200.0) - 60.0).abs() < 0.01);
    }
}