use std::sync::Arc;
use azul_css::props::basic::ColorU;
#[derive(Clone)]
pub enum MarginBoxContent {
None,
RunningElement(String),
NamedString(String),
PageCounter,
PagesCounter,
PageCounterFormatted { format: CounterFormat },
Combined(Vec<MarginBoxContent>),
Text(String),
Custom(Arc<dyn Fn(PageInfo) -> String + Send + Sync>),
}
impl std::fmt::Debug for MarginBoxContent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::None => write!(f, "None"),
Self::RunningElement(s) => f.debug_tuple("RunningElement").field(s).finish(),
Self::NamedString(s) => f.debug_tuple("NamedString").field(s).finish(),
Self::PageCounter => write!(f, "PageCounter"),
Self::PagesCounter => write!(f, "PagesCounter"),
Self::PageCounterFormatted { format } => f
.debug_struct("PageCounterFormatted")
.field("format", format)
.finish(),
Self::Combined(v) => f.debug_tuple("Combined").field(v).finish(),
Self::Text(s) => f.debug_tuple("Text").field(s).finish(),
Self::Custom(_) => write!(f, "Custom(<fn>)"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CounterFormat {
Decimal,
DecimalLeadingZero,
LowerRoman,
UpperRoman,
LowerAlpha,
UpperAlpha,
LowerGreek,
}
impl Default for CounterFormat {
fn default() -> Self {
Self::Decimal
}
}
impl CounterFormat {
#[must_use] pub fn format(&self, n: usize) -> String {
use super::counters::{to_alphabetic, to_greek, to_roman};
match self {
Self::Decimal => n.to_string(),
Self::DecimalLeadingZero => format!("{n:02}"),
Self::LowerRoman => to_roman(n, false),
Self::UpperRoman => to_roman(n, true),
Self::LowerAlpha => to_alphabetic(n, false),
Self::UpperAlpha => to_alphabetic(n, true),
Self::LowerGreek => to_greek(n, false),
}
}
}
#[derive(Debug, Clone, Copy)]
#[allow(clippy::struct_excessive_bools)] pub struct PageInfo {
pub page_number: usize,
pub total_pages: usize,
pub is_first: bool,
pub is_last: bool,
pub is_left: bool,
pub is_right: bool,
pub is_blank: bool,
}
impl PageInfo {
#[must_use] pub const fn new(page_number: usize, total_pages: usize) -> Self {
Self {
page_number,
total_pages,
is_first: page_number == 1,
is_last: total_pages > 0 && page_number == total_pages,
is_left: page_number.is_multiple_of(2), is_right: page_number % 2 == 1, is_blank: false,
}
}
}
const DEFAULT_HEADER_FOOTER_HEIGHT: f32 = 30.0;
const DEFAULT_HEADER_FOOTER_FONT_SIZE: f32 = 10.0;
#[derive(Debug, Clone)]
pub struct HeaderFooterConfig {
pub show_header: bool,
pub show_footer: bool,
pub header_height: f32,
pub footer_height: f32,
pub header_content: MarginBoxContent,
pub footer_content: MarginBoxContent,
pub font_size: f32,
pub text_color: ColorU,
pub skip_first_page: bool,
}
impl Default for HeaderFooterConfig {
fn default() -> Self {
Self {
show_header: false,
show_footer: false,
header_height: DEFAULT_HEADER_FOOTER_HEIGHT,
footer_height: DEFAULT_HEADER_FOOTER_HEIGHT,
header_content: MarginBoxContent::None,
footer_content: MarginBoxContent::None,
font_size: DEFAULT_HEADER_FOOTER_FONT_SIZE,
text_color: ColorU {
r: 0,
g: 0,
b: 0,
a: 255,
},
skip_first_page: false,
}
}
}
impl HeaderFooterConfig {
#[must_use] pub fn with_page_numbers() -> Self {
Self {
show_footer: true,
footer_content: MarginBoxContent::Combined(vec![
MarginBoxContent::Text("Page ".to_string()),
MarginBoxContent::PageCounter,
MarginBoxContent::Text(" of ".to_string()),
MarginBoxContent::PagesCounter,
]),
..Default::default()
}
}
#[must_use] pub fn with_header_and_footer_page_numbers() -> Self {
Self {
show_header: true,
show_footer: true,
header_content: MarginBoxContent::Combined(vec![
MarginBoxContent::Text("Page ".to_string()),
MarginBoxContent::PageCounter,
]),
footer_content: MarginBoxContent::Combined(vec![
MarginBoxContent::Text("Page ".to_string()),
MarginBoxContent::PageCounter,
MarginBoxContent::Text(" of ".to_string()),
MarginBoxContent::PagesCounter,
]),
..Default::default()
}
}
#[must_use]
pub fn with_header_text(mut self, text: impl Into<String>) -> Self {
self.show_header = true;
self.header_content = MarginBoxContent::Text(text.into());
self
}
#[must_use]
pub fn with_footer_text(mut self, text: impl Into<String>) -> Self {
self.show_footer = true;
self.footer_content = MarginBoxContent::Text(text.into());
self
}
#[allow(clippy::only_used_in_recursion)]
#[must_use] pub fn generate_content(&self, content: &MarginBoxContent, info: PageInfo) -> String {
match content {
MarginBoxContent::None => String::new(),
MarginBoxContent::Text(s) => s.clone(),
MarginBoxContent::PageCounter => info.page_number.to_string(),
MarginBoxContent::PagesCounter => {
if info.total_pages > 0 {
info.total_pages.to_string()
} else {
"?".to_string()
}
}
MarginBoxContent::PageCounterFormatted { format } => format.format(info.page_number),
MarginBoxContent::Combined(parts) => parts
.iter()
.map(|p| self.generate_content(p, info))
.collect(),
MarginBoxContent::NamedString(name) => {
format!("[string:{name}]")
}
MarginBoxContent::RunningElement(name) => {
format!("[element:{name}]")
}
MarginBoxContent::Custom(f) => f(info),
}
}
#[must_use] pub fn header_text(&self, info: PageInfo) -> String {
if !self.show_header {
return String::new();
}
if self.skip_first_page && info.is_first {
return String::new();
}
self.generate_content(&self.header_content, info)
}
#[must_use] pub fn footer_text(&self, info: PageInfo) -> String {
if !self.show_footer {
return String::new();
}
if self.skip_first_page && info.is_first {
return String::new();
}
self.generate_content(&self.footer_content, info)
}
}
#[derive(Debug, Clone)]
#[allow(clippy::struct_excessive_bools)] pub struct FakePageConfig {
pub show_header: bool,
pub show_footer: bool,
pub header_text: Option<String>,
pub footer_text: Option<String>,
pub header_page_number: bool,
pub footer_page_number: bool,
pub header_total_pages: bool,
pub footer_total_pages: bool,
pub number_format: CounterFormat,
pub skip_first_page: bool,
pub header_height: f32,
pub footer_height: f32,
pub font_size: f32,
pub text_color: ColorU,
}
impl Default for FakePageConfig {
fn default() -> Self {
Self {
show_header: false,
show_footer: false,
header_text: None,
footer_text: None,
header_page_number: false,
footer_page_number: false,
header_total_pages: false,
footer_total_pages: false,
number_format: CounterFormat::Decimal,
skip_first_page: false,
header_height: DEFAULT_HEADER_FOOTER_HEIGHT,
footer_height: DEFAULT_HEADER_FOOTER_HEIGHT,
font_size: DEFAULT_HEADER_FOOTER_FONT_SIZE,
text_color: ColorU {
r: 0,
g: 0,
b: 0,
a: 255,
},
}
}
}
impl FakePageConfig {
#[must_use] pub fn new() -> Self {
Self::default()
}
#[must_use] pub const fn with_footer_page_numbers(mut self) -> Self {
self.show_footer = true;
self.footer_page_number = true;
self.footer_total_pages = true;
self
}
#[must_use] pub const fn with_header_page_numbers(mut self) -> Self {
self.show_header = true;
self.header_page_number = true;
self
}
#[must_use] pub const fn with_header_and_footer_page_numbers(mut self) -> Self {
self.show_header = true;
self.show_footer = true;
self.header_page_number = true;
self.footer_page_number = true;
self.footer_total_pages = true;
self
}
#[must_use]
pub fn with_header_text(mut self, text: impl Into<String>) -> Self {
self.show_header = true;
self.header_text = Some(text.into());
self
}
#[must_use]
pub fn with_footer_text(mut self, text: impl Into<String>) -> Self {
self.show_footer = true;
self.footer_text = Some(text.into());
self
}
#[must_use] pub const fn with_number_format(mut self, format: CounterFormat) -> Self {
self.number_format = format;
self
}
#[must_use] pub const fn skip_first_page(mut self, skip: bool) -> Self {
self.skip_first_page = skip;
self
}
#[must_use] pub const fn with_header_height(mut self, height: f32) -> Self {
self.header_height = height;
self
}
#[must_use] pub const fn with_footer_height(mut self, height: f32) -> Self {
self.footer_height = height;
self
}
#[must_use] pub const fn with_font_size(mut self, size: f32) -> Self {
self.font_size = size;
self
}
#[must_use] pub const fn with_text_color(mut self, color: ColorU) -> Self {
self.text_color = color;
self
}
#[must_use] pub fn to_header_footer_config(&self) -> HeaderFooterConfig {
HeaderFooterConfig {
show_header: self.show_header,
show_footer: self.show_footer,
header_height: self.header_height,
footer_height: self.footer_height,
header_content: self.build_header_content(),
footer_content: self.build_footer_content(),
skip_first_page: self.skip_first_page,
font_size: self.font_size,
text_color: self.text_color,
}
}
fn build_header_content(&self) -> MarginBoxContent {
Self::build_margin_content(
self.header_text.as_deref(),
self.header_page_number,
self.header_total_pages,
self.number_format,
)
}
fn build_footer_content(&self) -> MarginBoxContent {
Self::build_margin_content(
self.footer_text.as_deref(),
self.footer_page_number,
self.footer_total_pages,
self.number_format,
)
}
fn build_margin_content(
text: Option<&str>,
page_number: bool,
total_pages: bool,
number_format: CounterFormat,
) -> MarginBoxContent {
let mut parts = Vec::new();
if let Some(text) = text {
parts.push(MarginBoxContent::Text(text.to_string()));
if page_number {
parts.push(MarginBoxContent::Text(" - ".to_string()));
}
}
if page_number {
parts.push(MarginBoxContent::Text("Page ".to_string()));
if number_format == CounterFormat::Decimal {
parts.push(MarginBoxContent::PageCounter);
} else {
parts.push(MarginBoxContent::PageCounterFormatted {
format: number_format,
});
}
if total_pages {
parts.push(MarginBoxContent::Text(" of ".to_string()));
parts.push(MarginBoxContent::PagesCounter);
}
}
if parts.is_empty() {
MarginBoxContent::None
} else if parts.len() == 1 {
parts.pop().unwrap()
} else {
MarginBoxContent::Combined(parts)
}
}
}
#[derive(Debug, Clone)]
pub struct TableHeaderInfo {
pub table_node_index: usize,
pub table_start_y: f32,
pub table_end_y: f32,
pub thead_items: Vec<super::display_list::DisplayListItem>,
pub thead_height: f32,
pub thead_offset_y: f32,
}
#[derive(Debug, Default, Clone)]
pub struct TableHeaderTracker {
pub tables: Vec<TableHeaderInfo>,
}
impl TableHeaderTracker {
#[must_use] pub fn new() -> Self {
Self::default()
}
pub fn register_table_header(&mut self, info: TableHeaderInfo) {
self.tables.push(info);
}
#[must_use] pub fn get_repeated_headers_for_page(
&self,
page_index: usize,
page_top_y: f32,
page_bottom_y: f32,
) -> Vec<(f32, &[super::display_list::DisplayListItem], f32)> {
let mut headers = Vec::new();
for table in &self.tables {
let table_starts_before_page = table.table_start_y < page_top_y;
let table_continues_on_page = table.table_end_y > page_top_y;
if table_starts_before_page && table_continues_on_page {
headers.push((
0.0, table.thead_items.as_slice(),
table.thead_height,
));
}
}
headers
}
}