paperforge-layout 0.2.0

High-level document layout engine
Documentation
use paperforge_core::{Color, Rect};

use crate::element::Align;

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BorderStyle {
    None,
    Solid,
    Dashed,
    Dotted,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Border {
    pub top: BorderStyle,
    pub bottom: BorderStyle,
    pub left: BorderStyle,
    pub right: BorderStyle,
    pub width: f64,
    pub color: Color,
}

impl Default for Border {
    fn default() -> Self {
        Self {
            top: BorderStyle::None,
            bottom: BorderStyle::None,
            left: BorderStyle::None,
            right: BorderStyle::None,
            width: 1.0,
            color: Color::black(),
        }
    }
}

#[derive(Debug, Clone)]
pub struct Cell {
    pub text: String,
    pub align: Align,
    pub font_size: f64,
    pub color: Color,
    pub border: Border,
    pub bg_color: Option<Color>,
    pub padding: f64,
    pub col_span: u32,
    pub row_span: u32,
}

impl Cell {
    pub fn new(text: &str) -> Self {
        Self {
            text: text.to_string(),
            align: Align::Left,
            font_size: 11.0,
            color: Color::black(),
            border: Border::default(),
            bg_color: None,
            padding: 5.0,
            col_span: 1,
            row_span: 1,
        }
    }

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

    pub fn font_size(mut self, size: f64) -> Self {
        self.font_size = size;
        self
    }

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

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

    pub fn bg_color(mut self, color: Color) -> Self {
        self.bg_color = Some(color);
        self
    }

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

    pub fn col_span(mut self, span: u32) -> Self {
        self.col_span = span;
        self
    }

    pub fn row_span(mut self, span: u32) -> Self {
        self.row_span = span;
        self
    }
}

impl Default for Cell {
    fn default() -> Self {
        Self::new("")
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum HeaderPosition {
    #[default]
    Top,
    Bottom,
    None,
}

#[derive(Debug, Clone)]
pub struct TableConfig {
    pub headers: Vec<Cell>,
    pub rows: Vec<Vec<Cell>>,
    pub column_widths: Vec<f64>,
    pub row_height: f64,
    pub header_position: HeaderPosition,
    pub border: Border,
    pub header_bg: Option<Color>,
    pub width: f64,
    pub x: f64,
    pub y: f64,
}

impl TableConfig {
    pub fn new() -> Self {
        Self {
            headers: Vec::new(),
            rows: Vec::new(),
            column_widths: Vec::new(),
            row_height: 20.0,
            header_position: HeaderPosition::Top,
            border: Border::default(),
            header_bg: None,
            width: 400.0,
            x: 50.0,
            y: 700.0,
        }
    }

    pub fn headers(mut self, headers: Vec<Cell>) -> Self {
        self.headers = headers;
        self
    }

    pub fn header_labels(mut self, labels: &[&str]) -> Self {
        self.headers = labels.iter().map(|s| Cell::new(s)).collect();
        self
    }

    pub fn rows(mut self, rows: Vec<Vec<Cell>>) -> Self {
        self.rows = rows;
        self
    }

    pub fn row(mut self, cells: Vec<Cell>) -> Self {
        self.rows.push(cells);
        self
    }

    pub fn column_widths(mut self, widths: Vec<f64>) -> Self {
        self.column_widths = widths;
        self
    }

    pub fn auto_width(self, total_width: f64, n_cols: usize) -> Self {
        let width = total_width / n_cols as f64;
        let column_widths = vec![width; n_cols];
        TableConfig {
            column_widths,
            ..self
        }
    }

    pub fn row_height(mut self, height: f64) -> Self {
        self.row_height = height;
        self
    }

    pub fn header_position(mut self, pos: HeaderPosition) -> Self {
        self.header_position = pos;
        self
    }

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

    pub fn header_bg(mut self, color: Color) -> Self {
        self.header_bg = Some(color);
        self
    }

    pub fn position(mut self, x: f64, y: f64) -> Self {
        self.x = x;
        self.y = y;
        self
    }

    pub fn width(mut self, w: f64) -> Self {
        self.width = w;
        self
    }

    pub fn compute_widths(&self) -> Vec<f64> {
        if !self.column_widths.is_empty() {
            return self.column_widths.clone();
        }
        let n_cols = self
            .headers
            .len()
            .max(self.rows.iter().map(|r| r.len()).max().unwrap_or(0));
        if n_cols == 0 {
            return Vec::new();
        }
        vec![self.width / n_cols as f64; n_cols]
    }

    pub fn column_count(&self) -> usize {
        let header_cols = self.headers.len();
        let max_row_cols = self.rows.iter().map(|r| r.len()).max().unwrap_or(0);
        header_cols.max(max_row_cols)
    }

    pub fn cell_bounds(&self, row: usize, col: usize) -> Option<Rect> {
        let widths = self.compute_widths();
        let n_cols = widths.len();
        if col >= n_cols {
            return None;
        }
        let x = self.x + (0..col).map(|i| widths[i]).sum::<f64>();
        let y = self.y - (row as f64) * self.row_height;
        Some(Rect::new(x, y, widths[col], self.row_height))
    }
}

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

pub struct TableBuilder {
    table: TableConfig,
}

impl TableBuilder {
    pub fn new() -> Self {
        Self {
            table: TableConfig::new(),
        }
    }

    pub fn headers(mut self, header_labels: &[&str]) -> Self {
        self.table.headers = header_labels.iter().map(|s| Cell::new(s)).collect();
        self
    }

    pub fn header_cells(mut self, cells: Vec<Cell>) -> Self {
        self.table.headers = cells;
        self
    }

    pub fn row(mut self, cells: Vec<Cell>) -> Self {
        self.table.rows.push(cells);
        self
    }

    pub fn row_str(mut self, cells: &[&str]) -> Self {
        self.table
            .rows
            .push(cells.iter().map(|s| Cell::new(s)).collect());
        self
    }

    pub fn column_widths(mut self, widths: Vec<f64>) -> Self {
        self.table.column_widths = widths;
        self
    }

    pub fn row_height(mut self, height: f64) -> Self {
        self.table.row_height = height;
        self
    }

    pub fn border(mut self, border: Border) -> Self {
        self.table.border = border;
        self
    }

    pub fn header_bg(mut self, color: Color) -> Self {
        self.table.header_bg = Some(color);
        self
    }

    pub fn position(mut self, x: f64, y: f64) -> Self {
        self.table.x = x;
        self.table.y = y;
        self
    }

    pub fn width(mut self, w: f64) -> Self {
        self.table.width = w;
        self
    }

    pub fn header_position(mut self, pos: HeaderPosition) -> Self {
        self.table.header_position = pos;
        self
    }

    pub fn build(self) -> TableConfig {
        let widths = self.table.compute_widths();
        if self.table.column_widths.is_empty() && widths.is_empty() {
            let mut t = self.table.clone();
            t.column_widths = vec![];
            return t;
        }
        self.table
    }
}

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

pub fn all_border(style: BorderStyle, width: f64, color: Color) -> Border {
    Border {
        top: style,
        bottom: style,
        left: style,
        right: style,
        width,
        color,
    }
}

pub fn grid_border(color: Color) -> Border {
    all_border(BorderStyle::Solid, 0.5, color)
}

pub fn outer_border(color: Color) -> Border {
    Border {
        top: BorderStyle::Solid,
        bottom: BorderStyle::Solid,
        left: BorderStyle::Solid,
        right: BorderStyle::Solid,
        width: 1.0,
        color,
    }
}