Skip to main content

azul_layout/solver3/
pagination.rs

1//! CSS Paged Media Pagination Engine - "Infinite Canvas with Physical Spacers"
2//!
3//! This module implements a pagination architecture where content is laid out
4//! on a single "infinite" vertical canvas, with "dead zones" representing page
5//! breaks (including headers, footers, and margins).
6//!
7//! ## Core Concept: Physical Spacers
8//!
9//! Instead of assigning nodes to logical pages, we map pages onto a single vertical
10//! coordinate system where page breaks are physical empty spaces:
11//!
12//! ```text
13//! 0px      ─────────────────────────────
14//!          │ Page 1 Content             │
15//! 1000px   ─────────────────────────────
16//!          │ Dead Space (Footer+Margin) │  ← Page break zone
17//! 1100px   ─────────────────────────────
18//!          │ Page 2 Content             │
19//! 2100px   ─────────────────────────────
20//!          │ Dead Space (Footer+Margin) │
21//! 2200px   ─────────────────────────────
22//! ```
23//!
24//! ## CSS Generated Content for Paged Media (GCPM) Level 3 Support
25//!
26//! This module provides the foundation for CSS GCPM Level 3 features:
27//!
28//! - **Running Elements** (`position: running(name)`) - Elements extracted from flow and displayed
29//!   in margin boxes (headers/footers)
30//! - **Page Selectors** (`@page :first`, `@page :left/:right`) - Per-page styling
31//! - **Named Strings** (`string-set`, `content: string(name)`) - Captured text for headers
32//! - **Page Counters** (`counter(page)`, `counter(pages)`) - Page numbering
33//!
34//! See: https://www.w3.org/TR/css-gcpm-3/
35
36use std::{collections::BTreeMap, sync::Arc};
37
38use azul_core::geom::{LogicalPosition, LogicalRect, LogicalSize};
39use azul_css::props::{
40    basic::ColorU,
41    layout::fragmentation::{BreakInside, PageBreak},
42};
43
44/// Manages the infinite canvas coordinate system with page boundaries.
45///
46/// The `PageGeometer` tracks page dimensions and provides utilities for:
47///
48/// - Determining which page a Y coordinate falls on
49/// - Calculating the next page start position
50/// - Checking if content crosses page boundaries
51#[derive(Debug, Clone)]
52pub struct PageGeometer {
53    /// Total height of each page (including margins, headers, footers)
54    pub page_size: LogicalSize,
55    /// Content area margins (space reserved at top/bottom of each page)
56    pub page_margins: PageMargins,
57    /// Height reserved for page header (if any)
58    pub header_height: f32,
59    /// Height reserved for page footer (if any)
60    pub footer_height: f32,
61    /// Current Y position on the infinite canvas
62    pub current_y: f32,
63}
64
65/// Page margin configuration
66#[derive(Debug, Clone, Copy, Default)]
67pub struct PageMargins {
68    pub top: f32,
69    pub right: f32,
70    pub bottom: f32,
71    pub left: f32,
72}
73
74impl PageMargins {
75    pub fn new(top: f32, right: f32, bottom: f32, left: f32) -> Self {
76        Self {
77            top,
78            right,
79            bottom,
80            left,
81        }
82    }
83
84    pub fn uniform(margin: f32) -> Self {
85        Self {
86            top: margin,
87            right: margin,
88            bottom: margin,
89            left: margin,
90        }
91    }
92}
93
94impl PageGeometer {
95    /// Create a new PageGeometer for paged layout.
96    pub fn new(page_size: LogicalSize, margins: PageMargins) -> Self {
97        Self {
98            page_size,
99            page_margins: margins,
100            header_height: 0.0,
101            footer_height: 0.0,
102            current_y: 0.0,
103        }
104    }
105
106    /// Create with header and footer space reserved.
107    pub fn with_header_footer(mut self, header: f32, footer: f32) -> Self {
108        self.header_height = header;
109        self.footer_height = footer;
110        self
111    }
112
113    /// Get the usable content height per page (page height minus margins/headers/footers).
114    pub fn content_height(&self) -> f32 {
115        self.page_size.height
116            - self.page_margins.top
117            - self.page_margins.bottom
118            - self.header_height
119            - self.footer_height
120    }
121
122    /// Get the usable content width per page (page width minus left/right margins).
123    pub fn content_width(&self) -> f32 {
124        self.page_size.width - self.page_margins.left - self.page_margins.right
125    }
126
127    /// Calculate which page a given Y coordinate falls on (0-indexed).
128    pub fn page_for_y(&self, y: f32) -> usize {
129        let content_h = self.content_height();
130        if content_h <= 0.0 {
131            return 0;
132        }
133
134        // Account for dead zones between pages
135        let full_page_slot = content_h + self.dead_zone_height();
136        (y / full_page_slot).floor() as usize
137    }
138
139    /// Get the Y coordinate where a page's content area starts.
140    pub fn page_content_start_y(&self, page_index: usize) -> f32 {
141        let full_page_slot = self.content_height() + self.dead_zone_height();
142        page_index as f32 * full_page_slot
143    }
144
145    /// Get the Y coordinate where a page's content area ends.
146    pub fn page_content_end_y(&self, page_index: usize) -> f32 {
147        self.page_content_start_y(page_index) + self.content_height()
148    }
149
150    /// Get the height of the "dead zone" between pages (footer + margin + header of next page).
151    pub fn dead_zone_height(&self) -> f32 {
152        self.footer_height + self.page_margins.bottom + self.page_margins.top + self.header_height
153    }
154
155    /// Calculate the Y coordinate where the NEXT page's content starts from a given position.
156    pub fn next_page_start_y(&self, current_y: f32) -> f32 {
157        let current_page = self.page_for_y(current_y);
158        self.page_content_start_y(current_page + 1)
159    }
160
161    /// Check if a range [start_y, end_y) crosses a page boundary.
162    pub fn crosses_page_break(&self, start_y: f32, end_y: f32) -> bool {
163        let start_page = self.page_for_y(start_y);
164        let end_page = self.page_for_y(end_y - 0.01); // Subtract epsilon for exclusive end
165        start_page != end_page
166    }
167
168    /// Get remaining space on the current page from a given Y position.
169    pub fn remaining_on_page(&self, y: f32) -> f32 {
170        let page = self.page_for_y(y);
171        let page_end = self.page_content_end_y(page);
172        (page_end - y).max(0.0)
173    }
174
175    /// Check if content of given height can fit starting at Y position.
176    pub fn can_fit(&self, y: f32, height: f32) -> bool {
177        self.remaining_on_page(y) >= height
178    }
179
180    /// Calculate the additional Y offset needed to push content to the next page.
181    /// Returns 0 if content fits on current page.
182    pub fn page_break_offset(&self, y: f32, height: f32) -> f32 {
183        if self.can_fit(y, height) {
184            return 0.0;
185        }
186
187        // Content doesn't fit - calculate offset to move to next page
188        let next_start = self.next_page_start_y(y);
189        next_start - y
190    }
191
192    /// Get the number of pages needed to contain content ending at Y.
193    pub fn page_count(&self, total_content_height: f32) -> usize {
194        if total_content_height <= 0.0 {
195            return 1;
196        }
197        self.page_for_y(total_content_height - 0.01) + 1
198    }
199}
200
201/// CSS break behavior classification for a box.
202#[derive(Debug, Clone, Copy, PartialEq, Eq)]
203pub enum BreakBehavior {
204    /// Box can be split at internal break points (paragraphs, containers)
205    Splittable,
206    /// Box should be kept together if possible (break-inside: avoid)
207    AvoidBreak,
208    /// Box cannot be split (replaced elements, overflow:scroll, etc.)
209    Monolithic,
210}
211
212/// Result of evaluating break properties for a box.
213#[derive(Debug, Clone)]
214pub struct BreakEvaluation {
215    /// Whether to force a page break before this element
216    pub force_break_before: bool,
217    /// Whether to force a page break after this element  
218    pub force_break_after: bool,
219    /// How this box should behave at potential break points
220    pub behavior: BreakBehavior,
221    /// For text: minimum lines to keep at page start (orphans)
222    pub orphans: u32,
223    /// For text: minimum lines to keep at page end (widows)
224    pub widows: u32,
225}
226
227impl Default for BreakEvaluation {
228    fn default() -> Self {
229        Self {
230            force_break_before: false,
231            force_break_after: false,
232            behavior: BreakBehavior::Splittable,
233            orphans: 2,
234            widows: 2,
235        }
236    }
237}
238
239/// Check if a break-before/after value forces a page break.
240pub fn is_forced_break(page_break: PageBreak) -> bool {
241    matches!(
242        page_break,
243        PageBreak::Always
244            | PageBreak::Page
245            | PageBreak::Left
246            | PageBreak::Right
247            | PageBreak::Recto
248            | PageBreak::Verso
249            | PageBreak::All
250    )
251}
252
253/// Check if a break-before/after value avoids breaks.
254pub fn is_avoid_break(page_break: PageBreak) -> bool {
255    matches!(page_break, PageBreak::Avoid | PageBreak::AvoidPage)
256}
257
258/// Metadata about table header repetition for a specific page.
259#[derive(Debug, Clone)]
260pub struct RepeatedTableHeader {
261    /// The Y position on the infinite canvas where this header should appear
262    pub inject_at_y: f32,
263    /// The display list items for the table header (cloned from original)
264    pub header_items: Vec<usize>, // Indices into the original display list
265    /// The height of the header
266    pub header_height: f32,
267}
268
269/// Context for pagination during layout.
270///
271/// This is passed into layout functions to allow them to make page-aware decisions.
272#[derive(Debug)]
273pub struct PaginationContext<'a> {
274    /// The page geometry calculator
275    pub geometer: &'a PageGeometer,
276    /// Accumulated break-inside: avoid depth from ancestors
277    pub break_avoid_depth: usize,
278    /// Track table headers that need to repeat on new pages
279    pub repeated_headers: Vec<RepeatedTableHeader>,
280}
281
282impl<'a> PaginationContext<'a> {
283    pub fn new(geometer: &'a PageGeometer) -> Self {
284        Self {
285            geometer,
286            break_avoid_depth: 0,
287            repeated_headers: Vec::new(),
288        }
289    }
290
291    /// Enter a box with break-inside: avoid
292    pub fn enter_avoid_break(&mut self) {
293        self.break_avoid_depth += 1;
294    }
295
296    /// Exit a box with break-inside: avoid
297    pub fn exit_avoid_break(&mut self) {
298        self.break_avoid_depth = self.break_avoid_depth.saturating_sub(1);
299    }
300
301    /// Check if we're inside an ancestor with break-inside: avoid
302    pub fn is_avoiding_breaks(&self) -> bool {
303        self.break_avoid_depth > 0
304    }
305
306    /// Register a table header for repetition on subsequent pages.
307    pub fn register_repeated_header(
308        &mut self,
309        inject_at_y: f32,
310        header_items: Vec<usize>,
311        header_height: f32,
312    ) {
313        self.repeated_headers.push(RepeatedTableHeader {
314            inject_at_y,
315            header_items,
316            header_height,
317        });
318    }
319}
320
321/// Calculate the position adjustment for a child element considering pagination.
322///
323/// This is called during BFC/IFC layout to determine if content needs to be
324/// pushed to the next page.
325///
326/// # Arguments
327/// * `geometer` - Page geometry calculator
328/// * `main_pen` - Current Y position in infinite canvas coordinates
329/// * `child_height` - Estimated height of the child element
330/// * `break_eval` - Break property evaluation for the child
331/// * `is_avoiding_breaks` - Whether an ancestor has break-inside: avoid
332///
333/// # Returns
334/// The Y offset to add to `main_pen` (0 if no adjustment needed, positive if pushing to next page)
335pub fn calculate_pagination_offset(
336    geometer: &PageGeometer,
337    main_pen: f32,
338    child_height: f32,
339    break_eval: &BreakEvaluation,
340    is_avoiding_breaks: bool,
341) -> f32 {
342    // 1. Handle forced break-before
343    if break_eval.force_break_before {
344        let remaining = geometer.remaining_on_page(main_pen);
345        if remaining < geometer.content_height() {
346            // Not at the start of a page - force break
347            return geometer.page_break_offset(main_pen, f32::MAX);
348        }
349    }
350
351    // 2. Check if content fits on current page
352    let remaining = geometer.remaining_on_page(main_pen);
353
354    // 3. Handle monolithic content (cannot be split)
355    if break_eval.behavior == BreakBehavior::Monolithic {
356        if child_height <= remaining {
357            // Fits on current page
358            return 0.0;
359        }
360        if child_height <= geometer.content_height() {
361            // Doesn't fit but would fit on empty page - move to next
362            return geometer.page_break_offset(main_pen, child_height);
363        }
364        // Too large for any page - let it overflow (no adjustment)
365        return 0.0;
366    }
367
368    // 4. Handle avoid-break content
369    if break_eval.behavior == BreakBehavior::AvoidBreak || is_avoiding_breaks {
370        if child_height <= remaining {
371            // Fits on current page
372            return 0.0;
373        }
374        if child_height <= geometer.content_height() {
375            // Move to next page to keep together
376            return geometer.page_break_offset(main_pen, child_height);
377        }
378        // Too large to keep together - must allow splitting
379    }
380
381    // 5. Splittable content - check orphans/widows constraints
382    // For now, just ensure we have at least some minimum space
383    let min_before_break = 20.0; // ~1-2 lines minimum
384    if remaining < min_before_break && remaining < geometer.content_height() {
385        // Not enough space for even a small amount - move to next page
386        return geometer.page_break_offset(main_pen, child_height);
387    }
388
389    0.0
390}
391
392// CSS GCPM Level 3: Running Elements & Page Margin Boxes
393//
394// This section provides infrastructure for CSS Generated Content for Paged Media
395// Level 3 (https://www.w3.org/TR/css-gcpm-3/).
396//
397// Key concepts:
398//
399// 1. **Running Elements** - Elements with `position: running(header)` are removed from the normal
400//    flow and available for display in page margin boxes.
401//
402// 2. **Page Margin Boxes** - 16 margin boxes around each page (@top-left, @top-center, @top-right,
403//    @bottom-left, etc.) that can contain running elements or generated content.
404//
405// 3. **Named Strings** - Text captured with `string-set: header content(text)` and displayed with
406//    `content: string(header)`.
407//
408// 4. **Page Counters** - `counter(page)` and `counter(pages)` for page numbering.
409
410/// Position of a margin box on a page (CSS GCPM margin box names).
411///
412/// CSS defines 16 margin boxes around the page content area:
413/// ```text
414/// ┌─────────┬─────────────────┬─────────┐
415/// │top-left │   top-center    │top-right│
416/// ├─────────┼─────────────────┼─────────┤
417/// │         │                 │         │
418/// │  left   │                 │  right  │
419/// │  -top   │                 │  -top   │
420/// │         │                 │         │
421/// │  left   │    CONTENT      │  right  │
422/// │-middle  │      AREA       │-middle  │
423/// │         │                 │         │
424/// │  left   │                 │  right  │
425/// │-bottom  │                 │-bottom  │
426/// │         │                 │         │
427/// ├─────────┼─────────────────┼─────────┤
428/// │bot-left │  bottom-center  │bot-right│
429/// └─────────┴─────────────────┴─────────┘
430/// ```
431#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
432pub enum MarginBoxPosition {
433    // Top row
434    TopLeftCorner,
435    TopLeft,
436    TopCenter,
437    TopRight,
438    TopRightCorner,
439    // Left column
440    LeftTop,
441    LeftMiddle,
442    LeftBottom,
443    // Right column
444    RightTop,
445    RightMiddle,
446    RightBottom,
447    // Bottom row
448    BottomLeftCorner,
449    BottomLeft,
450    BottomCenter,
451    BottomRight,
452    BottomRightCorner,
453}
454
455impl MarginBoxPosition {
456    /// Returns true if this margin box is in the top margin area.
457    pub fn is_top(&self) -> bool {
458        matches!(
459            self,
460            Self::TopLeftCorner
461                | Self::TopLeft
462                | Self::TopCenter
463                | Self::TopRight
464                | Self::TopRightCorner
465        )
466    }
467
468    /// Returns true if this margin box is in the bottom margin area.
469    pub fn is_bottom(&self) -> bool {
470        matches!(
471            self,
472            Self::BottomLeftCorner
473                | Self::BottomLeft
474                | Self::BottomCenter
475                | Self::BottomRight
476                | Self::BottomRightCorner
477        )
478    }
479}
480
481/// A running element that was extracted from the document flow.
482///
483/// CSS GCPM allows elements to be "running" - removed from normal flow
484/// and made available for display in page margin boxes.
485///
486/// ```css
487/// h1 { position: running(chapter-title); }
488/// @page { @top-center { content: element(chapter-title); } }
489/// ```
490#[derive(Debug, Clone)]
491pub struct RunningElement {
492    /// The name of this running element (e.g., "chapter-title")
493    pub name: String,
494    /// The display list items for this element (captured when encountered in flow)
495    pub display_items: Vec<super::display_list::DisplayListItem>,
496    /// The size of this element when rendered
497    pub size: LogicalSize,
498    /// Which page this element was defined on (for `running()` selector specificity)
499    pub source_page: usize,
500}
501
502/// Content that can appear in a page margin box.
503///
504/// This enum represents the various types of content that CSS GCPM
505/// allows in margin boxes.
506#[derive(Clone)]
507pub enum MarginBoxContent {
508    /// Empty margin box
509    None,
510    /// A running element referenced by name: `content: element(header)`
511    RunningElement(String),
512    /// A named string: `content: string(chapter)`
513    NamedString(String),
514    /// Page counter: `content: counter(page)`
515    PageCounter,
516    /// Total pages counter: `content: counter(pages)`
517    PagesCounter,
518    /// Page counter with format: `content: counter(page, lower-roman)`
519    PageCounterFormatted { format: CounterFormat },
520    /// Combined content (e.g., "Page " counter(page) " of " counter(pages))
521    Combined(Vec<MarginBoxContent>),
522    /// Literal text
523    Text(String),
524    /// Custom callback for dynamic content generation
525    Custom(Arc<dyn Fn(PageInfo) -> String + Send + Sync>),
526}
527
528impl std::fmt::Debug for MarginBoxContent {
529    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
530        match self {
531            Self::None => write!(f, "None"),
532            Self::RunningElement(s) => f.debug_tuple("RunningElement").field(s).finish(),
533            Self::NamedString(s) => f.debug_tuple("NamedString").field(s).finish(),
534            Self::PageCounter => write!(f, "PageCounter"),
535            Self::PagesCounter => write!(f, "PagesCounter"),
536            Self::PageCounterFormatted { format } => f
537                .debug_struct("PageCounterFormatted")
538                .field("format", format)
539                .finish(),
540            Self::Combined(v) => f.debug_tuple("Combined").field(v).finish(),
541            Self::Text(s) => f.debug_tuple("Text").field(s).finish(),
542            Self::Custom(_) => write!(f, "Custom(<fn>)"),
543        }
544    }
545}
546
547/// Counter formatting styles (subset of CSS list-style-type).
548#[derive(Debug, Clone, Copy, PartialEq, Eq)]
549pub enum CounterFormat {
550    Decimal,
551    DecimalLeadingZero,
552    LowerRoman,
553    UpperRoman,
554    LowerAlpha,
555    UpperAlpha,
556    LowerGreek,
557}
558
559impl Default for CounterFormat {
560    fn default() -> Self {
561        Self::Decimal
562    }
563}
564
565impl CounterFormat {
566    /// Format a number according to this counter style.
567    pub fn format(&self, n: usize) -> String {
568        match self {
569            Self::Decimal => n.to_string(),
570            Self::DecimalLeadingZero => format!("{:02}", n),
571            Self::LowerRoman => to_roman(n, false),
572            Self::UpperRoman => to_roman(n, true),
573            Self::LowerAlpha => to_alpha(n, false),
574            Self::UpperAlpha => to_alpha(n, true),
575            Self::LowerGreek => to_greek(n),
576        }
577    }
578}
579
580/// Convert number to roman numerals.
581fn to_roman(mut n: usize, uppercase: bool) -> String {
582    if n == 0 {
583        return "0".to_string();
584    }
585
586    let numerals = [
587        (1000, "m"),
588        (900, "cm"),
589        (500, "d"),
590        (400, "cd"),
591        (100, "c"),
592        (90, "xc"),
593        (50, "l"),
594        (40, "xl"),
595        (10, "x"),
596        (9, "ix"),
597        (5, "v"),
598        (4, "iv"),
599        (1, "i"),
600    ];
601
602    let mut result = String::new();
603    for (value, numeral) in &numerals {
604        while n >= *value {
605            result.push_str(numeral);
606            n -= value;
607        }
608    }
609
610    if uppercase {
611        result.to_uppercase()
612    } else {
613        result
614    }
615}
616
617/// Convert number to alphabetic (a-z, aa-az, etc.).
618fn to_alpha(n: usize, uppercase: bool) -> String {
619    if n == 0 {
620        return "0".to_string();
621    }
622
623    let mut result = String::new();
624    let mut remaining = n;
625
626    while remaining > 0 {
627        remaining -= 1;
628        let c = ((remaining % 26) as u8 + if uppercase { b'A' } else { b'a' }) as char;
629        result.insert(0, c);
630        remaining /= 26;
631    }
632
633    result
634}
635
636/// Convert number to Greek letters (α, β, γ, ...).
637fn to_greek(n: usize) -> String {
638    const GREEK: &[char] = &[
639        'α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η', 'θ', 'ι', 'κ', 'λ', 'μ', 'ν', 'ξ', 'ο', 'π', 'ρ', 'σ',
640        'τ', 'υ', 'φ', 'χ', 'ψ', 'ω',
641    ];
642    if n == 0 {
643        return "0".to_string();
644    }
645    if n <= GREEK.len() {
646        return GREEK[n - 1].to_string();
647    }
648
649    // For numbers > 24, use αα, αβ, etc.
650    let mut result = String::new();
651    let mut remaining = n;
652    while remaining > 0 {
653        remaining -= 1;
654        result.insert(0, GREEK[remaining % GREEK.len()]);
655        remaining /= GREEK.len();
656    }
657    result
658}
659
660/// Information about the current page, passed to content generators.
661#[derive(Debug, Clone, Copy)]
662pub struct PageInfo {
663    /// Current page number (1-indexed for display)
664    pub page_number: usize,
665    /// Total number of pages (may be 0 if unknown during first pass)
666    pub total_pages: usize,
667    /// Whether this is the first page
668    pub is_first: bool,
669    /// Whether this is the last page
670    pub is_last: bool,
671    /// Whether this is a left (verso) page (for duplex printing)
672    pub is_left: bool,
673    /// Whether this is a right (recto) page
674    pub is_right: bool,
675    /// Whether this is a blank page (inserted for left/right alignment)
676    pub is_blank: bool,
677}
678
679impl PageInfo {
680    /// Create PageInfo for a specific page.
681    pub fn new(page_number: usize, total_pages: usize) -> Self {
682        Self {
683            page_number,
684            total_pages,
685            is_first: page_number == 1,
686            is_last: total_pages > 0 && page_number == total_pages,
687            is_left: page_number % 2 == 0, // Even pages are left (verso)
688            is_right: page_number % 2 == 1, // Odd pages are right (recto)
689            is_blank: false,
690        }
691    }
692}
693
694/// Configuration for page headers and footers.
695///
696/// This is a simplified interface for the common case of adding
697/// headers and footers. For full GCPM support, use `PageTemplate`.
698#[derive(Debug, Clone)]
699pub struct HeaderFooterConfig {
700    /// Whether to show a header on each page
701    pub show_header: bool,
702    /// Whether to show a footer on each page
703    pub show_footer: bool,
704    /// Height of the header area (if shown)
705    pub header_height: f32,
706    /// Height of the footer area (if shown)  
707    pub footer_height: f32,
708    /// Content generator for the header
709    pub header_content: MarginBoxContent,
710    /// Content generator for the footer
711    pub footer_content: MarginBoxContent,
712    /// Font size for header/footer text
713    pub font_size: f32,
714    /// Text color for header/footer
715    pub text_color: ColorU,
716    /// Whether to skip header/footer on first page
717    pub skip_first_page: bool,
718}
719
720impl Default for HeaderFooterConfig {
721    fn default() -> Self {
722        Self {
723            show_header: false,
724            show_footer: false,
725            header_height: 30.0,
726            footer_height: 30.0,
727            header_content: MarginBoxContent::None,
728            footer_content: MarginBoxContent::None,
729            font_size: 10.0,
730            text_color: ColorU {
731                r: 0,
732                g: 0,
733                b: 0,
734                a: 255,
735            },
736            skip_first_page: false,
737        }
738    }
739}
740
741impl HeaderFooterConfig {
742    /// Create a config with page numbers in the footer.
743    pub fn with_page_numbers() -> Self {
744        Self {
745            show_footer: true,
746            footer_content: MarginBoxContent::Combined(vec![
747                MarginBoxContent::Text("Page ".to_string()),
748                MarginBoxContent::PageCounter,
749                MarginBoxContent::Text(" of ".to_string()),
750                MarginBoxContent::PagesCounter,
751            ]),
752            ..Default::default()
753        }
754    }
755
756    /// Create a config with page numbers in both header and footer.
757    pub fn with_header_and_footer_page_numbers() -> Self {
758        Self {
759            show_header: true,
760            show_footer: true,
761            header_content: MarginBoxContent::Combined(vec![
762                MarginBoxContent::Text("Page ".to_string()),
763                MarginBoxContent::PageCounter,
764            ]),
765            footer_content: MarginBoxContent::Combined(vec![
766                MarginBoxContent::Text("Page ".to_string()),
767                MarginBoxContent::PageCounter,
768                MarginBoxContent::Text(" of ".to_string()),
769                MarginBoxContent::PagesCounter,
770            ]),
771            ..Default::default()
772        }
773    }
774
775    /// Set custom header text.
776    pub fn with_header_text(mut self, text: impl Into<String>) -> Self {
777        self.show_header = true;
778        self.header_content = MarginBoxContent::Text(text.into());
779        self
780    }
781
782    /// Set custom footer text.
783    pub fn with_footer_text(mut self, text: impl Into<String>) -> Self {
784        self.show_footer = true;
785        self.footer_content = MarginBoxContent::Text(text.into());
786        self
787    }
788
789    /// Generate the text content for a margin box given page info.
790    pub fn generate_content(&self, content: &MarginBoxContent, info: PageInfo) -> String {
791        match content {
792            MarginBoxContent::None => String::new(),
793            MarginBoxContent::Text(s) => s.clone(),
794            MarginBoxContent::PageCounter => info.page_number.to_string(),
795            MarginBoxContent::PagesCounter => {
796                if info.total_pages > 0 {
797                    info.total_pages.to_string()
798                } else {
799                    "?".to_string()
800                }
801            }
802            MarginBoxContent::PageCounterFormatted { format } => format.format(info.page_number),
803            MarginBoxContent::Combined(parts) => parts
804                .iter()
805                .map(|p| self.generate_content(p, info))
806                .collect(),
807            MarginBoxContent::NamedString(name) => {
808                // TODO: Look up named string from document context
809                format!("[string:{}]", name)
810            }
811            MarginBoxContent::RunningElement(name) => {
812                // Running elements are rendered as display items, not text
813                format!("[element:{}]", name)
814            }
815            MarginBoxContent::Custom(f) => f(info),
816        }
817    }
818
819    /// Get the header text for a specific page.
820    pub fn header_text(&self, info: PageInfo) -> String {
821        if !self.show_header {
822            return String::new();
823        }
824        if self.skip_first_page && info.is_first {
825            return String::new();
826        }
827        self.generate_content(&self.header_content, info)
828    }
829
830    /// Get the footer text for a specific page.
831    pub fn footer_text(&self, info: PageInfo) -> String {
832        if !self.show_footer {
833            return String::new();
834        }
835        if self.skip_first_page && info.is_first {
836            return String::new();
837        }
838        self.generate_content(&self.footer_content, info)
839    }
840}
841
842/// Full page template with all 16 margin boxes (CSS GCPM @page support).
843///
844/// This provides complete control over page layout following the CSS
845/// Paged Media and GCPM specifications.
846#[derive(Debug, Clone, Default)]
847pub struct PageTemplate {
848    /// Content for each margin box position
849    pub margin_boxes: BTreeMap<MarginBoxPosition, MarginBoxContent>,
850    /// Page margins (space allocated for margin boxes)
851    pub margins: PageMargins,
852    /// Named strings captured from the document
853    pub named_strings: BTreeMap<String, String>,
854    /// Running elements available for this page
855    pub running_elements: BTreeMap<String, RunningElement>,
856}
857
858impl PageTemplate {
859    /// Create a new empty page template.
860    pub fn new() -> Self {
861        Self::default()
862    }
863
864    /// Set content for a specific margin box.
865    pub fn set_margin_box(&mut self, position: MarginBoxPosition, content: MarginBoxContent) {
866        self.margin_boxes.insert(position, content);
867    }
868
869    /// Create a simple template with centered page numbers in the footer.
870    pub fn with_centered_page_numbers() -> Self {
871        let mut template = Self::new();
872        template.set_margin_box(
873            MarginBoxPosition::BottomCenter,
874            MarginBoxContent::PageCounter,
875        );
876        template
877    }
878
879    /// Create a template with "Page X of Y" in the bottom right.
880    pub fn with_page_x_of_y() -> Self {
881        let mut template = Self::new();
882        template.set_margin_box(
883            MarginBoxPosition::BottomRight,
884            MarginBoxContent::Combined(vec![
885                MarginBoxContent::Text("Page ".to_string()),
886                MarginBoxContent::PageCounter,
887                MarginBoxContent::Text(" of ".to_string()),
888                MarginBoxContent::PagesCounter,
889            ]),
890        );
891        template
892    }
893}
894
895// Fake @page support (temporary solution)
896//
897// The following structures provide a programmatic API to configure
898// page headers and footers WITHOUT full CSS @page rule parsing.
899// This is a temporary solution until proper CSS @page support is implemented.
900//
901// Usage example:
902//
903// ```rust
904// let config = FakePageConfig::new()
905//     .with_footer_page_numbers()
906//     .with_header_text("My Document")
907//     .skip_first_page(true);
908//
909// let header_footer = config.to_header_footer_config();
910// ```
911
912/// Temporary configuration for page headers/footers without CSS @page parsing.
913///
914/// This is a "fake" implementation that provides programmatic control over
915/// page decoration until full CSS `@page` rule support is implemented.
916///
917/// ## Supported Features
918///
919/// - Page numbers in header and/or footer
920/// - Custom text in header and/or footer
921/// - Number format (decimal, roman numerals, alphabetic, greek)
922/// - Skip first page option
923#[derive(Debug, Clone)]
924pub struct FakePageConfig {
925    /// Show header on pages
926    pub show_header: bool,
927    /// Show footer on pages
928    pub show_footer: bool,
929    /// Header text (static text, or None for page numbers only)
930    pub header_text: Option<String>,
931    /// Footer text (static text, or None for page numbers only)
932    pub footer_text: Option<String>,
933    /// Include page number in header
934    pub header_page_number: bool,
935    /// Include page number in footer
936    pub footer_page_number: bool,
937    /// Include total pages count ("of Y") in header
938    pub header_total_pages: bool,
939    /// Include total pages count ("of Y") in footer
940    pub footer_total_pages: bool,
941    /// Number format for page counters
942    pub number_format: CounterFormat,
943    /// Skip header/footer on first page
944    pub skip_first_page: bool,
945    /// Header height in points
946    pub header_height: f32,
947    /// Footer height in points
948    pub footer_height: f32,
949    /// Font size for header/footer text
950    pub font_size: f32,
951    /// Text color for header/footer
952    pub text_color: ColorU,
953}
954
955impl Default for FakePageConfig {
956    fn default() -> Self {
957        Self {
958            show_header: false,
959            show_footer: false,
960            header_text: None,
961            footer_text: None,
962            header_page_number: false,
963            footer_page_number: false,
964            header_total_pages: false,
965            footer_total_pages: false,
966            number_format: CounterFormat::Decimal,
967            skip_first_page: false,
968            header_height: 30.0,
969            footer_height: 30.0,
970            font_size: 10.0,
971            text_color: ColorU {
972                r: 0,
973                g: 0,
974                b: 0,
975                a: 255,
976            },
977        }
978    }
979}
980
981impl FakePageConfig {
982    /// Create a new empty configuration (no headers/footers).
983    pub fn new() -> Self {
984        Self::default()
985    }
986
987    /// Enable footer with "Page X of Y" format.
988    pub fn with_footer_page_numbers(mut self) -> Self {
989        self.show_footer = true;
990        self.footer_page_number = true;
991        self.footer_total_pages = true;
992        self
993    }
994
995    /// Enable header with "Page X" format.
996    pub fn with_header_page_numbers(mut self) -> Self {
997        self.show_header = true;
998        self.header_page_number = true;
999        self
1000    }
1001
1002    /// Enable both header and footer with page numbers.
1003    pub fn with_header_and_footer_page_numbers(mut self) -> Self {
1004        self.show_header = true;
1005        self.show_footer = true;
1006        self.header_page_number = true;
1007        self.footer_page_number = true;
1008        self.footer_total_pages = true;
1009        self
1010    }
1011
1012    /// Set custom header text.
1013    pub fn with_header_text(mut self, text: impl Into<String>) -> Self {
1014        self.show_header = true;
1015        self.header_text = Some(text.into());
1016        self
1017    }
1018
1019    /// Set custom footer text.
1020    pub fn with_footer_text(mut self, text: impl Into<String>) -> Self {
1021        self.show_footer = true;
1022        self.footer_text = Some(text.into());
1023        self
1024    }
1025
1026    /// Set the number format for page counters.
1027    pub fn with_number_format(mut self, format: CounterFormat) -> Self {
1028        self.number_format = format;
1029        self
1030    }
1031
1032    /// Skip header/footer on the first page.
1033    pub fn skip_first_page(mut self, skip: bool) -> Self {
1034        self.skip_first_page = skip;
1035        self
1036    }
1037
1038    /// Set header height.
1039    pub fn with_header_height(mut self, height: f32) -> Self {
1040        self.header_height = height;
1041        self
1042    }
1043
1044    /// Set footer height.
1045    pub fn with_footer_height(mut self, height: f32) -> Self {
1046        self.footer_height = height;
1047        self
1048    }
1049
1050    /// Set font size for header/footer text.
1051    pub fn with_font_size(mut self, size: f32) -> Self {
1052        self.font_size = size;
1053        self
1054    }
1055
1056    /// Set text color for header/footer.
1057    pub fn with_text_color(mut self, color: ColorU) -> Self {
1058        self.text_color = color;
1059        self
1060    }
1061
1062    /// Convert this fake config to the internal HeaderFooterConfig.
1063    ///
1064    /// This is the bridge between the user-facing API and the internal
1065    /// pagination engine.
1066    pub fn to_header_footer_config(&self) -> HeaderFooterConfig {
1067        HeaderFooterConfig {
1068            show_header: self.show_header,
1069            show_footer: self.show_footer,
1070            header_height: self.header_height,
1071            footer_height: self.footer_height,
1072            header_content: self.build_header_content(),
1073            footer_content: self.build_footer_content(),
1074            skip_first_page: self.skip_first_page,
1075            font_size: self.font_size,
1076            text_color: self.text_color,
1077        }
1078    }
1079
1080    /// Build the MarginBoxContent for the header.
1081    fn build_header_content(&self) -> MarginBoxContent {
1082        let mut parts = Vec::new();
1083
1084        // Add custom text if present
1085        if let Some(ref text) = self.header_text {
1086            parts.push(MarginBoxContent::Text(text.clone()));
1087            if self.header_page_number {
1088                parts.push(MarginBoxContent::Text(" - ".to_string()));
1089            }
1090        }
1091
1092        // Add page number if enabled
1093        if self.header_page_number {
1094            if self.number_format == CounterFormat::Decimal {
1095                parts.push(MarginBoxContent::Text("Page ".to_string()));
1096                parts.push(MarginBoxContent::PageCounter);
1097            } else {
1098                parts.push(MarginBoxContent::Text("Page ".to_string()));
1099                parts.push(MarginBoxContent::PageCounterFormatted {
1100                    format: self.number_format,
1101                });
1102            }
1103
1104            if self.header_total_pages {
1105                parts.push(MarginBoxContent::Text(" of ".to_string()));
1106                parts.push(MarginBoxContent::PagesCounter);
1107            }
1108        }
1109
1110        if parts.is_empty() {
1111            MarginBoxContent::None
1112        } else if parts.len() == 1 {
1113            parts.pop().unwrap()
1114        } else {
1115            MarginBoxContent::Combined(parts)
1116        }
1117    }
1118
1119    /// Build the MarginBoxContent for the footer.
1120    fn build_footer_content(&self) -> MarginBoxContent {
1121        let mut parts = Vec::new();
1122
1123        // Add custom text if present
1124        if let Some(ref text) = self.footer_text {
1125            parts.push(MarginBoxContent::Text(text.clone()));
1126            if self.footer_page_number {
1127                parts.push(MarginBoxContent::Text(" - ".to_string()));
1128            }
1129        }
1130
1131        // Add page number if enabled
1132        if self.footer_page_number {
1133            if self.number_format == CounterFormat::Decimal {
1134                parts.push(MarginBoxContent::Text("Page ".to_string()));
1135                parts.push(MarginBoxContent::PageCounter);
1136            } else {
1137                parts.push(MarginBoxContent::Text("Page ".to_string()));
1138                parts.push(MarginBoxContent::PageCounterFormatted {
1139                    format: self.number_format,
1140                });
1141            }
1142
1143            if self.footer_total_pages {
1144                parts.push(MarginBoxContent::Text(" of ".to_string()));
1145                parts.push(MarginBoxContent::PagesCounter);
1146            }
1147        }
1148
1149        if parts.is_empty() {
1150            MarginBoxContent::None
1151        } else if parts.len() == 1 {
1152            parts.pop().unwrap()
1153        } else {
1154            MarginBoxContent::Combined(parts)
1155        }
1156    }
1157}
1158
1159/// Information about a table that may need header repetition.
1160#[derive(Debug, Clone)]
1161pub struct TableHeaderInfo {
1162    /// The table's node index in the layout tree
1163    pub table_node_index: usize,
1164    /// The Y position where the table starts
1165    pub table_start_y: f32,
1166    /// The Y position where the table ends
1167    pub table_end_y: f32,
1168    /// The thead's display list items (captured during initial render)
1169    pub thead_items: Vec<super::display_list::DisplayListItem>,
1170    /// Height of the thead
1171    pub thead_height: f32,
1172    /// The Y position of the thead relative to table start
1173    pub thead_offset_y: f32,
1174}
1175
1176/// Context for tracking table headers across pages.
1177#[derive(Debug, Default, Clone)]
1178pub struct TableHeaderTracker {
1179    /// All tables with theads that might need repetition
1180    pub tables: Vec<TableHeaderInfo>,
1181}
1182
1183impl TableHeaderTracker {
1184    pub fn new() -> Self {
1185        Self::default()
1186    }
1187
1188    /// Register a table's thead for potential repetition.
1189    pub fn register_table_header(&mut self, info: TableHeaderInfo) {
1190        self.tables.push(info);
1191    }
1192
1193    /// Get theads that should be repeated on a specific page.
1194    ///
1195    /// Returns the thead items that need to be injected at the top of the page,
1196    /// along with the Y offset where they should appear.
1197    pub fn get_repeated_headers_for_page(
1198        &self,
1199        page_index: usize,
1200        page_top_y: f32,
1201        page_bottom_y: f32,
1202    ) -> Vec<(f32, &[super::display_list::DisplayListItem], f32)> {
1203        let mut headers = Vec::new();
1204
1205        for table in &self.tables {
1206            // Check if this table spans into this page (but didn't start on this page)
1207            let table_starts_before_page = table.table_start_y < page_top_y;
1208            let table_continues_on_page = table.table_end_y > page_top_y;
1209
1210            if table_starts_before_page && table_continues_on_page {
1211                // This table needs its header repeated on this page
1212                // The header should appear at the top of the page content area
1213                headers.push((
1214                    0.0, // Y offset from page top (header goes at very top)
1215                    table.thead_items.as_slice(),
1216                    table.thead_height,
1217                ));
1218            }
1219        }
1220
1221        headers
1222    }
1223}