use crate::model::Block;
use crate::render::dimension::Pt;
use super::build::{build_header_footer_content, BuildContext, HeaderFooterContent};
use super::draw_command::{DrawCommand, LayoutedPage};
use super::page::PageConfig;
use super::section::stack_blocks;
#[allow(clippy::too_many_arguments)]
pub fn render_headers_footers(
pages: &mut [LayoutedPage],
config: &PageConfig,
header_blocks: Option<&[Block]>,
footer_blocks: Option<&[Block]>,
ctx: &BuildContext,
default_line_height: Pt,
page_base: usize,
total_pages: usize,
) {
let content_width = config.content_width();
for (page_idx, page) in pages.iter_mut().enumerate() {
let page_number = page_base + page_idx + 1;
if let Some(blocks) = header_blocks {
ctx.field_ctx_cell
.set(crate::render::layout::fragment::FieldContext {
page_number: Some(page_number),
num_pages: Some(total_pages),
});
let hf = build_header_footer_content(blocks, ctx);
render_header(page, config, &hf, content_width, default_line_height);
}
if let Some(blocks) = footer_blocks {
ctx.field_ctx_cell
.set(crate::render::layout::fragment::FieldContext {
page_number: Some(page_number),
num_pages: Some(total_pages),
});
let hf = build_header_footer_content(blocks, ctx);
render_footer(page, config, &hf, content_width, default_line_height);
}
}
ctx.field_ctx_cell
.set(crate::render::layout::fragment::FieldContext::default());
}
fn render_header(
page: &mut LayoutedPage,
config: &PageConfig,
hf: &HeaderFooterContent,
content_width: Pt,
default_line_height: Pt,
) {
if hf.blocks.is_empty() {
return;
}
let (offset_x, offset_y) = if let Some((abs_x, abs_y)) = hf.absolute_position {
(abs_x, abs_y)
} else {
(config.margins.left, config.header_margin)
};
let result = stack_blocks(&hf.blocks, content_width, default_line_height, None);
let mut header_cmds: Vec<DrawCommand> = result
.commands
.into_iter()
.map(|mut cmd| {
cmd.shift(offset_x, offset_y);
cmd
})
.collect();
for fi in &hf.floating_images {
let img_y = match fi.y {
super::section::FloatingImageY::Absolute(y) => y,
super::section::FloatingImageY::RelativeToParagraph(offset) => offset_y + offset,
};
header_cmds.push(DrawCommand::Image {
rect: crate::render::geometry::PtRect::from_xywh(
fi.x,
img_y,
fi.size.width,
fi.size.height,
),
image_data: fi.image_data.clone(),
});
}
header_cmds.append(&mut page.commands);
page.commands = header_cmds;
}
fn render_footer(
page: &mut LayoutedPage,
config: &PageConfig,
hf: &HeaderFooterContent,
content_width: Pt,
default_line_height: Pt,
) {
if hf.blocks.is_empty() {
return;
}
let result = stack_blocks(&hf.blocks, content_width, default_line_height, None);
let footer_y = config.page_size.height - config.footer_margin - result.height;
for mut cmd in result.commands {
cmd.shift(config.margins.left, footer_y);
page.commands.push(cmd);
}
for fi in &hf.floating_images {
let img_y = match fi.y {
super::section::FloatingImageY::Absolute(y) => y,
super::section::FloatingImageY::RelativeToParagraph(offset) => footer_y + offset,
};
page.commands.push(DrawCommand::Image {
rect: crate::render::geometry::PtRect::from_xywh(
fi.x,
img_y,
fi.size.width,
fi.size.height,
),
image_data: fi.image_data.clone(),
});
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::geometry::{PtEdgeInsets, PtOffset, PtSize};
use crate::render::layout::fragment::{FontProps, Fragment, TextMetrics};
use crate::render::layout::paragraph::ParagraphStyle;
use crate::render::layout::section::LayoutBlock;
use crate::render::resolve::color::RgbColor;
use std::rc::Rc;
fn make_hf(frags: Vec<Fragment>) -> HeaderFooterContent {
HeaderFooterContent {
blocks: vec![LayoutBlock::Paragraph {
fragments: frags,
style: ParagraphStyle::default(),
page_break_before: false,
footnotes: vec![],
floating_images: vec![],
}],
absolute_position: None,
floating_images: vec![],
}
}
fn text_frag(s: &str) -> Fragment {
let font = FontProps {
family: Rc::from("Test"),
size: Pt::new(12.0),
bold: false,
italic: false,
underline: false,
char_spacing: Pt::ZERO,
underline_position: Pt::ZERO,
underline_thickness: Pt::ZERO,
};
Fragment::Text {
text: s.to_string(),
font,
color: RgbColor::BLACK,
shading: None,
border: None,
width: Pt::new(40.0),
trimmed_width: Pt::new(40.0),
metrics: TextMetrics {
ascent: Pt::new(10.0),
descent: Pt::new(4.0),
},
hyperlink_url: None,
baseline_offset: Pt::ZERO,
text_offset: Pt::ZERO,
}
}
fn test_config() -> PageConfig {
use crate::render::layout::page::ColumnGeometry;
PageConfig {
page_size: PtSize::new(Pt::new(612.0), Pt::new(792.0)),
margins: PtEdgeInsets::new(Pt::new(72.0), Pt::new(72.0), Pt::new(72.0), Pt::new(72.0)),
header_margin: Pt::new(36.0),
footer_margin: Pt::new(36.0),
columns: vec![ColumnGeometry {
x_offset: Pt::ZERO,
width: Pt::new(468.0),
}],
}
}
#[test]
fn no_header_footer_leaves_page_unchanged() {
let mut pages = [LayoutedPage::new(PtSize::new(
Pt::new(612.0),
Pt::new(792.0),
))];
pages[0].commands.push(DrawCommand::Text {
text: "body".into(),
position: PtOffset::new(Pt::ZERO, Pt::ZERO),
font_family: Rc::from("T"),
font_size: Pt::new(12.0),
char_spacing: Pt::ZERO,
bold: false,
italic: false,
color: RgbColor::BLACK,
});
let config = test_config();
let hf = HeaderFooterContent {
blocks: vec![],
absolute_position: None,
floating_images: vec![],
};
render_header(
&mut pages[0],
&config,
&hf,
config.content_width(),
Pt::new(14.0),
);
render_footer(
&mut pages[0],
&config,
&hf,
config.content_width(),
Pt::new(14.0),
);
assert_eq!(pages[0].commands.len(), 1, "no changes");
}
#[test]
fn header_prepended_to_page() {
let mut pages = [LayoutedPage::new(PtSize::new(
Pt::new(612.0),
Pt::new(792.0),
))];
pages[0].commands.push(DrawCommand::Text {
text: "body".into(),
position: PtOffset::new(Pt::ZERO, Pt::ZERO),
font_family: Rc::from("T"),
font_size: Pt::new(12.0),
char_spacing: Pt::ZERO,
bold: false,
italic: false,
color: RgbColor::BLACK,
});
let config = test_config();
let header = make_hf(vec![text_frag("Header")]);
render_header(
&mut pages[0],
&config,
&header,
config.content_width(),
Pt::new(14.0),
);
assert!(pages[0].commands.len() > 1);
if let DrawCommand::Text { text, .. } = &pages[0].commands[0] {
assert_eq!(text, "Header");
}
}
#[test]
fn footer_appended_to_page() {
let mut pages = [LayoutedPage::new(PtSize::new(
Pt::new(612.0),
Pt::new(792.0),
))];
let config = test_config();
let footer = make_hf(vec![text_frag("Footer")]);
render_footer(
&mut pages[0],
&config,
&footer,
config.content_width(),
Pt::new(14.0),
);
assert_eq!(pages[0].commands.len(), 1);
if let DrawCommand::Text { text, position, .. } = &pages[0].commands[0] {
assert_eq!(text, "Footer");
assert!(position.y.raw() > 700.0, "footer y={}", position.y.raw());
}
}
#[test]
fn header_applied_to_all_pages() {
let mut pages = vec![
LayoutedPage::new(PtSize::new(Pt::new(612.0), Pt::new(792.0))),
LayoutedPage::new(PtSize::new(Pt::new(612.0), Pt::new(792.0))),
];
let config = test_config();
let header = make_hf(vec![text_frag("H")]);
for page in pages.iter_mut() {
render_header(
page,
&config,
&header,
config.content_width(),
Pt::new(14.0),
);
}
for page in &pages {
assert!(!page.commands.is_empty());
}
}
#[test]
fn header_y_position_uses_header_margin() {
let mut pages = [LayoutedPage::new(PtSize::new(
Pt::new(612.0),
Pt::new(792.0),
))];
let config = test_config();
let header = make_hf(vec![text_frag("H")]);
render_header(
&mut pages[0],
&config,
&header,
config.content_width(),
Pt::new(14.0),
);
if let DrawCommand::Text { position, .. } = &pages[0].commands[0] {
assert!(
position.y.raw() > 36.0 && position.y.raw() < 72.0,
"header y={} should be between header_margin and top margin",
position.y.raw()
);
}
}
}