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