use std::{collections::BTreeMap, sync::Arc};
use azul_core::geom::{LogicalPosition, LogicalRect, LogicalSize};
use azul_css::props::{
basic::ColorU,
layout::fragmentation::{BreakInside, PageBreak},
};
#[derive(Debug, Clone)]
pub struct PageGeometer {
pub page_size: LogicalSize,
pub page_margins: PageMargins,
pub header_height: f32,
pub footer_height: f32,
pub current_y: f32,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct PageMargins {
pub top: f32,
pub right: f32,
pub bottom: f32,
pub left: f32,
}
impl PageMargins {
pub fn new(top: f32, right: f32, bottom: f32, left: f32) -> Self {
Self {
top,
right,
bottom,
left,
}
}
pub fn uniform(margin: f32) -> Self {
Self {
top: margin,
right: margin,
bottom: margin,
left: margin,
}
}
}
impl PageGeometer {
pub fn new(page_size: LogicalSize, margins: PageMargins) -> Self {
Self {
page_size,
page_margins: margins,
header_height: 0.0,
footer_height: 0.0,
current_y: 0.0,
}
}
pub fn with_header_footer(mut self, header: f32, footer: f32) -> Self {
self.header_height = header;
self.footer_height = footer;
self
}
pub fn content_height(&self) -> f32 {
self.page_size.height
- self.page_margins.top
- self.page_margins.bottom
- self.header_height
- self.footer_height
}
pub fn content_width(&self) -> f32 {
self.page_size.width - self.page_margins.left - self.page_margins.right
}
pub fn page_for_y(&self, y: f32) -> usize {
let content_h = self.content_height();
if content_h <= 0.0 {
return 0;
}
let full_page_slot = content_h + self.dead_zone_height();
(y / full_page_slot).floor() as usize
}
pub fn page_content_start_y(&self, page_index: usize) -> f32 {
let full_page_slot = self.content_height() + self.dead_zone_height();
page_index as f32 * full_page_slot
}
pub fn page_content_end_y(&self, page_index: usize) -> f32 {
self.page_content_start_y(page_index) + self.content_height()
}
pub fn dead_zone_height(&self) -> f32 {
self.footer_height + self.page_margins.bottom + self.page_margins.top + self.header_height
}
pub fn next_page_start_y(&self, current_y: f32) -> f32 {
let current_page = self.page_for_y(current_y);
self.page_content_start_y(current_page + 1)
}
pub fn crosses_page_break(&self, start_y: f32, end_y: f32) -> bool {
let start_page = self.page_for_y(start_y);
let end_page = self.page_for_y(end_y - 0.01); start_page != end_page
}
pub fn remaining_on_page(&self, y: f32) -> f32 {
let page = self.page_for_y(y);
let page_end = self.page_content_end_y(page);
(page_end - y).max(0.0)
}
pub fn can_fit(&self, y: f32, height: f32) -> bool {
self.remaining_on_page(y) >= height
}
pub fn page_break_offset(&self, y: f32, height: f32) -> f32 {
if self.can_fit(y, height) {
return 0.0;
}
let next_start = self.next_page_start_y(y);
next_start - y
}
pub fn page_count(&self, total_content_height: f32) -> usize {
if total_content_height <= 0.0 {
return 1;
}
self.page_for_y(total_content_height - 0.01) + 1
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BreakBehavior {
Splittable,
AvoidBreak,
Monolithic,
}
#[derive(Debug, Clone)]
pub struct BreakEvaluation {
pub force_break_before: bool,
pub force_break_after: bool,
pub behavior: BreakBehavior,
pub orphans: u32,
pub widows: u32,
}
impl Default for BreakEvaluation {
fn default() -> Self {
Self {
force_break_before: false,
force_break_after: false,
behavior: BreakBehavior::Splittable,
orphans: 2,
widows: 2,
}
}
}
pub fn is_forced_break(page_break: PageBreak) -> bool {
matches!(
page_break,
PageBreak::Always
| PageBreak::Page
| PageBreak::Left
| PageBreak::Right
| PageBreak::Recto
| PageBreak::Verso
| PageBreak::All
)
}
pub fn is_avoid_break(page_break: PageBreak) -> bool {
matches!(page_break, PageBreak::Avoid | PageBreak::AvoidPage)
}
#[derive(Debug, Clone)]
pub struct RepeatedTableHeader {
pub inject_at_y: f32,
pub header_items: Vec<usize>, pub header_height: f32,
}
#[derive(Debug)]
pub struct PaginationContext<'a> {
pub geometer: &'a PageGeometer,
pub break_avoid_depth: usize,
pub repeated_headers: Vec<RepeatedTableHeader>,
}
impl<'a> PaginationContext<'a> {
pub fn new(geometer: &'a PageGeometer) -> Self {
Self {
geometer,
break_avoid_depth: 0,
repeated_headers: Vec::new(),
}
}
pub fn enter_avoid_break(&mut self) {
self.break_avoid_depth += 1;
}
pub fn exit_avoid_break(&mut self) {
self.break_avoid_depth = self.break_avoid_depth.saturating_sub(1);
}
pub fn is_avoiding_breaks(&self) -> bool {
self.break_avoid_depth > 0
}
pub fn register_repeated_header(
&mut self,
inject_at_y: f32,
header_items: Vec<usize>,
header_height: f32,
) {
self.repeated_headers.push(RepeatedTableHeader {
inject_at_y,
header_items,
header_height,
});
}
}
pub fn calculate_pagination_offset(
geometer: &PageGeometer,
main_pen: f32,
child_height: f32,
break_eval: &BreakEvaluation,
is_avoiding_breaks: bool,
) -> f32 {
if break_eval.force_break_before {
let remaining = geometer.remaining_on_page(main_pen);
if remaining < geometer.content_height() {
return geometer.page_break_offset(main_pen, f32::MAX);
}
}
let remaining = geometer.remaining_on_page(main_pen);
if break_eval.behavior == BreakBehavior::Monolithic {
if child_height <= remaining {
return 0.0;
}
if child_height <= geometer.content_height() {
return geometer.page_break_offset(main_pen, child_height);
}
return 0.0;
}
if break_eval.behavior == BreakBehavior::AvoidBreak || is_avoiding_breaks {
if child_height <= remaining {
return 0.0;
}
if child_height <= geometer.content_height() {
return geometer.page_break_offset(main_pen, child_height);
}
}
let min_before_break = 20.0; if remaining < min_before_break && remaining < geometer.content_height() {
return geometer.page_break_offset(main_pen, child_height);
}
0.0
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum MarginBoxPosition {
TopLeftCorner,
TopLeft,
TopCenter,
TopRight,
TopRightCorner,
LeftTop,
LeftMiddle,
LeftBottom,
RightTop,
RightMiddle,
RightBottom,
BottomLeftCorner,
BottomLeft,
BottomCenter,
BottomRight,
BottomRightCorner,
}
impl MarginBoxPosition {
pub fn is_top(&self) -> bool {
matches!(
self,
Self::TopLeftCorner
| Self::TopLeft
| Self::TopCenter
| Self::TopRight
| Self::TopRightCorner
)
}
pub fn is_bottom(&self) -> bool {
matches!(
self,
Self::BottomLeftCorner
| Self::BottomLeft
| Self::BottomCenter
| Self::BottomRight
| Self::BottomRightCorner
)
}
}
#[derive(Debug, Clone)]
pub struct RunningElement {
pub name: String,
pub display_items: Vec<super::display_list::DisplayListItem>,
pub size: LogicalSize,
pub source_page: usize,
}
#[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 {
pub fn format(&self, n: usize) -> String {
match self {
Self::Decimal => n.to_string(),
Self::DecimalLeadingZero => format!("{:02}", n),
Self::LowerRoman => to_roman(n, false),
Self::UpperRoman => to_roman(n, true),
Self::LowerAlpha => to_alpha(n, false),
Self::UpperAlpha => to_alpha(n, true),
Self::LowerGreek => to_greek(n),
}
}
}
fn to_roman(mut n: usize, uppercase: bool) -> String {
if n == 0 {
return "0".to_string();
}
let numerals = [
(1000, "m"),
(900, "cm"),
(500, "d"),
(400, "cd"),
(100, "c"),
(90, "xc"),
(50, "l"),
(40, "xl"),
(10, "x"),
(9, "ix"),
(5, "v"),
(4, "iv"),
(1, "i"),
];
let mut result = String::new();
for (value, numeral) in &numerals {
while n >= *value {
result.push_str(numeral);
n -= value;
}
}
if uppercase {
result.to_uppercase()
} else {
result
}
}
fn to_alpha(n: usize, uppercase: bool) -> String {
if n == 0 {
return "0".to_string();
}
let mut result = String::new();
let mut remaining = n;
while remaining > 0 {
remaining -= 1;
let c = ((remaining % 26) as u8 + if uppercase { b'A' } else { b'a' }) as char;
result.insert(0, c);
remaining /= 26;
}
result
}
fn to_greek(n: usize) -> String {
const GREEK: &[char] = &[
'α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η', 'θ', 'ι', 'κ', 'λ', 'μ', 'ν', 'ξ', 'ο', 'π', 'ρ', 'σ',
'τ', 'υ', 'φ', 'χ', 'ψ', 'ω',
];
if n == 0 {
return "0".to_string();
}
if n <= GREEK.len() {
return GREEK[n - 1].to_string();
}
let mut result = String::new();
let mut remaining = n;
while remaining > 0 {
remaining -= 1;
result.insert(0, GREEK[remaining % GREEK.len()]);
remaining /= GREEK.len();
}
result
}
#[derive(Debug, Clone, Copy)]
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 {
pub 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 % 2 == 0, is_right: page_number % 2 == 1, is_blank: false,
}
}
}
#[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: 30.0,
footer_height: 30.0,
header_content: MarginBoxContent::None,
footer_content: MarginBoxContent::None,
font_size: 10.0,
text_color: ColorU {
r: 0,
g: 0,
b: 0,
a: 255,
},
skip_first_page: false,
}
}
}
impl HeaderFooterConfig {
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()
}
}
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()
}
}
pub fn with_header_text(mut self, text: impl Into<String>) -> Self {
self.show_header = true;
self.header_content = MarginBoxContent::Text(text.into());
self
}
pub fn with_footer_text(mut self, text: impl Into<String>) -> Self {
self.show_footer = true;
self.footer_content = MarginBoxContent::Text(text.into());
self
}
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),
}
}
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)
}
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, Default)]
pub struct PageTemplate {
pub margin_boxes: BTreeMap<MarginBoxPosition, MarginBoxContent>,
pub margins: PageMargins,
pub named_strings: BTreeMap<String, String>,
pub running_elements: BTreeMap<String, RunningElement>,
}
impl PageTemplate {
pub fn new() -> Self {
Self::default()
}
pub fn set_margin_box(&mut self, position: MarginBoxPosition, content: MarginBoxContent) {
self.margin_boxes.insert(position, content);
}
pub fn with_centered_page_numbers() -> Self {
let mut template = Self::new();
template.set_margin_box(
MarginBoxPosition::BottomCenter,
MarginBoxContent::PageCounter,
);
template
}
pub fn with_page_x_of_y() -> Self {
let mut template = Self::new();
template.set_margin_box(
MarginBoxPosition::BottomRight,
MarginBoxContent::Combined(vec![
MarginBoxContent::Text("Page ".to_string()),
MarginBoxContent::PageCounter,
MarginBoxContent::Text(" of ".to_string()),
MarginBoxContent::PagesCounter,
]),
);
template
}
}
#[derive(Debug, Clone)]
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: 30.0,
footer_height: 30.0,
font_size: 10.0,
text_color: ColorU {
r: 0,
g: 0,
b: 0,
a: 255,
},
}
}
}
impl FakePageConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_footer_page_numbers(mut self) -> Self {
self.show_footer = true;
self.footer_page_number = true;
self.footer_total_pages = true;
self
}
pub fn with_header_page_numbers(mut self) -> Self {
self.show_header = true;
self.header_page_number = true;
self
}
pub 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
}
pub fn with_header_text(mut self, text: impl Into<String>) -> Self {
self.show_header = true;
self.header_text = Some(text.into());
self
}
pub fn with_footer_text(mut self, text: impl Into<String>) -> Self {
self.show_footer = true;
self.footer_text = Some(text.into());
self
}
pub fn with_number_format(mut self, format: CounterFormat) -> Self {
self.number_format = format;
self
}
pub fn skip_first_page(mut self, skip: bool) -> Self {
self.skip_first_page = skip;
self
}
pub fn with_header_height(mut self, height: f32) -> Self {
self.header_height = height;
self
}
pub fn with_footer_height(mut self, height: f32) -> Self {
self.footer_height = height;
self
}
pub fn with_font_size(mut self, size: f32) -> Self {
self.font_size = size;
self
}
pub fn with_text_color(mut self, color: ColorU) -> Self {
self.text_color = color;
self
}
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 {
let mut parts = Vec::new();
if let Some(ref text) = self.header_text {
parts.push(MarginBoxContent::Text(text.clone()));
if self.header_page_number {
parts.push(MarginBoxContent::Text(" - ".to_string()));
}
}
if self.header_page_number {
if self.number_format == CounterFormat::Decimal {
parts.push(MarginBoxContent::Text("Page ".to_string()));
parts.push(MarginBoxContent::PageCounter);
} else {
parts.push(MarginBoxContent::Text("Page ".to_string()));
parts.push(MarginBoxContent::PageCounterFormatted {
format: self.number_format,
});
}
if self.header_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)
}
}
fn build_footer_content(&self) -> MarginBoxContent {
let mut parts = Vec::new();
if let Some(ref text) = self.footer_text {
parts.push(MarginBoxContent::Text(text.clone()));
if self.footer_page_number {
parts.push(MarginBoxContent::Text(" - ".to_string()));
}
}
if self.footer_page_number {
if self.number_format == CounterFormat::Decimal {
parts.push(MarginBoxContent::Text("Page ".to_string()));
parts.push(MarginBoxContent::PageCounter);
} else {
parts.push(MarginBoxContent::Text("Page ".to_string()));
parts.push(MarginBoxContent::PageCounterFormatted {
format: self.number_format,
});
}
if self.footer_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 {
pub fn new() -> Self {
Self::default()
}
pub fn register_table_header(&mut self, info: TableHeaderInfo) {
self.tables.push(info);
}
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
}
}