use crate::model::{Columns, SectionProperties};
use crate::render::dimension::Pt;
use crate::render::geometry::{PtEdgeInsets, PtSize};
const SPEC_DEFAULT_PAGE_WIDTH: Pt = Pt::new(612.0);
const SPEC_DEFAULT_PAGE_HEIGHT: Pt = Pt::new(792.0);
const SPEC_DEFAULT_MARGIN: Pt = Pt::new(72.0);
#[derive(Debug, Clone, Copy)]
pub struct ColumnGeometry {
pub x_offset: Pt,
pub width: Pt,
}
#[derive(Debug, Clone)]
pub struct PageConfig {
pub page_size: PtSize,
pub margins: PtEdgeInsets,
pub header_margin: Pt,
pub footer_margin: Pt,
pub columns: Vec<ColumnGeometry>,
}
impl Default for PageConfig {
fn default() -> Self {
let content_width = SPEC_DEFAULT_PAGE_WIDTH - SPEC_DEFAULT_MARGIN - SPEC_DEFAULT_MARGIN;
Self {
page_size: PtSize::new(SPEC_DEFAULT_PAGE_WIDTH, SPEC_DEFAULT_PAGE_HEIGHT),
margins: PtEdgeInsets::new(
SPEC_DEFAULT_MARGIN,
SPEC_DEFAULT_MARGIN,
SPEC_DEFAULT_MARGIN,
SPEC_DEFAULT_MARGIN,
),
header_margin: SPEC_DEFAULT_MARGIN / 2.0,
footer_margin: SPEC_DEFAULT_MARGIN / 2.0,
columns: vec![ColumnGeometry {
x_offset: Pt::ZERO,
width: content_width,
}],
}
}
}
impl PageConfig {
pub fn from_section(sect: &SectionProperties) -> Self {
let mut cfg = Self::default();
if let Some(ref ps) = sect.page_size {
if let Some(w) = ps.width {
cfg.page_size.width = Pt::from(w);
}
if let Some(h) = ps.height {
cfg.page_size.height = Pt::from(h);
}
}
if let Some(ref pm) = sect.page_margins {
if let Some(t) = pm.top {
cfg.margins.top = Pt::from(t);
}
if let Some(r) = pm.right {
cfg.margins.right = Pt::from(r);
}
if let Some(b) = pm.bottom {
cfg.margins.bottom = Pt::from(b);
}
if let Some(l) = pm.left {
cfg.margins.left = Pt::from(l);
}
if let Some(h) = pm.header {
cfg.header_margin = Pt::from(h);
}
if let Some(f) = pm.footer {
cfg.footer_margin = Pt::from(f);
}
}
cfg.columns = compute_columns(cfg.content_width(), §.columns);
cfg
}
pub fn content_width(&self) -> Pt {
(self.page_size.width - self.margins.left - self.margins.right).max(Pt::ZERO)
}
pub fn num_columns(&self) -> usize {
self.columns.len()
}
pub fn content_height(&self) -> Pt {
(self.page_size.height - self.margins.top - self.margins.bottom).max(Pt::ZERO)
}
}
const DEFAULT_COLUMN_SPACE: Pt = Pt::new(36.0);
const MIN_COLUMN_WIDTH: Pt = Pt::new(1.0);
fn clamp_column_count(requested: u32, content_width: Pt, space: Pt) -> usize {
let requested = (requested as usize).max(1);
if content_width <= Pt::ZERO {
return 1;
}
let space = space.raw().max(0.0);
let max_by_page =
(((content_width.raw() + space) / (MIN_COLUMN_WIDTH.raw() + space)) as usize).max(1);
if requested > max_by_page {
log::warn!(
"§17.6.4: w:num={requested} does not fit a {content_width:?} text area at \
{space}pt spacing; clamped to {max_by_page}"
);
}
requested.min(max_by_page)
}
fn compute_columns(content_width: Pt, columns: &Option<Columns>) -> Vec<ColumnGeometry> {
let single = || {
vec![ColumnGeometry {
x_offset: Pt::ZERO,
width: content_width,
}]
};
let cols = match columns {
Some(c) if c.count.unwrap_or(1) > 1 => c,
_ => return single(),
};
let default_space = cols.space.map(Pt::from).unwrap_or(DEFAULT_COLUMN_SPACE);
let num = clamp_column_count(cols.count.unwrap_or(1), content_width, default_space);
if num <= 1 {
return single();
}
if cols.equal_width == Some(false) && !cols.columns.is_empty() {
let mut result = Vec::with_capacity(num);
let mut x = Pt::ZERO;
for i in 0..num {
let def = cols.columns.get(i);
let w = def
.and_then(|d| d.width)
.map(Pt::from)
.unwrap_or(content_width / num as f32);
result.push(ColumnGeometry {
x_offset: x,
width: w,
});
if i + 1 < num {
let gap = def
.and_then(|d| d.space)
.map(Pt::from)
.unwrap_or(default_space);
x += w + gap;
}
}
return result;
}
let total_gap = default_space * (num as f32 - 1.0);
let col_width = (content_width - total_gap) / num as f32;
let mut result = Vec::with_capacity(num);
for i in 0..num {
let x = (col_width + default_space) * i as f32;
result.push(ColumnGeometry {
x_offset: x,
width: col_width,
});
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::dimension::{Dimension, Twips};
use crate::model::{PageMargins, PageSize};
#[test]
fn default_is_us_letter() {
let cfg = PageConfig::default();
assert_eq!(cfg.page_size.width.raw(), 612.0);
assert_eq!(cfg.page_size.height.raw(), 792.0);
assert_eq!(cfg.margins.top.raw(), 72.0);
}
#[test]
fn content_dimensions() {
let cfg = PageConfig::default();
assert_eq!(cfg.content_width().raw(), 468.0); assert_eq!(cfg.content_height().raw(), 648.0); }
#[test]
fn from_section_with_page_size() {
let sect = SectionProperties {
page_size: Some(PageSize {
width: Some(Dimension::<Twips>::new(12240)), height: Some(Dimension::<Twips>::new(15840)), orientation: None,
}),
..Default::default()
};
let cfg = PageConfig::from_section(§);
assert_eq!(cfg.page_size.width.raw(), 612.0);
assert_eq!(cfg.page_size.height.raw(), 792.0);
}
#[test]
fn from_section_with_margins() {
let sect = SectionProperties {
page_margins: Some(PageMargins {
top: Some(Dimension::<Twips>::new(1440)), right: Some(Dimension::<Twips>::new(1440)),
bottom: Some(Dimension::<Twips>::new(1440)),
left: Some(Dimension::<Twips>::new(1440)),
header: Some(Dimension::<Twips>::new(720)), footer: Some(Dimension::<Twips>::new(720)),
gutter: None,
}),
..Default::default()
};
let cfg = PageConfig::from_section(§);
assert_eq!(cfg.margins.top.raw(), 72.0);
assert_eq!(cfg.header_margin.raw(), 36.0);
}
#[test]
fn from_section_partial_uses_defaults() {
let sect = SectionProperties {
page_margins: Some(PageMargins {
top: Some(Dimension::<Twips>::new(2880)), right: None,
bottom: None,
left: None,
header: None,
footer: None,
gutter: None,
}),
..Default::default()
};
let cfg = PageConfig::from_section(§);
assert_eq!(cfg.margins.top.raw(), 144.0, "custom top margin");
assert_eq!(cfg.margins.right.raw(), 72.0, "default right margin");
}
use crate::model::{ColumnDefinition, Columns};
fn cols_of(c: Columns) -> Vec<ColumnGeometry> {
compute_columns(Pt::new(468.0), &Some(c))
}
fn base() -> Columns {
Columns {
count: None,
space: None,
equal_width: None,
separator: None,
columns: vec![],
}
}
#[test]
fn no_columns_is_one_full_width_column() {
let g = compute_columns(Pt::new(468.0), &None);
assert_eq!(g.len(), 1);
assert_eq!(g[0].x_offset, Pt::ZERO);
assert_eq!(g[0].width, Pt::new(468.0));
let one = cols_of(Columns {
count: Some(1),
..base()
});
assert_eq!(one.len(), 1);
assert_eq!(one[0].width, Pt::new(468.0));
}
#[test]
fn equal_width_columns_split_the_remainder_after_gaps() {
let g = cols_of(Columns {
count: Some(3),
..base()
});
assert_eq!(g.len(), 3);
for c in &g {
assert_eq!(c.width, Pt::new(132.0));
}
assert_eq!(g[0].x_offset, Pt::ZERO);
assert_eq!(g[1].x_offset, Pt::new(168.0)); assert_eq!(g[2].x_offset, Pt::new(336.0));
assert_eq!(g[2].x_offset + g[2].width, Pt::new(468.0));
}
#[test]
fn explicit_space_overrides_the_default_gap() {
let g = cols_of(Columns {
count: Some(2),
space: Some(Dimension::<Twips>::new(1440)), ..base()
});
assert_eq!(g[0].width, Pt::new(198.0));
assert_eq!(g[1].x_offset, Pt::new(270.0)); }
#[test]
fn col_children_are_ignored_unless_equal_width_is_explicitly_false() {
let children = vec![
ColumnDefinition {
width: Some(Dimension::<Twips>::new(1440)), space: None,
},
ColumnDefinition {
width: Some(Dimension::<Twips>::new(7200)), space: None,
},
];
let absent = cols_of(Columns {
count: Some(2),
columns: children.clone(),
..base()
});
assert_eq!(absent[0].width, absent[1].width, "absent means equal");
assert_eq!(absent[0].width, Pt::new(216.0));
let opted_out = cols_of(Columns {
count: Some(2),
equal_width: Some(false),
columns: children,
..base()
});
assert_eq!(opted_out[0].width, Pt::new(72.0));
assert_eq!(opted_out[1].width, Pt::new(360.0));
assert_eq!(opted_out[1].x_offset, Pt::new(108.0)); }
#[test]
fn per_column_space_applies_to_its_own_gap_only() {
let g = cols_of(Columns {
count: Some(3),
equal_width: Some(false),
columns: vec![
ColumnDefinition {
width: Some(Dimension::<Twips>::new(2000)), space: Some(Dimension::<Twips>::new(200)), },
ColumnDefinition {
width: Some(Dimension::<Twips>::new(2000)),
space: None, },
ColumnDefinition {
width: Some(Dimension::<Twips>::new(2000)),
space: Some(Dimension::<Twips>::new(9999)), },
],
..base()
});
assert_eq!(g[0].x_offset, Pt::ZERO);
assert_eq!(g[1].x_offset, Pt::new(110.0)); assert_eq!(g[2].x_offset, Pt::new(246.0)); }
#[test]
fn num_wins_when_col_children_are_missing_or_extra() {
let short = cols_of(Columns {
count: Some(3),
equal_width: Some(false),
columns: vec![ColumnDefinition {
width: Some(Dimension::<Twips>::new(1440)), space: None,
}],
..base()
});
assert_eq!(short.len(), 3, "w:num=3 yields three columns");
assert_eq!(short[0].width, Pt::new(72.0), "the declared one");
assert_eq!(short[1].width, Pt::new(156.0), "defaulted: 468/3");
assert_eq!(short[2].width, Pt::new(156.0));
let extra = cols_of(Columns {
count: Some(2),
equal_width: Some(false),
columns: vec![
ColumnDefinition {
width: Some(Dimension::<Twips>::new(1440)),
space: None,
},
ColumnDefinition {
width: Some(Dimension::<Twips>::new(1440)),
space: None,
},
ColumnDefinition {
width: Some(Dimension::<Twips>::new(1440)),
space: None,
},
],
..base()
});
assert_eq!(extra.len(), 2, "children beyond w:num are ignored");
}
#[test]
fn absurd_column_counts_are_bounded_by_the_page() {
let g = cols_of(Columns {
count: Some(u32::MAX),
..base()
});
assert!(
g.len() <= 468,
"468pt of text area at 1pt minimum caps the count, got {}",
g.len()
);
let degenerate = compute_columns(
Pt::ZERO,
&Some(Columns {
count: Some(u32::MAX),
..base()
}),
);
assert_eq!(degenerate.len(), 1);
}
#[test]
fn column_count_never_yields_a_negative_width() {
for n in [2u32, 13, 14, 15, 16, 50, 1000, u32::MAX] {
let g = cols_of(Columns {
count: Some(n),
..base()
});
assert!(!g.is_empty(), "n={n}");
for (i, c) in g.iter().enumerate() {
assert!(
c.width >= Pt::ZERO,
"n={n} col{i} width {:?} is negative",
c.width
);
}
}
}
#[test]
fn margins_wider_than_the_page_clamp_to_zero_not_negative() {
let sect = SectionProperties {
page_size: Some(PageSize {
width: Some(Dimension::<Twips>::new(2000)), height: Some(Dimension::<Twips>::new(2000)),
orientation: None,
}),
page_margins: Some(PageMargins {
left: Some(Dimension::<Twips>::new(1440)), right: Some(Dimension::<Twips>::new(1440)), top: Some(Dimension::<Twips>::new(1440)),
bottom: Some(Dimension::<Twips>::new(1440)),
header: None,
footer: None,
gutter: None,
}),
..Default::default()
};
let cfg = PageConfig::from_section(§);
assert_eq!(cfg.content_width(), Pt::ZERO, "clamped, not -44pt");
assert_eq!(cfg.content_height(), Pt::ZERO);
assert!(cfg.columns.iter().all(|c| c.width >= Pt::ZERO));
}
#[test]
fn spacing_wider_than_the_page_yields_one_column_not_negative_ones() {
let g = cols_of(Columns {
count: Some(2),
space: Some(Dimension::<Twips>::new(20000)), ..base()
});
assert_eq!(g.len(), 1, "two columns cannot fit a 1000pt gap");
assert!(g[0].width >= Pt::ZERO);
}
#[test]
fn admitted_counts_leave_every_column_usably_wide() {
let g = cols_of(Columns {
count: Some(13),
..base()
});
assert_eq!(g.len(), 13);
for c in &g {
assert!(c.width >= MIN_COLUMN_WIDTH, "got {:?}", c.width);
}
let over = cols_of(Columns {
count: Some(14),
..base()
});
assert_eq!(over.len(), 13, "clamped to what fits");
}
}