use alloc::{boxed::Box, collections::BTreeMap, string::String, sync::Arc, vec::Vec};
use core::fmt;
use azul_core::{
dom::NodeId,
geom::{LogicalPosition, LogicalRect, LogicalSize},
};
use azul_css::props::layout::fragmentation::{
BoxDecorationBreak, BreakInside, Orphans, PageBreak, Widows,
};
#[cfg(all(feature = "text_layout", feature = "font_loading"))]
use crate::solver3::display_list::{DisplayList, DisplayListItem};
#[cfg(not(all(feature = "text_layout", feature = "font_loading")))]
#[derive(Debug, Clone, Default)]
pub struct DisplayList {
pub items: Vec<DisplayListItem>,
}
#[cfg(not(all(feature = "text_layout", feature = "font_loading")))]
#[derive(Debug, Clone)]
pub struct DisplayListItem;
#[derive(Debug, Clone)]
pub struct PageCounter {
pub page_number: usize,
pub total_pages: Option<usize>,
pub chapter: Option<usize>,
pub named_counters: BTreeMap<String, i32>,
}
impl Default for PageCounter {
fn default() -> Self {
Self {
page_number: 1,
total_pages: None,
chapter: None,
named_counters: BTreeMap::new(),
}
}
}
impl PageCounter {
pub fn new() -> Self {
Self::default()
}
pub fn with_page_number(mut self, page: usize) -> Self {
self.page_number = page;
self
}
pub fn with_total_pages(mut self, total: usize) -> Self {
self.total_pages = Some(total);
self
}
pub fn format_page_number(&self, style: PageNumberStyle) -> String {
match style {
PageNumberStyle::Decimal => format!("{}", self.page_number),
PageNumberStyle::LowerRoman => to_lower_roman(self.page_number),
PageNumberStyle::UpperRoman => to_upper_roman(self.page_number),
PageNumberStyle::LowerAlpha => to_lower_alpha(self.page_number),
PageNumberStyle::UpperAlpha => to_upper_alpha(self.page_number),
}
}
pub fn format_page_of_total(&self) -> String {
match self.total_pages {
Some(total) => format!("Page {} of {}", self.page_number, total),
None => format!("Page {}", self.page_number),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PageNumberStyle {
Decimal,
LowerRoman,
UpperRoman,
LowerAlpha,
UpperAlpha,
}
impl Default for PageNumberStyle {
fn default() -> Self {
Self::Decimal
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PageSlotPosition {
TopLeft,
TopCenter,
TopRight,
BottomLeft,
BottomCenter,
BottomRight,
}
#[derive(Clone)]
pub enum PageSlotContent {
Text(String),
PageNumber(PageNumberStyle),
PageOfTotal,
RunningHeader(String),
Dynamic(Arc<DynamicSlotContentFn>),
}
pub struct DynamicSlotContentFn {
func: Box<dyn Fn(&PageCounter) -> String + Send + Sync>,
}
impl DynamicSlotContentFn {
pub fn new<F: Fn(&PageCounter) -> String + Send + Sync + 'static>(f: F) -> Self {
Self { func: Box::new(f) }
}
pub fn call(&self, counter: &PageCounter) -> String {
(self.func)(counter)
}
}
impl fmt::Debug for DynamicSlotContentFn {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "<dynamic content fn>")
}
}
impl fmt::Debug for PageSlotContent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PageSlotContent::Text(s) => write!(f, "Text({:?})", s),
PageSlotContent::PageNumber(style) => write!(f, "PageNumber({:?})", style),
PageSlotContent::PageOfTotal => write!(f, "PageOfTotal"),
PageSlotContent::RunningHeader(s) => write!(f, "RunningHeader({:?})", s),
PageSlotContent::Dynamic(_) => write!(f, "Dynamic(<fn>)"),
}
}
}
#[derive(Debug, Clone)]
pub struct PageSlot {
pub position: PageSlotPosition,
pub content: PageSlotContent,
pub font_size_pt: Option<f32>,
pub color: Option<azul_css::props::basic::ColorU>,
}
#[derive(Debug, Clone)]
pub struct PageTemplate {
pub header_height: f32,
pub footer_height: f32,
pub slots: Vec<PageSlot>,
pub header_on_first_page: bool,
pub footer_on_first_page: bool,
pub left_page_slots: Option<Vec<PageSlot>>,
pub right_page_slots: Option<Vec<PageSlot>>,
}
impl Default for PageTemplate {
fn default() -> Self {
Self {
header_height: 0.0,
footer_height: 0.0,
slots: Vec::new(),
header_on_first_page: true,
footer_on_first_page: true,
left_page_slots: None,
right_page_slots: None,
}
}
}
impl PageTemplate {
pub fn new() -> Self {
Self::default()
}
pub fn with_page_number_footer(mut self, height: f32) -> Self {
self.footer_height = height;
self.slots.push(PageSlot {
position: PageSlotPosition::BottomCenter,
content: PageSlotContent::PageNumber(PageNumberStyle::Decimal),
font_size_pt: Some(10.0),
color: None,
});
self
}
pub fn with_page_of_total_footer(mut self, height: f32) -> Self {
self.footer_height = height;
self.slots.push(PageSlot {
position: PageSlotPosition::BottomCenter,
content: PageSlotContent::PageOfTotal,
font_size_pt: Some(10.0),
color: None,
});
self
}
pub fn with_book_header(mut self, title: String, height: f32) -> Self {
self.header_height = height;
self.slots.push(PageSlot {
position: PageSlotPosition::TopLeft,
content: PageSlotContent::Text(title),
font_size_pt: Some(10.0),
color: None,
});
self.slots.push(PageSlot {
position: PageSlotPosition::TopRight,
content: PageSlotContent::PageNumber(PageNumberStyle::Decimal),
font_size_pt: Some(10.0),
color: None,
});
self
}
pub fn slots_for_page(&self, page_number: usize) -> &[PageSlot] {
let is_left_page = page_number % 2 == 0;
if is_left_page {
if let Some(ref left_slots) = self.left_page_slots {
return left_slots;
}
} else {
if let Some(ref right_slots) = self.right_page_slots {
return right_slots;
}
}
&self.slots
}
pub fn show_header(&self, page_number: usize) -> bool {
if page_number == 1 && !self.header_on_first_page {
return false;
}
self.header_height > 0.0
}
pub fn show_footer(&self, page_number: usize) -> bool {
if page_number == 1 && !self.footer_on_first_page {
return false;
}
self.footer_height > 0.0
}
pub fn content_area_height(&self, page_height: f32, page_number: usize) -> f32 {
let header = if self.show_header(page_number) {
self.header_height
} else {
0.0
};
let footer = if self.show_footer(page_number) {
self.footer_height
} else {
0.0
};
page_height - header - footer
}
}
#[derive(Debug, Clone)]
pub enum BoxBreakBehavior {
Splittable {
min_before_break: f32,
min_after_break: f32,
},
KeepTogether {
estimated_height: f32,
priority: KeepTogetherPriority,
},
Monolithic {
height: f32,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum KeepTogetherPriority {
Low = 0,
Normal = 1,
High = 2,
Critical = 3,
}
#[derive(Debug, Clone)]
pub struct BreakPoint {
pub y_position: f32,
pub break_class: BreakClass,
pub break_before: PageBreak,
pub break_after: PageBreak,
pub ancestor_avoid_depth: usize,
pub preceding_node: Option<NodeId>,
pub following_node: Option<NodeId>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BreakClass {
ClassA,
ClassB,
ClassC,
}
impl BreakPoint {
pub fn is_allowed(&self) -> bool {
if is_forced_break(&self.break_before) || is_forced_break(&self.break_after) {
return true; }
if is_avoid_break(&self.break_before) || is_avoid_break(&self.break_after) {
return false; }
if self.ancestor_avoid_depth > 0 {
return false;
}
true
}
pub fn is_forced(&self) -> bool {
is_forced_break(&self.break_before) || is_forced_break(&self.break_after)
}
}
#[derive(Debug)]
pub struct PageFragment {
pub page_index: usize,
pub bounds: LogicalRect,
pub items: Vec<DisplayListItem>,
pub source_node: Option<NodeId>,
pub is_continuation: bool,
pub continues_on_next: bool,
}
#[derive(Debug)]
pub struct FragmentationLayoutContext {
pub page_size: LogicalSize,
pub margins: PageMargins,
pub template: PageTemplate,
pub current_page: usize,
pub current_y: f32,
pub available_height: f32,
pub page_content_height: f32,
pub break_inside_avoid_depth: usize,
pub orphans: u32,
pub widows: u32,
pub fragments: Vec<PageFragment>,
pub counter: PageCounter,
pub defaults: FragmentationDefaults,
pub break_points: Vec<BreakPoint>,
pub avoid_break_before_next: bool,
}
#[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,
}
}
pub fn horizontal(&self) -> f32 {
self.left + self.right
}
pub fn vertical(&self) -> f32 {
self.top + self.bottom
}
}
#[derive(Debug, Clone)]
pub struct FragmentationDefaults {
pub keep_headers_with_content: bool,
pub min_paragraph_lines: u32,
pub keep_figures_together: bool,
pub keep_table_headers: bool,
pub keep_list_markers: bool,
pub small_block_threshold_lines: u32,
pub default_orphans: u32,
pub default_widows: u32,
}
impl Default for FragmentationDefaults {
fn default() -> Self {
Self {
keep_headers_with_content: true,
min_paragraph_lines: 3,
keep_figures_together: true,
keep_table_headers: true,
keep_list_markers: true,
small_block_threshold_lines: 3,
default_orphans: 2,
default_widows: 2,
}
}
}
impl FragmentationLayoutContext {
pub fn new(page_size: LogicalSize, margins: PageMargins) -> Self {
let template = PageTemplate::default();
let page_content_height =
page_size.height - margins.vertical() - template.header_height - template.footer_height;
Self {
page_size,
margins,
template,
current_page: 0,
current_y: 0.0,
available_height: page_content_height,
page_content_height,
break_inside_avoid_depth: 0,
orphans: 2,
widows: 2,
fragments: Vec::new(),
counter: PageCounter::new(),
defaults: FragmentationDefaults::default(),
break_points: Vec::new(),
avoid_break_before_next: false,
}
}
pub fn with_template(mut self, template: PageTemplate) -> Self {
self.template = template;
self.recalculate_content_height();
self
}
pub fn with_defaults(mut self, defaults: FragmentationDefaults) -> Self {
self.orphans = defaults.default_orphans;
self.widows = defaults.default_widows;
self.defaults = defaults;
self
}
fn recalculate_content_height(&mut self) {
let header = if self.template.show_header(self.current_page + 1) {
self.template.header_height
} else {
0.0
};
let footer = if self.template.show_footer(self.current_page + 1) {
self.template.footer_height
} else {
0.0
};
self.page_content_height =
self.page_size.height - self.margins.vertical() - header - footer;
self.available_height = self.page_content_height - self.current_y;
}
pub fn content_origin(&self) -> LogicalPosition {
let header = if self.template.show_header(self.current_page + 1) {
self.template.header_height
} else {
0.0
};
LogicalPosition {
x: self.margins.left,
y: self.margins.top + header,
}
}
pub fn content_size(&self) -> LogicalSize {
LogicalSize {
width: self.page_size.width - self.margins.horizontal(),
height: self.page_content_height,
}
}
pub fn use_space(&mut self, height: f32) {
self.current_y += height;
self.available_height = (self.page_content_height - self.current_y).max(0.0);
}
pub fn can_fit(&self, height: f32) -> bool {
self.available_height >= height
}
pub fn would_fit_on_empty_page(&self, height: f32) -> bool {
height <= self.page_content_height
}
pub fn advance_page(&mut self) {
self.current_page += 1;
self.current_y = 0.0;
self.counter.page_number += 1;
self.recalculate_content_height();
self.avoid_break_before_next = false;
}
pub fn advance_to_left_page(&mut self) {
self.advance_page();
if self.current_page % 2 != 0 {
self.advance_page();
}
}
pub fn advance_to_right_page(&mut self) {
self.advance_page();
if self.current_page % 2 == 0 {
self.advance_page();
}
}
pub fn enter_avoid_break(&mut self) {
self.break_inside_avoid_depth += 1;
}
pub fn exit_avoid_break(&mut self) {
self.break_inside_avoid_depth = self.break_inside_avoid_depth.saturating_sub(1);
}
pub fn set_avoid_break_before_next(&mut self) {
self.avoid_break_before_next = true;
}
pub fn add_fragment(&mut self, fragment: PageFragment) {
self.fragments.push(fragment);
}
pub fn page_count(&self) -> usize {
self.current_page + 1
}
pub fn set_total_pages(&mut self, total: usize) {
self.counter.total_pages = Some(total);
}
pub fn into_display_lists(self) -> Vec<DisplayList> {
let page_count = self.page_count();
let mut display_lists: Vec<DisplayList> =
(0..page_count).map(|_| DisplayList::default()).collect();
for fragment in self.fragments {
if fragment.page_index < display_lists.len() {
display_lists[fragment.page_index]
.items
.extend(fragment.items);
}
}
display_lists
}
pub fn generate_page_chrome(&self, page_index: usize) -> Vec<DisplayListItem> {
let mut items = Vec::new();
let page_number = page_index + 1;
let counter = PageCounter {
page_number,
total_pages: self.counter.total_pages,
chapter: self.counter.chapter,
named_counters: self.counter.named_counters.clone(),
};
let slots = self.template.slots_for_page(page_number);
for slot in slots {
let _text = match &slot.content {
PageSlotContent::Text(s) => s.clone(),
PageSlotContent::PageNumber(style) => counter.format_page_number(*style),
PageSlotContent::PageOfTotal => counter.format_page_of_total(),
PageSlotContent::RunningHeader(s) => s.clone(),
PageSlotContent::Dynamic(f) => f.call(&counter),
};
let (_x, _y) = self.slot_position(slot.position, page_number);
}
items
}
fn slot_position(&self, position: PageSlotPosition, page_number: usize) -> (f32, f32) {
let content_width = self.page_size.width - self.margins.horizontal();
let x = match position {
PageSlotPosition::TopLeft | PageSlotPosition::BottomLeft => self.margins.left,
PageSlotPosition::TopCenter | PageSlotPosition::BottomCenter => {
self.margins.left + content_width / 2.0
}
PageSlotPosition::TopRight | PageSlotPosition::BottomRight => {
self.page_size.width - self.margins.right
}
};
let y = match position {
PageSlotPosition::TopLeft
| PageSlotPosition::TopCenter
| PageSlotPosition::TopRight => self.margins.top + self.template.header_height / 2.0,
PageSlotPosition::BottomLeft
| PageSlotPosition::BottomCenter
| PageSlotPosition::BottomRight => {
self.page_size.height - self.margins.bottom - self.template.footer_height / 2.0
}
};
(x, y)
}
}
#[derive(Debug, Clone)]
pub enum BreakDecision {
FitOnCurrentPage,
MoveToNextPage,
SplitAcrossPages {
height_on_current: f32,
height_remaining: f32,
},
ForceBreakBefore,
ForceBreakAfter,
}
pub fn decide_break(
behavior: &BoxBreakBehavior,
ctx: &FragmentationLayoutContext,
break_before: PageBreak,
break_after: PageBreak,
) -> BreakDecision {
if is_forced_break(&break_before) {
if ctx.current_y > 0.0 {
return BreakDecision::ForceBreakBefore;
}
}
match behavior {
BoxBreakBehavior::Monolithic { height } => {
decide_monolithic_break(*height, ctx, break_before)
}
BoxBreakBehavior::KeepTogether {
estimated_height,
priority,
} => decide_keep_together_break(*estimated_height, *priority, ctx, break_before),
BoxBreakBehavior::Splittable {
min_before_break,
min_after_break,
} => decide_splittable_break(*min_before_break, *min_after_break, ctx, break_before),
}
}
fn decide_monolithic_break(
height: f32,
ctx: &FragmentationLayoutContext,
break_before: PageBreak,
) -> BreakDecision {
if ctx.can_fit(height) {
BreakDecision::FitOnCurrentPage
} else if ctx.current_y > 0.0 && ctx.would_fit_on_empty_page(height) {
BreakDecision::MoveToNextPage
} else {
BreakDecision::FitOnCurrentPage
}
}
fn decide_keep_together_break(
height: f32,
priority: KeepTogetherPriority,
ctx: &FragmentationLayoutContext,
break_before: PageBreak,
) -> BreakDecision {
if ctx.can_fit(height) {
BreakDecision::FitOnCurrentPage
} else if ctx.would_fit_on_empty_page(height) {
BreakDecision::MoveToNextPage
} else {
let height_on_current = ctx.available_height;
let height_remaining = height - height_on_current;
BreakDecision::SplitAcrossPages {
height_on_current,
height_remaining,
}
}
}
fn decide_splittable_break(
min_before: f32,
min_after: f32,
ctx: &FragmentationLayoutContext,
break_before: PageBreak,
) -> BreakDecision {
let available = ctx.available_height;
if available < min_before && ctx.current_y > 0.0 {
BreakDecision::MoveToNextPage
} else {
BreakDecision::FitOnCurrentPage
}
}
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
)
}
fn is_avoid_break(page_break: &PageBreak) -> bool {
matches!(page_break, PageBreak::Avoid | PageBreak::AvoidPage)
}
fn to_lower_roman(n: usize) -> String {
to_upper_roman(n).to_lowercase()
}
fn to_upper_roman(mut n: usize) -> String {
if n == 0 {
return String::from("0");
}
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.iter() {
while n >= *value {
result.push_str(numeral);
n -= value;
}
}
result
}
fn to_lower_alpha(n: usize) -> String {
to_upper_alpha(n).to_lowercase()
}
fn to_upper_alpha(mut n: usize) -> String {
if n == 0 {
return String::from("0");
}
let mut result = String::new();
while n > 0 {
n -= 1;
result.insert(0, (b'A' + (n % 26) as u8) as char);
n /= 26;
}
result
}