Skip to main content

azul_layout/
fragmentation.rs

1//! CSS Fragmentation Engine for Paged Media
2//!
3//! This module implements the CSS Fragmentation specification (css-break-3) for
4//! breaking content across pages, columns, or regions.
5//!
6//! ## Key Concepts
7//!
8//! - **Fragmentainer**: A container (page, column, region) that holds a portion of content
9//! - **FragmentationContext**: Tracks layout state during fragmentation
10//! - **BoxBreakBehavior**: How a box should be handled at page breaks
11//! - **PageTemplate**: Headers, footers, and running content for pages
12//!
13//! ## Algorithm Overview
14//!
15//! Unlike post-layout splitting, this module integrates fragmentation INTO layout:
16//!
17//! 1. Classify each box's break behavior (splittable, keep-together, monolithic)
18//! 2. During layout, check if content fits in current fragmentainer
19//! 3. Apply break-before/break-after rules
20//! 4. Split or defer content as needed
21//! 5. Handle orphans/widows for text content
22//!
23//! **Note**: `solver3/pagination.rs` provides an alternative page-layout implementation
24//! with its own `PageGeometer`, `PageTemplate`, and `PageMargins`. See that module for
25//! the currently active paged-layout pipeline.
26
27use alloc::{boxed::Box, collections::BTreeMap, string::String, sync::Arc, vec::Vec};
28use core::fmt;
29
30use azul_core::{
31    dom::NodeId,
32    geom::{LogicalPosition, LogicalRect, LogicalSize},
33};
34use azul_css::props::layout::fragmentation::{
35    BoxDecorationBreak, BreakInside, Orphans, PageBreak, Widows,
36};
37
38#[cfg(all(feature = "text_layout", feature = "font_loading"))]
39use crate::solver3::display_list::{DisplayList, DisplayListItem};
40
41// Stub types when text_layout or font_loading is disabled
42#[cfg(not(all(feature = "text_layout", feature = "font_loading")))]
43#[derive(Debug, Clone, Default)]
44pub struct DisplayList {
45    pub items: Vec<DisplayListItem>,
46}
47
48#[cfg(not(all(feature = "text_layout", feature = "font_loading")))]
49#[derive(Debug, Clone)]
50pub struct DisplayListItem;
51
52// Page Templates (Headers, Footers, Running Content)
53
54/// Counter that tracks page numbers and other running content
55#[derive(Debug, Clone)]
56pub struct PageCounter {
57    /// Current page number (1-indexed)
58    pub page_number: usize,
59    /// Total page count (may be unknown during first pass)
60    pub total_pages: Option<usize>,
61    /// Chapter or section number
62    pub chapter: Option<usize>,
63    /// Custom named counters (CSS counter() function)
64    pub named_counters: BTreeMap<String, i32>,
65}
66
67impl Default for PageCounter {
68    fn default() -> Self {
69        Self {
70            page_number: 1,
71            total_pages: None,
72            chapter: None,
73            named_counters: BTreeMap::new(),
74        }
75    }
76}
77
78impl PageCounter {
79    pub fn new() -> Self {
80        Self::default()
81    }
82
83    pub fn with_page_number(mut self, page: usize) -> Self {
84        self.page_number = page;
85        self
86    }
87
88    pub fn with_total_pages(mut self, total: usize) -> Self {
89        self.total_pages = Some(total);
90        self
91    }
92
93    /// Format page number as string (e.g., "3", "iii", "C")
94    pub fn format_page_number(&self, style: PageNumberStyle) -> String {
95        match style {
96            PageNumberStyle::Decimal => format!("{}", self.page_number),
97            PageNumberStyle::LowerRoman => to_lower_roman(self.page_number),
98            PageNumberStyle::UpperRoman => to_upper_roman(self.page_number),
99            PageNumberStyle::LowerAlpha => to_lower_alpha(self.page_number),
100            PageNumberStyle::UpperAlpha => to_upper_alpha(self.page_number),
101        }
102    }
103
104    /// Get "Page X of Y" string
105    pub fn format_page_of_total(&self) -> String {
106        match self.total_pages {
107            Some(total) => format!("Page {} of {}", self.page_number, total),
108            None => format!("Page {}", self.page_number),
109        }
110    }
111}
112
113/// Style for page number formatting
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub enum PageNumberStyle {
116    /// 1, 2, 3, ...
117    Decimal,
118    /// i, ii, iii, iv, ...
119    LowerRoman,
120    /// I, II, III, IV, ...
121    UpperRoman,
122    /// a, b, c, ..., z, aa, ab, ...
123    LowerAlpha,
124    /// A, B, C, ..., Z, AA, AB, ...
125    UpperAlpha,
126}
127
128impl Default for PageNumberStyle {
129    fn default() -> Self {
130        Self::Decimal
131    }
132}
133
134/// Slot position for dynamic content in page template
135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136pub enum PageSlotPosition {
137    /// Top-left corner
138    TopLeft,
139    /// Top-center
140    TopCenter,
141    /// Top-right corner
142    TopRight,
143    /// Bottom-left corner
144    BottomLeft,
145    /// Bottom-center
146    BottomCenter,
147    /// Bottom-right corner
148    BottomRight,
149}
150
151/// Content that can be placed in a page template slot
152#[derive(Clone)]
153pub enum PageSlotContent {
154    /// Static text
155    Text(String),
156    /// Page number with formatting
157    PageNumber(PageNumberStyle),
158    /// "Page X of Y"
159    PageOfTotal,
160    /// Chapter/section title (from running headers)
161    RunningHeader(String),
162    /// Custom function that generates content per page
163    Dynamic(Arc<DynamicSlotContentFn>),
164}
165
166/// Wrapper for dynamic slot content functions to allow Debug impl.
167///
168/// Use [`DynamicSlotContentFn::new`] to wrap a closure, then place it
169/// inside [`PageSlotContent::Dynamic`] via `Arc`:
170///
171/// ```ignore
172/// let func = DynamicSlotContentFn::new(|counter| {
173///     format!("Page {}", counter.page_number)
174/// });
175/// let content = PageSlotContent::Dynamic(Arc::new(func));
176/// ```
177pub struct DynamicSlotContentFn {
178    func: Box<dyn Fn(&PageCounter) -> String + Send + Sync>,
179}
180
181impl DynamicSlotContentFn {
182    pub fn new<F: Fn(&PageCounter) -> String + Send + Sync + 'static>(f: F) -> Self {
183        Self { func: Box::new(f) }
184    }
185
186    pub fn call(&self, counter: &PageCounter) -> String {
187        (self.func)(counter)
188    }
189}
190
191impl fmt::Debug for DynamicSlotContentFn {
192    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193        write!(f, "<dynamic content fn>")
194    }
195}
196
197impl fmt::Debug for PageSlotContent {
198    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199        match self {
200            PageSlotContent::Text(s) => write!(f, "Text({:?})", s),
201            PageSlotContent::PageNumber(style) => write!(f, "PageNumber({:?})", style),
202            PageSlotContent::PageOfTotal => write!(f, "PageOfTotal"),
203            PageSlotContent::RunningHeader(s) => write!(f, "RunningHeader({:?})", s),
204            PageSlotContent::Dynamic(_) => write!(f, "Dynamic(<fn>)"),
205        }
206    }
207}
208
209/// A slot in the page template
210#[derive(Debug, Clone)]
211pub struct PageSlot {
212    /// Position of this slot
213    pub position: PageSlotPosition,
214    /// Content to display
215    pub content: PageSlotContent,
216    /// Font size in points (optional override)
217    pub font_size_pt: Option<f32>,
218    /// Color (optional override)
219    pub color: Option<azul_css::props::basic::ColorU>,
220}
221
222/// Template for page headers, footers, and margins
223#[derive(Debug, Clone)]
224pub struct PageTemplate {
225    /// Header height in points (0 = no header)
226    pub header_height: f32,
227    /// Footer height in points (0 = no footer)
228    pub footer_height: f32,
229    /// Slots for dynamic content
230    pub slots: Vec<PageSlot>,
231    /// Whether to show header on first page
232    pub header_on_first_page: bool,
233    /// Whether to show footer on first page
234    pub footer_on_first_page: bool,
235    /// Different template for left (even) pages
236    pub left_page_slots: Option<Vec<PageSlot>>,
237    /// Different template for right (odd) pages  
238    pub right_page_slots: Option<Vec<PageSlot>>,
239}
240
241impl Default for PageTemplate {
242    fn default() -> Self {
243        Self {
244            header_height: 0.0,
245            footer_height: 0.0,
246            slots: Vec::new(),
247            header_on_first_page: true,
248            footer_on_first_page: true,
249            left_page_slots: None,
250            right_page_slots: None,
251        }
252    }
253}
254
255/// Default font size in points for page template slots
256const DEFAULT_SLOT_FONT_SIZE_PT: f32 = 10.0;
257
258impl PageTemplate {
259    pub fn new() -> Self {
260        Self::default()
261    }
262
263    /// Add a simple page number footer (centered)
264    pub fn with_page_number_footer(mut self, height: f32) -> Self {
265        self.footer_height = height;
266        self.slots.push(PageSlot {
267            position: PageSlotPosition::BottomCenter,
268            content: PageSlotContent::PageNumber(PageNumberStyle::Decimal),
269            font_size_pt: Some(DEFAULT_SLOT_FONT_SIZE_PT),
270            color: None,
271        });
272        self
273    }
274
275    /// Add "Page X of Y" footer
276    pub fn with_page_of_total_footer(mut self, height: f32) -> Self {
277        self.footer_height = height;
278        self.slots.push(PageSlot {
279            position: PageSlotPosition::BottomCenter,
280            content: PageSlotContent::PageOfTotal,
281            font_size_pt: Some(DEFAULT_SLOT_FONT_SIZE_PT),
282            color: None,
283        });
284        self
285    }
286
287    /// Add a header with title on left and page number on right
288    pub fn with_book_header(mut self, title: String, height: f32) -> Self {
289        self.header_height = height;
290        self.slots.push(PageSlot {
291            position: PageSlotPosition::TopLeft,
292            content: PageSlotContent::Text(title),
293            font_size_pt: Some(DEFAULT_SLOT_FONT_SIZE_PT),
294            color: None,
295        });
296        self.slots.push(PageSlot {
297            position: PageSlotPosition::TopRight,
298            content: PageSlotContent::PageNumber(PageNumberStyle::Decimal),
299            font_size_pt: Some(DEFAULT_SLOT_FONT_SIZE_PT),
300            color: None,
301        });
302        self
303    }
304
305    /// Get slots for a specific page (handles left/right page differences)
306    pub fn slots_for_page(&self, page_number: usize) -> &[PageSlot] {
307        let override_slots = if page_number % 2 == 0 {
308            self.left_page_slots.as_deref()
309        } else {
310            self.right_page_slots.as_deref()
311        };
312        override_slots.unwrap_or(&self.slots)
313    }
314
315    /// Check if header should be shown on this page
316    pub fn show_header(&self, page_number: usize) -> bool {
317        if page_number == 1 && !self.header_on_first_page {
318            return false;
319        }
320        self.header_height > 0.0
321    }
322
323    /// Check if footer should be shown on this page
324    pub fn show_footer(&self, page_number: usize) -> bool {
325        if page_number == 1 && !self.footer_on_first_page {
326            return false;
327        }
328        self.footer_height > 0.0
329    }
330
331    /// Get the content area height (page height minus header and footer)
332    pub fn content_area_height(&self, page_height: f32, page_number: usize) -> f32 {
333        let header = if self.show_header(page_number) {
334            self.header_height
335        } else {
336            0.0
337        };
338        let footer = if self.show_footer(page_number) {
339            self.footer_height
340        } else {
341            0.0
342        };
343        page_height - header - footer
344    }
345}
346
347// Box Break Behavior Classification
348
349/// How a box should behave at fragmentation breaks
350#[derive(Debug, Clone)]
351pub enum BoxBreakBehavior {
352    /// Can be split at any internal break point (paragraphs, containers)
353    Splittable {
354        /// Minimum content height before a break (orphans-like)
355        min_before_break: f32,
356        /// Minimum content height after a break (widows-like)
357        min_after_break: f32,
358    },
359    /// Should be kept together if possible (headers, small blocks)
360    KeepTogether {
361        /// Estimated total height of this box
362        estimated_height: f32,
363        /// Priority level (higher = more important to keep together)
364        priority: KeepTogetherPriority,
365    },
366    /// Cannot be split (images, replaced elements, overflow:scroll)
367    Monolithic {
368        /// Fixed height of this element
369        height: f32,
370    },
371}
372
373/// Priority for keeping content together
374#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
375pub enum KeepTogetherPriority {
376    /// Low priority - can break if needed
377    Low = 0,
378    /// Normal priority (default for break-inside: avoid)
379    Normal = 1,
380    /// High priority (headers with following content)
381    High = 2,
382    /// Critical (figure with caption, table headers)
383    Critical = 3,
384}
385
386/// Information about a potential break point
387#[derive(Debug, Clone)]
388pub struct BreakPoint {
389    /// Y position of this break point (in content coordinates)
390    pub y_position: f32,
391    /// Type of break point (Class A, B, or C)
392    pub break_class: BreakClass,
393    /// Break-before value at this point
394    pub break_before: PageBreak,
395    /// Break-after value at this point  
396    pub break_after: PageBreak,
397    /// Whether ancestors have break-inside: avoid
398    pub ancestor_avoid_depth: usize,
399    /// Node that precedes this break point
400    pub preceding_node: Option<NodeId>,
401    /// Node that follows this break point
402    pub following_node: Option<NodeId>,
403}
404
405/// CSS Fragmentation break point class
406#[derive(Debug, Clone, Copy, PartialEq, Eq)]
407pub enum BreakClass {
408    /// Between sibling block-level boxes
409    ClassA,
410    // +spec:block-formatting-context:a019b9 - break opportunities only between line boxes, not inside them
411    /// Between line boxes inside a block container
412    ClassB,
413    /// Between content edge and child margin edge
414    ClassC,
415}
416
417impl BreakPoint {
418    /// Check if this break point is allowed (respecting all break rules)
419    pub fn is_allowed(&self) -> bool {
420        // Rule 1: Check break-after/break-before
421        if is_forced_break(&self.break_before) || is_forced_break(&self.break_after) {
422            return true; // Forced breaks are always allowed
423        }
424
425        if is_avoid_break(&self.break_before) || is_avoid_break(&self.break_after) {
426            return false; // Avoid breaks
427        }
428
429        // Rule 2: Check ancestor break-inside: avoid
430        if self.ancestor_avoid_depth > 0 {
431            return false;
432        }
433
434        // Rules 3 & 4 are handled at a higher level (orphans/widows, etc.)
435        true
436    }
437
438    /// Check if this is a forced break
439    pub fn is_forced(&self) -> bool {
440        is_forced_break(&self.break_before) || is_forced_break(&self.break_after)
441    }
442}
443
444// Fragmentation Layout Context
445
446/// A fragment of content placed on a specific page
447#[derive(Debug)]
448pub struct PageFragment {
449    /// Which page this fragment belongs to (0-indexed)
450    pub page_index: usize,
451    /// Bounds of this fragment on the page (in page coordinates)
452    pub bounds: LogicalRect,
453    /// Display list items for this fragment
454    pub items: Vec<DisplayListItem>,
455    /// Node ID that this fragment belongs to
456    pub source_node: Option<NodeId>,
457    /// Whether this is a continuation from previous page
458    pub is_continuation: bool,
459    /// Whether this continues on the next page
460    pub continues_on_next: bool,
461}
462
463/// Context for fragmentation-aware layout
464#[derive(Debug)]
465pub struct FragmentationLayoutContext {
466    /// Page size (including margins)
467    pub page_size: LogicalSize,
468    /// Content area margins
469    pub margins: PageMargins,
470    /// Page template for headers/footers
471    pub template: PageTemplate,
472    /// Current page being laid out (0-indexed)
473    pub current_page: usize,
474    /// Y position on current page (0 = top of content area)
475    pub current_y: f32,
476    /// Available height remaining on current page
477    pub available_height: f32,
478    /// Page content height (without margins and headers/footers)
479    pub page_content_height: f32,
480    /// Accumulated break-inside: avoid depth from ancestors
481    pub break_inside_avoid_depth: usize,
482    /// Current orphans setting (inherited)
483    pub orphans: u32,
484    /// Current widows setting (inherited)
485    pub widows: u32,
486    /// All page fragments generated so far
487    pub fragments: Vec<PageFragment>,
488    /// Page counter for headers/footers
489    pub counter: PageCounter,
490    /// Fragmentation defaults (smart behavior settings)
491    pub defaults: FragmentationDefaults,
492    /// Break points encountered during layout
493    pub break_points: Vec<BreakPoint>,
494    /// Whether to avoid break before next box
495    pub avoid_break_before_next: bool,
496}
497
498/// Page margins in points
499#[derive(Debug, Clone, Copy, Default)]
500pub struct PageMargins {
501    pub top: f32,
502    pub right: f32,
503    pub bottom: f32,
504    pub left: f32,
505}
506
507impl PageMargins {
508    pub fn new(top: f32, right: f32, bottom: f32, left: f32) -> Self {
509        Self {
510            top,
511            right,
512            bottom,
513            left,
514        }
515    }
516
517    pub fn uniform(margin: f32) -> Self {
518        Self {
519            top: margin,
520            right: margin,
521            bottom: margin,
522            left: margin,
523        }
524    }
525
526    pub fn horizontal(&self) -> f32 {
527        self.left + self.right
528    }
529
530    pub fn vertical(&self) -> f32 {
531        self.top + self.bottom
532    }
533}
534
535/// Configuration for intelligent fragmentation defaults
536#[derive(Debug, Clone)]
537pub struct FragmentationDefaults {
538    /// Keep headers (h1-h6) with following content
539    pub keep_headers_with_content: bool,
540    /// Minimum lines to keep together for short paragraphs
541    pub min_paragraph_lines: u32,
542    /// Keep figure/figcaption together
543    pub keep_figures_together: bool,
544    /// Keep table headers with first data row
545    pub keep_table_headers: bool,
546    /// Keep list item markers with content
547    pub keep_list_markers: bool,
548    /// Treat small blocks as monolithic (height threshold in lines)
549    pub small_block_threshold_lines: u32,
550    /// Default orphans value
551    pub default_orphans: u32,
552    /// Default widows value
553    pub default_widows: u32,
554}
555
556impl Default for FragmentationDefaults {
557    fn default() -> Self {
558        Self {
559            keep_headers_with_content: true,
560            min_paragraph_lines: 3,
561            keep_figures_together: true,
562            keep_table_headers: true,
563            keep_list_markers: true,
564            small_block_threshold_lines: 3,
565            default_orphans: 2,
566            default_widows: 2,
567        }
568    }
569}
570
571impl FragmentationLayoutContext {
572    /// Create a new fragmentation context for paged layout
573    pub fn new(page_size: LogicalSize, margins: PageMargins) -> Self {
574        let template = PageTemplate::default();
575
576        let page_content_height =
577            page_size.height - margins.vertical() - template.header_height - template.footer_height;
578
579        Self {
580            page_size,
581            margins,
582            template,
583            current_page: 0,
584            current_y: 0.0,
585            available_height: page_content_height,
586            page_content_height,
587            break_inside_avoid_depth: 0,
588            orphans: 2,
589            widows: 2,
590            fragments: Vec::new(),
591            counter: PageCounter::new(),
592            defaults: FragmentationDefaults::default(),
593            break_points: Vec::new(),
594            avoid_break_before_next: false,
595        }
596    }
597
598    /// Create context with a page template
599    pub fn with_template(mut self, template: PageTemplate) -> Self {
600        self.template = template;
601        self.recalculate_content_height();
602        self
603    }
604
605    /// Create context with custom defaults
606    pub fn with_defaults(mut self, defaults: FragmentationDefaults) -> Self {
607        self.orphans = defaults.default_orphans;
608        self.widows = defaults.default_widows;
609        self.defaults = defaults;
610        self
611    }
612
613    /// Recalculate content height based on template
614    fn recalculate_content_height(&mut self) {
615        let page_height = self.page_size.height - self.margins.vertical();
616        self.page_content_height =
617            self.template.content_area_height(page_height, self.current_page + 1);
618        self.available_height = self.page_content_height - self.current_y;
619    }
620
621    /// Get the content area origin for the current page
622    pub fn content_origin(&self) -> LogicalPosition {
623        let header = if self.template.show_header(self.current_page + 1) {
624            self.template.header_height
625        } else {
626            0.0
627        };
628        LogicalPosition {
629            x: self.margins.left,
630            y: self.margins.top + header,
631        }
632    }
633
634    /// Get the content area size for the current page
635    pub fn content_size(&self) -> LogicalSize {
636        LogicalSize {
637            width: self.page_size.width - self.margins.horizontal(),
638            height: self.page_content_height,
639        }
640    }
641
642    /// Use space on the current page
643    pub fn use_space(&mut self, height: f32) {
644        self.current_y += height;
645        self.available_height = (self.page_content_height - self.current_y).max(0.0);
646    }
647
648    /// Check if content of given height can fit on current page
649    pub fn can_fit(&self, height: f32) -> bool {
650        self.available_height >= height
651    }
652
653    /// Check if content would fit on an empty page
654    pub fn would_fit_on_empty_page(&self, height: f32) -> bool {
655        height <= self.page_content_height
656    }
657
658    /// Advance to the next page
659    pub fn advance_page(&mut self) {
660        self.current_page += 1;
661        self.current_y = 0.0;
662        self.counter.page_number += 1;
663        self.recalculate_content_height();
664        self.avoid_break_before_next = false;
665    }
666
667    /// Advance to a left (even) page.
668    ///
669    /// May insert a blank page if the current page is already even,
670    /// in order to land on the next even-numbered page (standard
671    /// recto/verso paged-media behavior).
672    pub fn advance_to_left_page(&mut self) {
673        self.advance_page();
674        if self.current_page % 2 != 0 {
675            // Current page is odd (right), advance one more
676            self.advance_page();
677        }
678    }
679
680    /// Advance to a right (odd) page.
681    ///
682    /// May insert a blank page if the current page is already odd,
683    /// in order to land on the next odd-numbered page (standard
684    /// recto/verso paged-media behavior).
685    pub fn advance_to_right_page(&mut self) {
686        self.advance_page();
687        if self.current_page % 2 == 0 {
688            // Current page is even (left), advance one more
689            self.advance_page();
690        }
691    }
692
693    /// Enter a box with break-inside: avoid
694    pub fn enter_avoid_break(&mut self) {
695        self.break_inside_avoid_depth += 1;
696    }
697
698    /// Exit a box with break-inside: avoid
699    pub fn exit_avoid_break(&mut self) {
700        self.break_inside_avoid_depth = self.break_inside_avoid_depth.saturating_sub(1);
701    }
702
703    /// Set flag to avoid break before next content
704    pub fn set_avoid_break_before_next(&mut self) {
705        self.avoid_break_before_next = true;
706    }
707
708    /// Add a page fragment
709    pub fn add_fragment(&mut self, fragment: PageFragment) {
710        self.fragments.push(fragment);
711    }
712
713    /// Get the total number of pages so far
714    pub fn page_count(&self) -> usize {
715        self.current_page + 1
716    }
717
718    /// Set total page count (for "Page X of Y" footers)
719    pub fn set_total_pages(&mut self, total: usize) {
720        self.counter.total_pages = Some(total);
721    }
722
723    /// Convert fragments to display lists (one per page)
724    pub fn into_display_lists(self) -> Vec<DisplayList> {
725        let page_count = self.page_count();
726        let mut display_lists: Vec<DisplayList> =
727            (0..page_count).map(|_| DisplayList::default()).collect();
728
729        for fragment in self.fragments {
730            if fragment.page_index < display_lists.len() {
731                display_lists[fragment.page_index]
732                    .items
733                    .extend(fragment.items);
734            }
735        }
736
737        display_lists
738    }
739
740    /// Generate header/footer display list items for a specific page
741    pub fn generate_page_chrome(&self, page_index: usize) -> Vec<DisplayListItem> {
742        let mut items = Vec::new();
743        let page_number = page_index + 1;
744
745        let counter = PageCounter {
746            page_number,
747            total_pages: self.counter.total_pages,
748            chapter: self.counter.chapter,
749            named_counters: self.counter.named_counters.clone(),
750        };
751
752        let slots = self.template.slots_for_page(page_number);
753
754        for slot in slots {
755            let _text = match &slot.content {
756                PageSlotContent::Text(s) => s.clone(),
757                PageSlotContent::PageNumber(style) => counter.format_page_number(*style),
758                PageSlotContent::PageOfTotal => counter.format_page_of_total(),
759                PageSlotContent::RunningHeader(s) => s.clone(),
760                PageSlotContent::Dynamic(f) => f.call(&counter),
761            };
762
763            // Calculate position based on slot
764            let (_x, _y) = self.slot_position(slot.position, page_number);
765
766            // TODO: Create proper text DisplayListItem
767            // For now we'll need to integrate with text layout
768            // This is a placeholder that shows where the text would go
769        }
770
771        items
772    }
773
774    /// Calculate position for a page slot
775    fn slot_position(&self, position: PageSlotPosition, page_number: usize) -> (f32, f32) {
776        let content_width = self.page_size.width - self.margins.horizontal();
777
778        let x = match position {
779            PageSlotPosition::TopLeft | PageSlotPosition::BottomLeft => self.margins.left,
780            PageSlotPosition::TopCenter | PageSlotPosition::BottomCenter => {
781                self.margins.left + content_width / 2.0
782            }
783            PageSlotPosition::TopRight | PageSlotPosition::BottomRight => {
784                self.page_size.width - self.margins.right
785            }
786        };
787
788        let y = match position {
789            PageSlotPosition::TopLeft
790            | PageSlotPosition::TopCenter
791            | PageSlotPosition::TopRight => self.margins.top + self.template.header_height / 2.0,
792            PageSlotPosition::BottomLeft
793            | PageSlotPosition::BottomCenter
794            | PageSlotPosition::BottomRight => {
795                self.page_size.height - self.margins.bottom - self.template.footer_height / 2.0
796            }
797        };
798
799        (x, y)
800    }
801}
802
803// Break Decision Logic
804
805/// Result of deciding how to handle a box at a potential break point
806#[derive(Debug, Clone)]
807pub enum BreakDecision {
808    /// Place the entire box on the current page
809    FitOnCurrentPage,
810    /// Move the entire box to the next page
811    MoveToNextPage,
812    /// Split the box across pages
813    SplitAcrossPages {
814        /// Height to place on current page
815        height_on_current: f32,
816        /// Height to place on next page(s)
817        height_remaining: f32,
818    },
819    /// Force a page break before this box
820    ForceBreakBefore,
821    /// Force a page break after this box
822    ForceBreakAfter,
823}
824
825/// Make a break decision for a box with given behavior
826pub fn decide_break(
827    behavior: &BoxBreakBehavior,
828    ctx: &FragmentationLayoutContext,
829    break_before: PageBreak,
830    break_after: PageBreak,
831) -> BreakDecision {
832    // Check for forced break before
833    if is_forced_break(&break_before) {
834        if ctx.current_y > 0.0 {
835            return BreakDecision::ForceBreakBefore;
836        }
837    }
838
839    match behavior {
840        BoxBreakBehavior::Monolithic { height } => {
841            decide_monolithic_break(*height, ctx, break_before)
842        }
843        BoxBreakBehavior::KeepTogether {
844            estimated_height,
845            priority,
846        } => decide_keep_together_break(*estimated_height, *priority, ctx, break_before),
847        BoxBreakBehavior::Splittable {
848            min_before_break,
849            min_after_break,
850        } => decide_splittable_break(*min_before_break, *min_after_break, ctx, break_before),
851    }
852}
853
854fn decide_monolithic_break(
855    height: f32,
856    ctx: &FragmentationLayoutContext,
857    _break_before: PageBreak,
858) -> BreakDecision {
859    // Monolithic content cannot be split
860    if ctx.can_fit(height) {
861        BreakDecision::FitOnCurrentPage
862    } else if ctx.current_y > 0.0 && ctx.would_fit_on_empty_page(height) {
863        // Doesn't fit but would fit on empty page
864        BreakDecision::MoveToNextPage
865    } else {
866        // Too large for any page - place anyway (will overflow)
867        BreakDecision::FitOnCurrentPage
868    }
869}
870
871fn decide_keep_together_break(
872    height: f32,
873    _priority: KeepTogetherPriority,
874    ctx: &FragmentationLayoutContext,
875    _break_before: PageBreak,
876) -> BreakDecision {
877    if ctx.can_fit(height) {
878        BreakDecision::FitOnCurrentPage
879    } else if ctx.would_fit_on_empty_page(height) {
880        // Would fit on empty page, move there
881        BreakDecision::MoveToNextPage
882    } else {
883        // Too tall for any page - must split despite keep-together
884        // Calculate split point
885        let height_on_current = ctx.available_height;
886        let height_remaining = height - height_on_current;
887        BreakDecision::SplitAcrossPages {
888            height_on_current,
889            height_remaining,
890        }
891    }
892}
893
894fn decide_splittable_break(
895    min_before: f32,
896    _min_after: f32,
897    ctx: &FragmentationLayoutContext,
898    _break_before: PageBreak,
899) -> BreakDecision {
900    // For splittable content, we need to consider orphans/widows
901    let available = ctx.available_height;
902
903    if available < min_before && ctx.current_y > 0.0 {
904        // Can't fit minimum orphan content, move to next page
905        BreakDecision::MoveToNextPage
906    } else {
907        // Can split - but actual split point determined during text layout
908        BreakDecision::FitOnCurrentPage
909    }
910}
911
912// Helper Functions
913
914fn is_forced_break(page_break: &PageBreak) -> bool {
915    matches!(
916        page_break,
917        PageBreak::Always
918            | PageBreak::Page
919            | PageBreak::Left
920            | PageBreak::Right
921            | PageBreak::Recto
922            | PageBreak::Verso
923            | PageBreak::All
924    )
925}
926
927fn is_avoid_break(page_break: &PageBreak) -> bool {
928    matches!(page_break, PageBreak::Avoid | PageBreak::AvoidPage)
929}
930
931// Roman numeral conversion
932//
933// Note: Roman numerals and alphabetic numbering have no representation for
934// zero. These functions return `"0"` as a fallback when `n == 0`.
935
936fn to_lower_roman(n: usize) -> String {
937    to_upper_roman(n).to_lowercase()
938}
939
940fn to_upper_roman(mut n: usize) -> String {
941    if n == 0 {
942        return String::from("0");
943    }
944
945    let numerals = [
946        (1000, "M"),
947        (900, "CM"),
948        (500, "D"),
949        (400, "CD"),
950        (100, "C"),
951        (90, "XC"),
952        (50, "L"),
953        (40, "XL"),
954        (10, "X"),
955        (9, "IX"),
956        (5, "V"),
957        (4, "IV"),
958        (1, "I"),
959    ];
960
961    let mut result = String::new();
962    for (value, numeral) in numerals.iter() {
963        while n >= *value {
964            result.push_str(numeral);
965            n -= value;
966        }
967    }
968    result
969}
970
971fn to_lower_alpha(n: usize) -> String {
972    to_upper_alpha(n).to_lowercase()
973}
974
975fn to_upper_alpha(mut n: usize) -> String {
976    if n == 0 {
977        return String::from("0");
978    }
979
980    let mut result = String::new();
981    while n > 0 {
982        n -= 1;
983        result.insert(0, (b'A' + (n % 26) as u8) as char);
984        n /= 26;
985    }
986    result
987}