use crate::graphics::GraphicsElem;
use crate::hbox::{HorzBox, PureHorzBox};
use crate::length::Length;
use crate::linebreak::{fit_cell, natural_metrics};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Paddings {
pub l: Length,
pub r: Length,
pub t: Length,
pub b: Length,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Cell {
Normal(Paddings, Vec<HorzBox>),
Empty,
Multi(usize, usize, Paddings, Vec<HorzBox>),
}
#[derive(Clone, Debug, PartialEq, syan::visit::Ast)]
#[subast(crate::hbox::PureHorzBox)]
pub struct TabularCellBox {
pub x: Length,
pub baseline_y: Length,
pub contents: Vec<(Length, PureHorzBox)>,
}
#[derive(Clone, Debug, PartialEq, syan::visit::Ast)]
#[subast(crate::tabular::TabularCellBox, crate::graphics::GraphicsElem)]
pub struct TabularBox {
pub width: Length,
pub height: Length,
pub depth: Length,
pub cells: Vec<TabularCellBox>,
pub rules: Vec<GraphicsElem>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Solved {
pub width: Length,
pub height: Length,
pub cells: Vec<TabularCellBox>,
pub xs: Vec<Length>,
pub ys: Vec<Length>,
}
type RestRow = Vec<Option<(usize, Length)>>;
type RestCol = Vec<Option<(usize, Length)>>;
fn normalize_tabular(rows: Vec<Vec<Cell>>) -> (usize, Vec<Vec<Cell>>) {
let ncols = rows.iter().map(|r| r.len()).max().unwrap_or(0);
let htabular = rows
.into_iter()
.map(|mut row| {
while row.len() < ncols {
row.push(Cell::Empty);
}
row
})
.collect();
(ncols, htabular)
}
fn transpose(rows: &[Vec<Cell>], ncols: usize) -> Vec<Vec<&Cell>> {
(0..ncols)
.map(|c| rows.iter().map(|row| &row[c]).collect())
.collect()
}
fn determine_row_metrics(restprev: &RestRow, row: &[Cell]) -> (RestRow, Length, Length) {
let mut restacc: RestRow = Vec::with_capacity(row.len());
let mut hgt_max = Length::ZERO;
let mut dpt_mag_max = Length::ZERO;
for (slot, cell) in restprev.iter().zip(row.iter()) {
match (slot, cell) {
(None, Cell::Normal(pads, content)) => {
let (_, hgt, dpt) = natural_metrics(content);
hgt_max = hgt_max.max(hgt + pads.t);
dpt_mag_max = dpt_mag_max.max(dpt + pads.b);
restacc.push(None);
}
(None, Cell::Empty) => restacc.push(None),
(None, Cell::Multi(nr, _nc, pads, content)) => {
let (_, hgt, dpt) = natural_metrics(content);
let len = (hgt + pads.t) + (dpt + pads.b);
let nr = (*nr).max(1);
let restelem = if nr == 1 { None } else { Some((nr, len)) };
restacc.push(restelem);
}
(Some((numrow, len)), Cell::Empty) => {
restacc.push(Some((*numrow, *len)));
}
(Some(_), Cell::Normal(pads, content)) => {
let (_, hgt, dpt) = natural_metrics(content);
hgt_max = hgt_max.max(hgt + pads.t);
dpt_mag_max = dpt_mag_max.max(dpt + pads.b);
restacc.push(None);
}
(Some(_), Cell::Multi(nr, _nc, pads, content)) => {
let (_, hgt, dpt) = natural_metrics(content);
let len = (hgt + pads.t) + (dpt + pads.b);
let nr = (*nr).max(1);
let restelem = if nr == 1 { None } else { Some((nr, len)) };
restacc.push(restelem);
}
}
}
let rest = restacc
.into_iter()
.map(|slot| match slot {
None => None,
Some((1, _)) => None,
Some((numrow, len)) => Some((numrow - 1, len - hgt_max - dpt_mag_max)),
})
.collect();
(rest, hgt_max, dpt_mag_max)
}
fn determine_column_width(restprev: &RestCol, col: &[&Cell]) -> (RestCol, Length) {
let mut restacc: RestCol = Vec::with_capacity(col.len());
let mut wid_max = Length::ZERO;
for (slot, cell) in restprev.iter().zip(col.iter()) {
match (slot, cell) {
(None, Cell::Normal(pads, content)) => {
let (wid, _, _) = natural_metrics(content);
wid_max = wid_max.max(pads.l + wid + pads.r);
restacc.push(None);
}
(None, Cell::Empty) => restacc.push(None),
(None, Cell::Multi(_nr, nc, pads, content)) => {
let (widraw, _, _) = natural_metrics(content);
let wid = pads.l + widraw + pads.r;
let nc = (*nc).max(1);
if nc == 1 {
wid_max = wid_max.max(wid);
}
restacc.push(Some((nc, wid)));
}
(Some((numcol, widrest)), Cell::Empty) => {
let numcol = *numcol;
if numcol == 1 {
wid_max = wid_max.max(*widrest);
}
restacc.push(Some((numcol, *widrest)));
}
(Some(_), Cell::Normal(pads, content)) => {
let (wid, _, _) = natural_metrics(content);
wid_max = wid_max.max(pads.l + wid + pads.r);
restacc.push(None);
}
(Some(_), Cell::Multi(_nr, nc, pads, content)) => {
let (widraw, _, _) = natural_metrics(content);
let wid = pads.l + widraw + pads.r;
let nc = (*nc).max(1);
if nc == 1 {
wid_max = wid_max.max(wid);
}
restacc.push(Some((nc, wid)));
}
}
}
let rest = restacc
.into_iter()
.map(|slot| match slot {
None => None,
Some((1, _)) => None,
Some((numcol, wid)) => Some((numcol - 1, wid - wid_max)),
})
.collect();
(rest, wid_max)
}
fn multi_cell_width(widlst: &[Length], index_c: usize, nc: usize) -> Length {
if widlst.is_empty() {
return Length::ZERO;
}
let end = (index_c + nc).saturating_sub(1).min(widlst.len() - 1);
widlst[index_c.min(end)..=end]
.iter()
.fold(Length::ZERO, |acc, w| acc + *w)
}
fn multi_cell_vertical(vmetrlst: &[(Length, Length)], index_r: usize, nr: usize) -> Length {
if vmetrlst.is_empty() {
return Length::ZERO;
}
let end = (index_r + nr).saturating_sub(1).min(vmetrlst.len() - 1);
vmetrlst[index_r.min(end)..=end]
.iter()
.fold(Length::ZERO, |acc, (hgt, dpt)| acc + *hgt + *dpt)
}
fn pad_content(pads: Paddings, content: Vec<HorzBox>) -> Vec<HorzBox> {
let mut out = Vec::with_capacity(content.len() + 2);
out.push(HorzBox::Pure(PureHorzBox::FixedEmpty { width: pads.l }));
out.extend(content);
out.push(HorzBox::Pure(PureHorzBox::FixedEmpty { width: pads.r }));
out
}
fn solidify_tabular(
vmetrlst: &[(Length, Length)],
widlst: &[Length],
xs: &[Length],
ys: &[Length],
htabular: Vec<Vec<Cell>>,
) -> Vec<TabularCellBox> {
let mut cells = Vec::new();
for (index_r, row) in htabular.into_iter().enumerate() {
let hgt_row = vmetrlst
.get(index_r)
.map(|(h, _)| *h)
.unwrap_or(Length::ZERO);
let row_top = ys.get(index_r).copied().unwrap_or(Length::ZERO);
for (index_c, cell) in row.into_iter().enumerate() {
let x = xs.get(index_c).copied().unwrap_or(Length::ZERO);
match cell {
Cell::Empty => {}
Cell::Normal(pads, content) => {
let wid = widlst.get(index_c).copied().unwrap_or(Length::ZERO);
let padded = pad_content(pads, content);
let (contents, _fit_hgt, _fit_dpt) = fit_cell(padded, wid);
let baseline_y = row_top - hgt_row;
cells.push(TabularCellBox {
x,
baseline_y,
contents,
});
}
Cell::Multi(nr, nc, pads, content) => {
let nr = nr.max(1);
let nc = nc.max(1);
let wid = multi_cell_width(widlst, index_c, nc);
let padded = pad_content(pads, content);
let (contents, fit_hgt, fit_dpt) = fit_cell(padded, wid);
let hgt_cell = if nr == 1 {
hgt_row
} else {
let vlen_cell = multi_cell_vertical(vmetrlst, index_r, nr);
let vlen_content = fit_hgt + fit_dpt;
let lenspace = (vlen_cell - vlen_content) * 0.5;
fit_hgt + lenspace
};
let baseline_y = row_top - hgt_cell;
cells.push(TabularCellBox {
x,
baseline_y,
contents,
});
}
}
}
}
cells
}
pub fn main(rows: Vec<Vec<Cell>>) -> Solved {
let nrows = rows.len();
let (ncols, htabular) = normalize_tabular(rows);
let mut restrow: RestRow = vec![None; ncols];
let mut vmetrlst: Vec<(Length, Length)> = Vec::with_capacity(nrows);
for row in &htabular {
let (rest, hgt, dpt) = determine_row_metrics(&restrow, row);
restrow = rest;
vmetrlst.push((hgt, dpt));
}
let vtabular = transpose(&htabular, ncols);
let mut restcol: RestCol = vec![None; nrows];
let mut widlst: Vec<Length> = Vec::with_capacity(ncols);
for col in &vtabular {
let (rest, wid) = determine_column_width(&restcol, col);
restcol = rest;
widlst.push(wid);
}
let width = widlst.iter().fold(Length::ZERO, |acc, w| acc + *w);
let height = vmetrlst
.iter()
.fold(Length::ZERO, |acc, (h, d)| acc + *h + *d);
let mut xs = Vec::with_capacity(ncols + 1);
xs.push(Length::ZERO);
let mut x = Length::ZERO;
for w in &widlst {
x = x + *w;
xs.push(x);
}
let mut ys = Vec::with_capacity(nrows + 1);
ys.push(height);
let mut y = height;
for (hgt, dpt) in &vmetrlst {
y = y - (*hgt + *dpt);
ys.push(y);
}
let cells = solidify_tabular(&vmetrlst, &widlst, &xs, &ys, htabular);
Solved {
width,
height,
cells,
xs,
ys,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn probe(w: f64, h: f64, d: f64) -> Vec<HorzBox> {
vec![HorzBox::Pure(PureHorzBox::Graphics {
width: Length::pt(w),
height: Length::pt(h),
depth: Length::pt(d),
elems: Vec::new(),
origin_independent: false,
})]
}
fn zero_pads() -> Paddings {
Paddings {
l: Length::ZERO,
r: Length::ZERO,
t: Length::ZERO,
b: Length::ZERO,
}
}
#[test]
fn two_by_two_normal_grid_geometry() {
let rows = vec![
vec![
Cell::Normal(zero_pads(), probe(30.0, 12.0, 3.0)),
Cell::Normal(zero_pads(), probe(20.0, 8.0, 2.0)),
],
vec![
Cell::Normal(zero_pads(), probe(25.0, 10.0, 4.0)),
Cell::Normal(zero_pads(), probe(15.0, 6.0, 1.0)),
],
];
let solved = main(rows);
assert_eq!(solved.width, Length::pt(50.0));
assert_eq!(solved.height, Length::pt(29.0));
assert_eq!(
solved.xs,
vec![Length::pt(0.0), Length::pt(30.0), Length::pt(50.0)]
);
assert_eq!(
solved.ys,
vec![Length::pt(29.0), Length::pt(14.0), Length::pt(0.0)]
);
assert_eq!(solved.cells.len(), 4);
assert_eq!(solved.cells[0].x, Length::pt(0.0));
assert_eq!(solved.cells[0].baseline_y, Length::pt(17.0));
assert_eq!(solved.cells[1].x, Length::pt(30.0));
assert_eq!(solved.cells[1].baseline_y, Length::pt(17.0));
assert_eq!(solved.cells[2].x, Length::pt(0.0));
assert_eq!(solved.cells[2].baseline_y, Length::pt(4.0));
assert_eq!(solved.cells[3].x, Length::pt(30.0));
assert_eq!(solved.cells[3].baseline_y, Length::pt(4.0));
}
#[test]
fn empty_cell_produces_no_box() {
let rows = vec![vec![
Cell::Normal(zero_pads(), probe(10.0, 5.0, 1.0)),
Cell::Empty,
]];
let solved = main(rows);
assert_eq!(solved.cells.len(), 1);
assert_eq!(solved.xs.len(), 3);
}
#[test]
fn multi_column_span_absorbs_following_empty() {
let rows = vec![
vec![
Cell::Multi(1, 2, zero_pads(), probe(50.0, 10.0, 2.0)),
Cell::Empty,
],
vec![
Cell::Normal(zero_pads(), probe(20.0, 5.0, 1.0)),
Cell::Normal(zero_pads(), probe(25.0, 6.0, 1.0)),
],
];
let solved = main(rows);
assert_eq!(
solved.xs,
vec![Length::pt(0.0), Length::pt(20.0), Length::pt(50.0)]
);
assert_eq!(solved.cells.len(), 3);
assert_eq!(solved.cells[0].x, Length::pt(0.0));
}
#[test]
fn multi_row_span_centers_content_across_the_rows_it_spans() {
let rows = vec![
vec![
Cell::Normal(zero_pads(), probe(20.0, 10.0, 2.0)),
Cell::Normal(zero_pads(), probe(15.0, 8.0, 1.0)),
],
vec![
Cell::Multi(2, 1, zero_pads(), probe(12.0, 6.0, 1.0)),
Cell::Normal(zero_pads(), probe(15.0, 9.0, 3.0)),
],
vec![
Cell::Empty,
Cell::Normal(zero_pads(), probe(15.0, 7.0, 2.0)),
],
];
let solved = main(rows);
assert_eq!(solved.height, Length::pt(33.0));
assert_eq!(
solved.ys,
vec![
Length::pt(33.0),
Length::pt(21.0),
Length::pt(9.0),
Length::pt(0.0)
]
);
assert_eq!(
solved.xs,
vec![Length::pt(0.0), Length::pt(20.0), Length::pt(35.0)]
);
assert_eq!(solved.cells.len(), 5);
assert_eq!(solved.cells[0].baseline_y, Length::pt(23.0)); assert_eq!(solved.cells[1].baseline_y, Length::pt(23.0));
assert_eq!(solved.cells[2].x, Length::pt(0.0));
assert_eq!(solved.cells[2].baseline_y, Length::pt(8.0));
assert_eq!(solved.cells[3].baseline_y, Length::pt(12.0)); assert_eq!(solved.cells[4].baseline_y, Length::pt(2.0)); }
#[test]
fn tabular_box_measures_as_a_single_leaf() {
let rows = vec![vec![Cell::Normal(zero_pads(), probe(30.0, 12.0, 3.0))]];
let solved = main(rows);
let tab = TabularBox {
width: solved.width,
height: solved.height,
depth: Length::ZERO,
cells: solved.cells,
rules: Vec::new(),
};
let bx = HorzBox::Pure(PureHorzBox::Tabular(tab.clone()));
assert_eq!(
crate::linebreak::natural_metrics(std::slice::from_ref(&bx)),
(tab.width, tab.height, Length::ZERO)
);
let HorzBox::Pure(p) = &bx;
assert!(!p.is_glue());
assert_eq!(p.natural_width(), tab.width);
}
}