scrin 0.1.1

A terminal UI toolkit with panes, widgets, overlays, animations, and Aisling-powered effects/loaders.
Documentation
use crate::core::rect::Rect;

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Constraint {
    Length(u16),
    Min(u16),
    Max(u16),
    Percentage(u16),
    Ratio(u32, u32),
    Fill(u16),
}

impl Constraint {
    fn apply(&self, total: u16) -> u16 {
        match *self {
            Constraint::Length(l) => l.min(total),
            Constraint::Min(m) => total.max(m),
            Constraint::Max(m) => total.min(m),
            Constraint::Percentage(p) => (total as f32 * p as f32 / 100.0) as u16,
            Constraint::Ratio(a, b) => {
                if b == 0 {
                    0
                } else {
                    (total as f32 * a as f32 / b as f32) as u16
                }
            }
            Constraint::Fill(gap) => {
                if gap == 0 {
                    total
                } else {
                    (total / gap).max(1) * gap
                }
            }
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Direction {
    Horizontal,
    Vertical,
}

#[derive(Debug, Clone, PartialEq)]
pub struct Layout {
    pub direction: Direction,
    pub constraints: Vec<Constraint>,
    pub margin: Rect,
}

impl Layout {
    pub fn default() -> Self {
        Self {
            direction: Direction::Vertical,
            constraints: Vec::new(),
            margin: Rect::ZERO,
        }
    }

    pub fn horizontal(constraints: Vec<Constraint>) -> Self {
        Self {
            direction: Direction::Horizontal,
            constraints,
            margin: Rect::ZERO,
        }
    }

    pub fn vertical(constraints: Vec<Constraint>) -> Self {
        Self {
            direction: Direction::Vertical,
            constraints,
            margin: Rect::ZERO,
        }
    }

    pub fn margin(mut self, margin: Rect) -> Self {
        self.margin = margin;
        self
    }

    pub fn split(&self, area: Rect) -> Vec<Rect> {
        let inner = area.inner(self.margin);
        match self.direction {
            Direction::Horizontal => self.split_horizontal(inner),
            Direction::Vertical => self.split_vertical(inner),
        }
    }

    fn split_horizontal(&self, area: Rect) -> Vec<Rect> {
        let total = area.width;
        let gap = self.margin.x;
        let mut remaining = total;
        let mut positions = Vec::with_capacity(self.constraints.len());

        for constraint in &self.constraints {
            let size = constraint.apply(remaining);
            positions.push(size);
            remaining = remaining.saturating_sub(size + gap);
        }

        let mut rects = Vec::with_capacity(self.constraints.len());
        let mut x = area.x;

        for &size in &positions {
            rects.push(Rect::new(x, area.y, size, area.height));
            x = x.saturating_add(size + gap);
        }

        rects
    }

    fn split_vertical(&self, area: Rect) -> Vec<Rect> {
        let total = area.height;
        let gap = self.margin.y;
        let mut remaining = total;
        let mut positions = Vec::with_capacity(self.constraints.len());

        for constraint in &self.constraints {
            let size = constraint.apply(remaining);
            positions.push(size);
            remaining = remaining.saturating_sub(size + gap);
        }

        let mut rects = Vec::with_capacity(self.constraints.len());
        let mut y = area.y;

        for &size in &positions {
            rects.push(Rect::new(area.x, y, area.width, size));
            y = y.saturating_add(size + gap);
        }

        rects
    }
}

impl Default for Layout {
    fn default() -> Self {
        Self::default()
    }
}

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

    #[test]
    fn test_constraint_length() {
        assert_eq!(Constraint::Length(5).apply(20), 5);
        assert_eq!(Constraint::Length(25).apply(20), 20);
    }

    #[test]
    fn test_constraint_percentage() {
        assert_eq!(Constraint::Percentage(50).apply(20), 10);
        assert_eq!(Constraint::Percentage(100).apply(20), 20);
    }

    #[test]
    fn test_layout_split_vertical() {
        let layout = Layout::vertical(vec![
            Constraint::Length(3),
            Constraint::Length(5),
            Constraint::Min(0),
        ]);
        let area = Rect::new(0, 0, 40, 20);
        let rects = layout.split(area);
        assert_eq!(rects.len(), 3);
        assert_eq!(rects[0].height, 3);
        assert_eq!(rects[1].height, 5);
        assert_eq!(rects[2].y, 8);
    }

    #[test]
    fn test_layout_split_horizontal() {
        let layout = Layout::horizontal(vec![
            Constraint::Percentage(33),
            Constraint::Percentage(33),
            Constraint::Min(0),
        ]);
        let area = Rect::new(0, 0, 30, 10);
        let rects = layout.split(area);
        assert_eq!(rects.len(), 3);
        assert_eq!(rects[0].width, 9);
        assert_eq!(rects[0].height, 10);
    }
}