use crate::layout_result::ComputedLayout;
use crate::tree::StyledTree;
use crate::units::Pt;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PageMargins {
pub top: Pt,
pub right: Pt,
pub bottom: Pt,
pub left: Pt,
}
impl PageMargins {
pub const ZERO: Self = Self {
top: Pt::ZERO,
right: Pt::ZERO,
bottom: Pt::ZERO,
left: Pt::ZERO,
};
pub const STANDARD: Self = Self {
top: Pt::new(72.0),
right: Pt::new(72.0),
bottom: Pt::new(72.0),
left: Pt::new(72.0),
};
#[must_use]
pub const fn uniform(size: Pt) -> Self {
Self {
top: size,
right: size,
bottom: size,
left: size,
}
}
#[must_use]
pub const fn symmetric(vertical: Pt, horizontal: Pt) -> Self {
Self {
top: vertical,
right: horizontal,
bottom: vertical,
left: horizontal,
}
}
#[must_use]
pub fn horizontal(&self) -> Pt {
self.left + self.right
}
#[must_use]
pub fn vertical(&self) -> Pt {
self.top + self.bottom
}
pub fn validate(&self, page_width: Pt, page_height: Pt) -> Option<String> {
if self.top.get() < 0.0
|| self.right.get() < 0.0
|| self.bottom.get() < 0.0
|| self.left.get() < 0.0
{
return Some("page margins must be non-negative".into());
}
if self.horizontal().get() >= page_width.get() {
return Some(format!(
"horizontal margins ({:.1}pt) exceed page width ({:.1}pt)",
self.horizontal().get(),
page_width.get()
));
}
if self.vertical().get() >= page_height.get() {
return Some(format!(
"vertical margins ({:.1}pt) exceed page height ({:.1}pt)",
self.vertical().get(),
page_height.get()
));
}
None
}
}
impl Default for PageMargins {
fn default() -> Self {
Self::ZERO
}
}
#[derive(Debug, Clone)]
pub enum FirstPageContent {
Custom(StyledTree),
Suppress,
}
#[derive(Debug, Clone, Default)]
pub struct PageTemplate {
pub margins: PageMargins,
pub header: Option<StyledTree>,
pub footer: Option<StyledTree>,
pub first_page_header: Option<FirstPageContent>,
pub first_page_footer: Option<FirstPageContent>,
pub left_page_header: Option<StyledTree>,
pub left_page_footer: Option<StyledTree>,
pub right_page_header: Option<StyledTree>,
pub right_page_footer: Option<StyledTree>,
}
#[derive(Debug, Clone)]
pub struct HeaderFooterLayout {
pub tree: StyledTree,
pub layout: ComputedLayout,
pub height: Pt,
}
#[derive(Debug, Clone)]
pub struct PageDecorations {
pub margins: PageMargins,
pub content_offset_y: Pt,
pub content_offset_x: Pt,
pub header: Option<HeaderFooterLayout>,
pub footer: Option<HeaderFooterLayout>,
pub first_page_header: Option<Option<HeaderFooterLayout>>,
pub first_page_footer: Option<Option<HeaderFooterLayout>>,
pub left_page_header: Option<HeaderFooterLayout>,
pub left_page_footer: Option<HeaderFooterLayout>,
pub right_page_header: Option<HeaderFooterLayout>,
pub right_page_footer: Option<HeaderFooterLayout>,
pub total_pages: u32,
}
impl PageDecorations {
pub fn content_area_height(&self, page_height: Pt) -> Pt {
let footer_h = self.footer.as_ref().map_or(Pt::ZERO, |f| f.height);
page_height - self.content_offset_y - footer_h - self.margins.bottom
}
pub fn header_for_page(&self, page_idx: usize) -> Option<&HeaderFooterLayout> {
if page_idx == 0 {
match &self.first_page_header {
Some(Some(hf)) => return Some(hf),
Some(None) => return None,
None => {} }
}
let display_page = page_idx + 1;
if display_page % 2 == 1 {
if let Some(ref hf) = self.right_page_header {
return Some(hf);
}
} else {
if let Some(ref hf) = self.left_page_header {
return Some(hf);
}
}
self.header.as_ref()
}
pub fn footer_for_page(&self, page_idx: usize) -> Option<&HeaderFooterLayout> {
if page_idx == 0 {
match &self.first_page_footer {
Some(Some(hf)) => return Some(hf),
Some(None) => return None,
None => {}
}
}
let display_page = page_idx + 1;
if display_page % 2 == 1 {
if let Some(ref hf) = self.right_page_footer {
return Some(hf);
}
} else {
if let Some(ref hf) = self.left_page_footer {
return Some(hf);
}
}
self.footer.as_ref()
}
pub fn footer_y(&self, page_height: Pt, footer_height: Pt) -> Pt {
page_height - self.margins.bottom - footer_height
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn margins_zero_default() {
assert_eq!(PageMargins::default(), PageMargins::ZERO);
}
#[test]
fn margins_horizontal_vertical() {
let m = PageMargins {
top: Pt::new(10.0),
right: Pt::new(20.0),
bottom: Pt::new(30.0),
left: Pt::new(40.0),
};
assert_eq!(m.horizontal().get(), 60.0);
assert_eq!(m.vertical().get(), 40.0);
}
#[test]
fn margins_uniform() {
let m = PageMargins::uniform(Pt::new(50.0));
assert_eq!(m.top, m.right);
assert_eq!(m.right, m.bottom);
assert_eq!(m.bottom, m.left);
assert_eq!(m.top.get(), 50.0);
}
#[test]
fn margins_symmetric() {
let m = PageMargins::symmetric(Pt::new(72.0), Pt::new(54.0));
assert_eq!(m.top, m.bottom);
assert_eq!(m.left, m.right);
assert_eq!(m.top.get(), 72.0);
assert_eq!(m.left.get(), 54.0);
}
#[test]
fn margins_standard_is_one_inch() {
let m = PageMargins::STANDARD;
assert_eq!(m.top.get(), 72.0);
assert_eq!(m.right.get(), 72.0);
assert_eq!(m.bottom.get(), 72.0);
assert_eq!(m.left.get(), 72.0);
}
#[test]
fn margins_validation_valid() {
let m = PageMargins::uniform(Pt::new(72.0));
assert!(m.validate(Pt::new(595.0), Pt::new(842.0)).is_none());
}
#[test]
fn margins_validation_exceeds_width() {
let m = PageMargins::uniform(Pt::new(300.0));
let err = m.validate(Pt::new(595.0), Pt::new(842.0));
assert!(err.is_some());
assert!(err.unwrap().contains("horizontal"));
}
#[test]
fn margins_validation_negative() {
let m = PageMargins {
top: Pt::new(-1.0),
..PageMargins::ZERO
};
assert!(m.validate(Pt::new(595.0), Pt::new(842.0)).is_some());
}
#[test]
fn page_template_default_has_no_margins() {
let t = PageTemplate::default();
assert_eq!(t.margins, PageMargins::ZERO);
assert!(t.header.is_none());
assert!(t.footer.is_none());
}
#[test]
fn content_area_height_with_margins() {
let dec = PageDecorations {
margins: PageMargins {
top: Pt::new(72.0),
right: Pt::new(72.0),
bottom: Pt::new(72.0),
left: Pt::new(72.0),
},
content_offset_y: Pt::new(72.0),
content_offset_x: Pt::new(72.0),
header: None,
footer: None,
first_page_header: None,
first_page_footer: None,
left_page_header: None,
left_page_footer: None,
right_page_header: None,
right_page_footer: None,
total_pages: 0,
};
let cah = dec.content_area_height(Pt::new(842.0));
assert!((cah.get() - 698.0).abs() < 0.01);
}
#[test]
fn header_for_page_suppressed_first() {
let dec = PageDecorations {
margins: PageMargins::ZERO,
content_offset_y: Pt::ZERO,
content_offset_x: Pt::ZERO,
header: None,
footer: None,
first_page_header: Some(None),
first_page_footer: None,
left_page_header: None,
left_page_footer: None,
right_page_header: None,
right_page_footer: None,
total_pages: 3,
};
assert!(dec.header_for_page(0).is_none());
assert!(dec.header_for_page(1).is_none());
}
}