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
//! **Note:** Running elements, named strings, and page selectors are currently
35
//! stub implementations. Only page counters and header/footer configuration
36
//! are functional.
37
//!
38
//! See: https://www.w3.org/TR/css-gcpm-3/
39

            
40
use std::{collections::BTreeMap, sync::Arc};
41

            
42
use azul_core::geom::LogicalSize;
43
use azul_css::props::{
44
    basic::ColorU,
45
    layout::fragmentation::PageBreak,
46
};
47

            
48
/// Manages the infinite canvas coordinate system with page boundaries.
49
///
50
/// The `PageGeometer` tracks page dimensions and provides utilities for:
51
///
52
/// - Determining which page a Y coordinate falls on
53
/// - Calculating the next page start position
54
/// - Checking if content crosses page boundaries
55
#[derive(Debug, Clone)]
56
pub struct PageGeometer {
57
    /// Total height of each page (including margins, headers, footers)
58
    pub page_size: LogicalSize,
59
    /// Content area margins (space reserved at top/bottom of each page)
60
    pub page_margins: PageMargins,
61
    /// Height reserved for page header (if any)
62
    pub header_height: f32,
63
    /// Height reserved for page footer (if any)
64
    pub footer_height: f32,
65
    /// Current Y position on the infinite canvas
66
    pub current_y: f32,
67
}
68

            
69
/// Page margin configuration
70
#[derive(Debug, Clone, Copy, Default)]
71
pub struct PageMargins {
72
    pub top: f32,
73
    pub right: f32,
74
    pub bottom: f32,
75
    pub left: f32,
76
}
77

            
78
impl PageMargins {
79
    pub fn new(top: f32, right: f32, bottom: f32, left: f32) -> Self {
80
        Self {
81
            top,
82
            right,
83
            bottom,
84
            left,
85
        }
86
    }
87

            
88
    pub fn uniform(margin: f32) -> Self {
89
        Self {
90
            top: margin,
91
            right: margin,
92
            bottom: margin,
93
            left: margin,
94
        }
95
    }
96
}
97

            
98
impl PageGeometer {
99
    /// Create a new PageGeometer for paged layout.
100
    pub fn new(page_size: LogicalSize, margins: PageMargins) -> Self {
101
        Self {
102
            page_size,
103
            page_margins: margins,
104
            header_height: 0.0,
105
            footer_height: 0.0,
106
            current_y: 0.0,
107
        }
108
    }
109

            
110
    /// Create with header and footer space reserved.
111
    pub fn with_header_footer(mut self, header: f32, footer: f32) -> Self {
112
        self.header_height = header;
113
        self.footer_height = footer;
114
        self
115
    }
116

            
117
    /// Get the usable content height per page (page height minus margins/headers/footers).
118
    pub fn content_height(&self) -> f32 {
119
        self.page_size.height
120
            - self.page_margins.top
121
            - self.page_margins.bottom
122
            - self.header_height
123
            - self.footer_height
124
    }
125

            
126
    /// Get the usable content width per page (page width minus left/right margins).
127
    pub fn content_width(&self) -> f32 {
128
        self.page_size.width - self.page_margins.left - self.page_margins.right
129
    }
130

            
131
    /// Calculate which page a given Y coordinate falls on (0-indexed).
132
    ///
133
    /// Negative Y values are clamped to page 0.
134
    pub fn page_for_y(&self, y: f32) -> usize {
135
        if y < 0.0 {
136
            return 0;
137
        }
138
        let content_h = self.content_height();
139
        if content_h <= 0.0 {
140
            return 0;
141
        }
142

            
143
        // Account for dead zones between pages
144
        let full_page_slot = content_h + self.dead_zone_height();
145
        (y / full_page_slot).floor() as usize
146
    }
147

            
148
    /// Get the Y coordinate where a page's content area starts.
149
    pub fn page_content_start_y(&self, page_index: usize) -> f32 {
150
        let full_page_slot = self.content_height() + self.dead_zone_height();
151
        page_index as f32 * full_page_slot
152
    }
153

            
154
    /// Get the Y coordinate where a page's content area ends.
155
    pub fn page_content_end_y(&self, page_index: usize) -> f32 {
156
        self.page_content_start_y(page_index) + self.content_height()
157
    }
158

            
159
    /// Get the height of the "dead zone" between pages (footer + margin + header of next page).
160
    pub fn dead_zone_height(&self) -> f32 {
161
        self.footer_height + self.page_margins.bottom + self.page_margins.top + self.header_height
162
    }
163

            
164
    /// Calculate the Y coordinate where the NEXT page's content starts from a given position.
165
    pub fn next_page_start_y(&self, current_y: f32) -> f32 {
166
        let current_page = self.page_for_y(current_y);
167
        self.page_content_start_y(current_page + 1)
168
    }
169

            
170
    /// Check if a range [start_y, end_y) crosses a page boundary.
171
    pub fn crosses_page_break(&self, start_y: f32, end_y: f32) -> bool {
172
        let start_page = self.page_for_y(start_y);
173
        let end_page = self.page_for_y(end_y - 0.01); // Subtract epsilon for exclusive end
174
        start_page != end_page
175
    }
176

            
177
    /// Get remaining space on the current page from a given Y position.
178
    pub fn remaining_on_page(&self, y: f32) -> f32 {
179
        let page = self.page_for_y(y);
180
        let page_end = self.page_content_end_y(page);
181
        (page_end - y).max(0.0)
182
    }
183

            
184
    /// Check if content of given height can fit starting at Y position.
185
    pub fn can_fit(&self, y: f32, height: f32) -> bool {
186
        self.remaining_on_page(y) >= height
187
    }
188

            
189
    /// Calculate the additional Y offset needed to push content to the next page.
190
    /// Returns 0 if content fits on current page.
191
    pub fn page_break_offset(&self, y: f32, height: f32) -> f32 {
192
        if self.can_fit(y, height) {
193
            return 0.0;
194
        }
195

            
196
        // Content doesn't fit - calculate offset to move to next page
197
        let next_start = self.next_page_start_y(y);
198
        next_start - y
199
    }
200

            
201
    /// Get the number of pages needed to contain content ending at Y.
202
    pub fn page_count(&self, total_content_height: f32) -> usize {
203
        if total_content_height <= 0.0 {
204
            return 1;
205
        }
206
        self.page_for_y(total_content_height - 0.01) + 1
207
    }
208
}
209

            
210
/// CSS break behavior classification for a box.
211
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
212
pub enum BreakBehavior {
213
    /// Box can be split at internal break points (paragraphs, containers)
214
    Splittable,
215
    /// Box should be kept together if possible (break-inside: avoid)
216
    AvoidBreak,
217
    /// Box cannot be split (replaced elements, overflow:scroll, etc.)
218
    Monolithic,
219
}
220

            
221
/// Result of evaluating break properties for a box.
222
#[derive(Debug, Clone)]
223
pub struct BreakEvaluation {
224
    /// Whether to force a page break before this element
225
    pub force_break_before: bool,
226
    /// Whether to force a page break after this element  
227
    pub force_break_after: bool,
228
    /// How this box should behave at potential break points
229
    pub behavior: BreakBehavior,
230
    /// For text: minimum lines to keep at page start (orphans)
231
    pub orphans: u32,
232
    /// For text: minimum lines to keep at page end (widows)
233
    pub widows: u32,
234
}
235

            
236
impl Default for BreakEvaluation {
237
    fn default() -> Self {
238
        Self {
239
            force_break_before: false,
240
            force_break_after: false,
241
            behavior: BreakBehavior::Splittable,
242
            orphans: 2,
243
            widows: 2,
244
        }
245
    }
246
}
247

            
248
/// Check if a break-before/after value forces a page break.
249
pub fn is_forced_break(page_break: PageBreak) -> bool {
250
    matches!(
251
        page_break,
252
        PageBreak::Always
253
            | PageBreak::Page
254
            | PageBreak::Left
255
            | PageBreak::Right
256
            | PageBreak::Recto
257
            | PageBreak::Verso
258
            | PageBreak::All
259
    )
260
}
261

            
262
/// Check if a break-before/after value avoids breaks.
263
pub fn is_avoid_break(page_break: PageBreak) -> bool {
264
    matches!(page_break, PageBreak::Avoid | PageBreak::AvoidPage)
265
}
266

            
267
/// Metadata about table header repetition for a specific page.
268
#[derive(Debug, Clone)]
269
pub struct RepeatedTableHeader {
270
    /// The Y position on the infinite canvas where this header should appear
271
    pub inject_at_y: f32,
272
    /// The display list items for the table header (cloned from original)
273
    pub header_items: Vec<usize>, // Indices into the original display list
274
    /// The height of the header
275
    pub header_height: f32,
276
}
277

            
278
/// Context for pagination during layout.
279
///
280
/// This is passed into layout functions to allow them to make page-aware decisions.
281
#[derive(Debug)]
282
pub struct PaginationContext<'a> {
283
    /// The page geometry calculator
284
    pub geometer: &'a PageGeometer,
285
    /// Accumulated break-inside: avoid depth from ancestors
286
    pub break_avoid_depth: usize,
287
    /// Track table headers that need to repeat on new pages
288
    pub repeated_headers: Vec<RepeatedTableHeader>,
289
}
290

            
291
impl<'a> PaginationContext<'a> {
292
    pub fn new(geometer: &'a PageGeometer) -> Self {
293
        Self {
294
            geometer,
295
            break_avoid_depth: 0,
296
            repeated_headers: Vec::new(),
297
        }
298
    }
299

            
300
    /// Enter a box with break-inside: avoid
301
    pub fn enter_avoid_break(&mut self) {
302
        self.break_avoid_depth += 1;
303
    }
304

            
305
    /// Exit a box with break-inside: avoid
306
    pub fn exit_avoid_break(&mut self) {
307
        self.break_avoid_depth = self.break_avoid_depth.saturating_sub(1);
308
    }
309

            
310
    /// Check if we're inside an ancestor with break-inside: avoid
311
    pub fn is_avoiding_breaks(&self) -> bool {
312
        self.break_avoid_depth > 0
313
    }
314

            
315
    /// Register a table header for repetition on subsequent pages.
316
    pub fn register_repeated_header(
317
        &mut self,
318
        inject_at_y: f32,
319
        header_items: Vec<usize>,
320
        header_height: f32,
321
    ) {
322
        self.repeated_headers.push(RepeatedTableHeader {
323
            inject_at_y,
324
            header_items,
325
            header_height,
326
        });
327
    }
328
}
329

            
330
/// Calculate the position adjustment for a child element considering pagination.
331
///
332
/// This is called during BFC/IFC layout to determine if content needs to be
333
/// pushed to the next page.
334
///
335
/// # Arguments
336
/// * `geometer` - Page geometry calculator
337
/// * `main_pen` - Current Y position in infinite canvas coordinates
338
/// * `child_height` - Estimated height of the child element
339
/// * `break_eval` - Break property evaluation for the child
340
/// * `is_avoiding_breaks` - Whether an ancestor has break-inside: avoid
341
///
342
/// # Returns
343
/// The Y offset to add to `main_pen` (0 if no adjustment needed, positive if pushing to next page)
344
pub fn calculate_pagination_offset(
345
    geometer: &PageGeometer,
346
    main_pen: f32,
347
    child_height: f32,
348
    break_eval: &BreakEvaluation,
349
    is_avoiding_breaks: bool,
350
) -> f32 {
351
    // 1. Handle forced break-before
352
    if break_eval.force_break_before {
353
        let remaining = geometer.remaining_on_page(main_pen);
354
        if remaining < geometer.content_height() {
355
            // Not at the start of a page - force break
356
            return geometer.page_break_offset(main_pen, f32::MAX);
357
        }
358
    }
359

            
360
    // 2. Check if content fits on current page
361
    let remaining = geometer.remaining_on_page(main_pen);
362

            
363
    // 3. Handle monolithic content (cannot be split)
364
    // +spec:inline-formatting-context:cb2a20 - initial letter boxes are monolithic for block-axis fragmentation; breaks between lines alongside an initial letter should be avoided (like widows/orphans), but forced breaks take precedence
365
    if break_eval.behavior == BreakBehavior::Monolithic {
366
        if child_height <= remaining {
367
            // Fits on current page
368
            return 0.0;
369
        }
370
        if child_height <= geometer.content_height() {
371
            // Doesn't fit but would fit on empty page - move to next
372
            return geometer.page_break_offset(main_pen, child_height);
373
        }
374
        // Too large for any page - let it overflow (no adjustment)
375
        return 0.0;
376
    }
377

            
378
    // 4. Handle avoid-break content
379
    if break_eval.behavior == BreakBehavior::AvoidBreak || is_avoiding_breaks {
380
        if child_height <= remaining {
381
            // Fits on current page
382
            return 0.0;
383
        }
384
        if child_height <= geometer.content_height() {
385
            // Move to next page to keep together
386
            return geometer.page_break_offset(main_pen, child_height);
387
        }
388
        // Too large to keep together - must allow splitting
389
    }
390

            
391
    // 5. Splittable content - check orphans/widows constraints
392
    // For now, just ensure we have at least some minimum space
393
    let min_before_break = 20.0; // ~1-2 lines minimum
394
    if remaining < min_before_break && remaining < geometer.content_height() {
395
        // Not enough space for even a small amount - move to next page
396
        return geometer.page_break_offset(main_pen, child_height);
397
    }
398

            
399
    0.0
400
}
401

            
402
// CSS GCPM Level 3: Running Elements & Page Margin Boxes
403
//
404
// This section provides infrastructure for CSS Generated Content for Paged Media
405
// Level 3 (https://www.w3.org/TR/css-gcpm-3/).
406
//
407
// Key concepts:
408
//
409
// 1. **Running Elements** - Elements with `position: running(header)` are removed from the normal
410
//    flow and available for display in page margin boxes.
411
//
412
// 2. **Page Margin Boxes** - 16 margin boxes around each page (@top-left, @top-center, @top-right,
413
//    @bottom-left, etc.) that can contain running elements or generated content.
414
//
415
// 3. **Named Strings** - Text captured with `string-set: header content(text)` and displayed with
416
//    `content: string(header)`.
417
//
418
// 4. **Page Counters** - `counter(page)` and `counter(pages)` for page numbering.
419

            
420
/// Position of a margin box on a page (CSS GCPM margin box names).
421
///
422
/// CSS defines 16 margin boxes around the page content area:
423
/// ```text
424
/// ┌─────────┬─────────────────┬─────────┐
425
/// │top-left │   top-center    │top-right│
426
/// ├─────────┼─────────────────┼─────────┤
427
/// │         │                 │         │
428
/// │  left   │                 │  right  │
429
/// │  -top   │                 │  -top   │
430
/// │         │                 │         │
431
/// │  left   │    CONTENT      │  right  │
432
/// │-middle  │      AREA       │-middle  │
433
/// │         │                 │         │
434
/// │  left   │                 │  right  │
435
/// │-bottom  │                 │-bottom  │
436
/// │         │                 │         │
437
/// ├─────────┼─────────────────┼─────────┤
438
/// │bot-left │  bottom-center  │bot-right│
439
/// └─────────┴─────────────────┴─────────┘
440
/// ```
441
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
442
pub enum MarginBoxPosition {
443
    // Top row
444
    TopLeftCorner,
445
    TopLeft,
446
    TopCenter,
447
    TopRight,
448
    TopRightCorner,
449
    // Left column
450
    LeftTop,
451
    LeftMiddle,
452
    LeftBottom,
453
    // Right column
454
    RightTop,
455
    RightMiddle,
456
    RightBottom,
457
    // Bottom row
458
    BottomLeftCorner,
459
    BottomLeft,
460
    BottomCenter,
461
    BottomRight,
462
    BottomRightCorner,
463
}
464

            
465
impl MarginBoxPosition {
466
    /// Returns true if this margin box is in the top margin area.
467
    pub fn is_top(&self) -> bool {
468
        matches!(
469
            self,
470
            Self::TopLeftCorner
471
                | Self::TopLeft
472
                | Self::TopCenter
473
                | Self::TopRight
474
                | Self::TopRightCorner
475
        )
476
    }
477

            
478
    /// Returns true if this margin box is in the bottom margin area.
479
    pub fn is_bottom(&self) -> bool {
480
        matches!(
481
            self,
482
            Self::BottomLeftCorner
483
                | Self::BottomLeft
484
                | Self::BottomCenter
485
                | Self::BottomRight
486
                | Self::BottomRightCorner
487
        )
488
    }
489
}
490

            
491
/// A running element that was extracted from the document flow.
492
///
493
/// CSS GCPM allows elements to be "running" - removed from normal flow
494
/// and made available for display in page margin boxes.
495
///
496
/// ```css
497
/// h1 { position: running(chapter-title); }
498
/// @page { @top-center { content: element(chapter-title); } }
499
/// ```
500
#[derive(Debug, Clone)]
501
pub struct RunningElement {
502
    /// The name of this running element (e.g., "chapter-title")
503
    pub name: String,
504
    /// The display list items for this element (captured when encountered in flow)
505
    pub display_items: Vec<super::display_list::DisplayListItem>,
506
    /// The size of this element when rendered
507
    pub size: LogicalSize,
508
    /// Which page this element was defined on (for `running()` selector specificity)
509
    pub source_page: usize,
510
}
511

            
512
/// Content that can appear in a page margin box.
513
///
514
/// This enum represents the various types of content that CSS GCPM
515
/// allows in margin boxes.
516
#[derive(Clone)]
517
pub enum MarginBoxContent {
518
    /// Empty margin box
519
    None,
520
    /// A running element referenced by name: `content: element(header)`
521
    RunningElement(String),
522
    /// A named string: `content: string(chapter)`
523
    NamedString(String),
524
    /// Page counter: `content: counter(page)`
525
    PageCounter,
526
    /// Total pages counter: `content: counter(pages)`
527
    PagesCounter,
528
    /// Page counter with format: `content: counter(page, lower-roman)`
529
    PageCounterFormatted { format: CounterFormat },
530
    /// Combined content (e.g., "Page " counter(page) " of " counter(pages))
531
    Combined(Vec<MarginBoxContent>),
532
    /// Literal text
533
    Text(String),
534
    /// Custom callback for dynamic content generation
535
    Custom(Arc<dyn Fn(PageInfo) -> String + Send + Sync>),
536
}
537

            
538
impl std::fmt::Debug for MarginBoxContent {
539
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
540
        match self {
541
            Self::None => write!(f, "None"),
542
            Self::RunningElement(s) => f.debug_tuple("RunningElement").field(s).finish(),
543
            Self::NamedString(s) => f.debug_tuple("NamedString").field(s).finish(),
544
            Self::PageCounter => write!(f, "PageCounter"),
545
            Self::PagesCounter => write!(f, "PagesCounter"),
546
            Self::PageCounterFormatted { format } => f
547
                .debug_struct("PageCounterFormatted")
548
                .field("format", format)
549
                .finish(),
550
            Self::Combined(v) => f.debug_tuple("Combined").field(v).finish(),
551
            Self::Text(s) => f.debug_tuple("Text").field(s).finish(),
552
            Self::Custom(_) => write!(f, "Custom(<fn>)"),
553
        }
554
    }
555
}
556

            
557
/// Counter formatting styles (subset of CSS list-style-type).
558
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
559
pub enum CounterFormat {
560
    Decimal,
561
    DecimalLeadingZero,
562
    LowerRoman,
563
    UpperRoman,
564
    LowerAlpha,
565
    UpperAlpha,
566
    LowerGreek,
567
}
568

            
569
impl Default for CounterFormat {
570
    fn default() -> Self {
571
        Self::Decimal
572
    }
573
}
574

            
575
impl CounterFormat {
576
    /// Format a number according to this counter style.
577
    pub fn format(&self, n: usize) -> String {
578
        match self {
579
            Self::Decimal => n.to_string(),
580
            Self::DecimalLeadingZero => format!("{:02}", n),
581
            Self::LowerRoman => to_roman(n, false),
582
            Self::UpperRoman => to_roman(n, true),
583
            Self::LowerAlpha => to_alpha(n, false),
584
            Self::UpperAlpha => to_alpha(n, true),
585
            Self::LowerGreek => to_greek(n),
586
        }
587
    }
588
}
589

            
590
/// Convert number to roman numerals.
591
fn to_roman(mut n: usize, uppercase: bool) -> String {
592
    if n == 0 {
593
        return "0".to_string();
594
    }
595

            
596
    let numerals = [
597
        (1000, "m"),
598
        (900, "cm"),
599
        (500, "d"),
600
        (400, "cd"),
601
        (100, "c"),
602
        (90, "xc"),
603
        (50, "l"),
604
        (40, "xl"),
605
        (10, "x"),
606
        (9, "ix"),
607
        (5, "v"),
608
        (4, "iv"),
609
        (1, "i"),
610
    ];
611

            
612
    let mut result = String::new();
613
    for (value, numeral) in &numerals {
614
        while n >= *value {
615
            result.push_str(numeral);
616
            n -= value;
617
        }
618
    }
619

            
620
    if uppercase {
621
        result.to_uppercase()
622
    } else {
623
        result
624
    }
625
}
626

            
627
/// Convert number to alphabetic (a-z, aa-az, etc.).
628
fn to_alpha(n: usize, uppercase: bool) -> String {
629
    if n == 0 {
630
        return "0".to_string();
631
    }
632

            
633
    let mut result = String::new();
634
    let mut remaining = n;
635

            
636
    while remaining > 0 {
637
        remaining -= 1;
638
        let c = ((remaining % 26) as u8 + if uppercase { b'A' } else { b'a' }) as char;
639
        result.insert(0, c);
640
        remaining /= 26;
641
    }
642

            
643
    result
644
}
645

            
646
/// Convert number to Greek letters (α, β, γ, ...).
647
fn to_greek(n: usize) -> String {
648
    const GREEK: &[char] = &[
649
        'α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η', 'θ', 'ι', 'κ', 'λ', 'μ', 'ν', 'ξ', 'ο', 'π', 'ρ', 'σ',
650
        'τ', 'υ', 'φ', 'χ', 'ψ', 'ω',
651
    ];
652
    if n == 0 {
653
        return "0".to_string();
654
    }
655
    if n <= GREEK.len() {
656
        return GREEK[n - 1].to_string();
657
    }
658

            
659
    // For numbers > 24, use αα, αβ, etc.
660
    let mut result = String::new();
661
    let mut remaining = n;
662
    while remaining > 0 {
663
        remaining -= 1;
664
        result.insert(0, GREEK[remaining % GREEK.len()]);
665
        remaining /= GREEK.len();
666
    }
667
    result
668
}
669

            
670
/// Information about the current page, passed to content generators.
671
#[derive(Debug, Clone, Copy)]
672
pub struct PageInfo {
673
    /// Current page number (1-indexed for display)
674
    pub page_number: usize,
675
    /// Total number of pages (may be 0 if unknown during first pass)
676
    pub total_pages: usize,
677
    /// Whether this is the first page
678
    pub is_first: bool,
679
    /// Whether this is the last page
680
    pub is_last: bool,
681
    /// Whether this is a left (verso) page (for duplex printing)
682
    pub is_left: bool,
683
    /// Whether this is a right (recto) page
684
    pub is_right: bool,
685
    /// Whether this is a blank page (inserted for left/right alignment)
686
    pub is_blank: bool,
687
}
688

            
689
impl PageInfo {
690
    /// Create PageInfo for a specific page.
691
1925
    pub fn new(page_number: usize, total_pages: usize) -> Self {
692
        Self {
693
1925
            page_number,
694
1925
            total_pages,
695
1925
            is_first: page_number == 1,
696
1925
            is_last: total_pages > 0 && page_number == total_pages,
697
1925
            is_left: page_number % 2 == 0, // Even pages are left (verso)
698
1925
            is_right: page_number % 2 == 1, // Odd pages are right (recto)
699
            is_blank: false,
700
        }
701
1925
    }
702
}
703

            
704
/// Default height for page headers and footers (in points).
705
const DEFAULT_HEADER_FOOTER_HEIGHT: f32 = 30.0;
706

            
707
/// Default font size for header/footer text (in points).
708
const DEFAULT_HEADER_FOOTER_FONT_SIZE: f32 = 10.0;
709

            
710
/// Configuration for page headers and footers.
711
///
712
/// This is a simplified interface for the common case of adding
713
/// headers and footers. For full GCPM support, use `PageTemplate`.
714
#[derive(Debug, Clone)]
715
pub struct HeaderFooterConfig {
716
    /// Whether to show a header on each page
717
    pub show_header: bool,
718
    /// Whether to show a footer on each page
719
    pub show_footer: bool,
720
    /// Height of the header area (if shown)
721
    pub header_height: f32,
722
    /// Height of the footer area (if shown)  
723
    pub footer_height: f32,
724
    /// Content generator for the header
725
    pub header_content: MarginBoxContent,
726
    /// Content generator for the footer
727
    pub footer_content: MarginBoxContent,
728
    /// Font size for header/footer text
729
    pub font_size: f32,
730
    /// Text color for header/footer
731
    pub text_color: ColorU,
732
    /// Whether to skip header/footer on first page
733
    pub skip_first_page: bool,
734
}
735

            
736
impl Default for HeaderFooterConfig {
737
    fn default() -> Self {
738
        Self {
739
            show_header: false,
740
            show_footer: false,
741
            header_height: DEFAULT_HEADER_FOOTER_HEIGHT,
742
            footer_height: DEFAULT_HEADER_FOOTER_HEIGHT,
743
            header_content: MarginBoxContent::None,
744
            footer_content: MarginBoxContent::None,
745
            font_size: DEFAULT_HEADER_FOOTER_FONT_SIZE,
746
            text_color: ColorU {
747
                r: 0,
748
                g: 0,
749
                b: 0,
750
                a: 255,
751
            },
752
            skip_first_page: false,
753
        }
754
    }
755
}
756

            
757
impl HeaderFooterConfig {
758
    /// Create a config with page numbers in the footer.
759
    pub fn with_page_numbers() -> Self {
760
        Self {
761
            show_footer: true,
762
            footer_content: MarginBoxContent::Combined(vec![
763
                MarginBoxContent::Text("Page ".to_string()),
764
                MarginBoxContent::PageCounter,
765
                MarginBoxContent::Text(" of ".to_string()),
766
                MarginBoxContent::PagesCounter,
767
            ]),
768
            ..Default::default()
769
        }
770
    }
771

            
772
    /// Create a config with page numbers in both header and footer.
773
    pub fn with_header_and_footer_page_numbers() -> Self {
774
        Self {
775
            show_header: true,
776
            show_footer: true,
777
            header_content: MarginBoxContent::Combined(vec![
778
                MarginBoxContent::Text("Page ".to_string()),
779
                MarginBoxContent::PageCounter,
780
            ]),
781
            footer_content: MarginBoxContent::Combined(vec![
782
                MarginBoxContent::Text("Page ".to_string()),
783
                MarginBoxContent::PageCounter,
784
                MarginBoxContent::Text(" of ".to_string()),
785
                MarginBoxContent::PagesCounter,
786
            ]),
787
            ..Default::default()
788
        }
789
    }
790

            
791
    /// Set custom header text.
792
    pub fn with_header_text(mut self, text: impl Into<String>) -> Self {
793
        self.show_header = true;
794
        self.header_content = MarginBoxContent::Text(text.into());
795
        self
796
    }
797

            
798
    /// Set custom footer text.
799
    pub fn with_footer_text(mut self, text: impl Into<String>) -> Self {
800
        self.show_footer = true;
801
        self.footer_content = MarginBoxContent::Text(text.into());
802
        self
803
    }
804

            
805
    /// Generate the text content for a margin box given page info.
806
    pub fn generate_content(&self, content: &MarginBoxContent, info: PageInfo) -> String {
807
        match content {
808
            MarginBoxContent::None => String::new(),
809
            MarginBoxContent::Text(s) => s.clone(),
810
            MarginBoxContent::PageCounter => info.page_number.to_string(),
811
            MarginBoxContent::PagesCounter => {
812
                if info.total_pages > 0 {
813
                    info.total_pages.to_string()
814
                } else {
815
                    "?".to_string()
816
                }
817
            }
818
            MarginBoxContent::PageCounterFormatted { format } => format.format(info.page_number),
819
            MarginBoxContent::Combined(parts) => parts
820
                .iter()
821
                .map(|p| self.generate_content(p, info))
822
                .collect(),
823
            MarginBoxContent::NamedString(name) => {
824
                // TODO: Look up named string from document context
825
                format!("[string:{}]", name)
826
            }
827
            MarginBoxContent::RunningElement(name) => {
828
                // Running elements are rendered as display items, not text
829
                format!("[element:{}]", name)
830
            }
831
            MarginBoxContent::Custom(f) => f(info),
832
        }
833
    }
834

            
835
    /// Get the header text for a specific page.
836
    pub fn header_text(&self, info: PageInfo) -> String {
837
        if !self.show_header {
838
            return String::new();
839
        }
840
        if self.skip_first_page && info.is_first {
841
            return String::new();
842
        }
843
        self.generate_content(&self.header_content, info)
844
    }
845

            
846
    /// Get the footer text for a specific page.
847
    pub fn footer_text(&self, info: PageInfo) -> String {
848
        if !self.show_footer {
849
            return String::new();
850
        }
851
        if self.skip_first_page && info.is_first {
852
            return String::new();
853
        }
854
        self.generate_content(&self.footer_content, info)
855
    }
856
}
857

            
858
/// Full page template with all 16 margin boxes (CSS GCPM @page support).
859
///
860
/// This provides complete control over page layout following the CSS
861
/// Paged Media and GCPM specifications.
862
#[derive(Debug, Clone, Default)]
863
pub struct PageTemplate {
864
    /// Content for each margin box position
865
    pub margin_boxes: BTreeMap<MarginBoxPosition, MarginBoxContent>,
866
    /// Page margins (space allocated for margin boxes)
867
    pub margins: PageMargins,
868
    /// Named strings captured from the document
869
    pub named_strings: BTreeMap<String, String>,
870
    /// Running elements available for this page
871
    pub running_elements: BTreeMap<String, RunningElement>,
872
}
873

            
874
impl PageTemplate {
875
    /// Create a new empty page template.
876
    pub fn new() -> Self {
877
        Self::default()
878
    }
879

            
880
    /// Set content for a specific margin box.
881
    pub fn set_margin_box(&mut self, position: MarginBoxPosition, content: MarginBoxContent) {
882
        self.margin_boxes.insert(position, content);
883
    }
884

            
885
    /// Create a simple template with centered page numbers in the footer.
886
    pub fn with_centered_page_numbers() -> Self {
887
        let mut template = Self::new();
888
        template.set_margin_box(
889
            MarginBoxPosition::BottomCenter,
890
            MarginBoxContent::PageCounter,
891
        );
892
        template
893
    }
894

            
895
    /// Create a template with "Page X of Y" in the bottom right.
896
    pub fn with_page_x_of_y() -> Self {
897
        let mut template = Self::new();
898
        template.set_margin_box(
899
            MarginBoxPosition::BottomRight,
900
            MarginBoxContent::Combined(vec![
901
                MarginBoxContent::Text("Page ".to_string()),
902
                MarginBoxContent::PageCounter,
903
                MarginBoxContent::Text(" of ".to_string()),
904
                MarginBoxContent::PagesCounter,
905
            ]),
906
        );
907
        template
908
    }
909
}
910

            
911
/// Temporary configuration for page headers/footers without CSS `@page` parsing.
912
///
913
/// Provides programmatic control over page decoration until full CSS `@page`
914
/// rule support is implemented.
915
///
916
/// ## Supported Features
917
///
918
/// - Page numbers in header and/or footer
919
/// - Custom text in header and/or footer
920
/// - Number format (decimal, roman numerals, alphabetic, greek)
921
/// - Skip first page option
922
///
923
/// ## Example
924
///
925
/// ```rust
926
/// use azul_layout::solver3::pagination::FakePageConfig;
927
///
928
/// let config = FakePageConfig::new()
929
///     .with_footer_page_numbers()
930
///     .with_header_text("My Document")
931
///     .skip_first_page(true);
932
///
933
/// let header_footer = config.to_header_footer_config();
934
/// ```
935
#[derive(Debug, Clone)]
936
pub struct FakePageConfig {
937
    /// Show header on pages
938
    pub show_header: bool,
939
    /// Show footer on pages
940
    pub show_footer: bool,
941
    /// Header text (static text, or None for page numbers only)
942
    pub header_text: Option<String>,
943
    /// Footer text (static text, or None for page numbers only)
944
    pub footer_text: Option<String>,
945
    /// Include page number in header
946
    pub header_page_number: bool,
947
    /// Include page number in footer
948
    pub footer_page_number: bool,
949
    /// Include total pages count ("of Y") in header
950
    pub header_total_pages: bool,
951
    /// Include total pages count ("of Y") in footer
952
    pub footer_total_pages: bool,
953
    /// Number format for page counters
954
    pub number_format: CounterFormat,
955
    /// Skip header/footer on first page
956
    pub skip_first_page: bool,
957
    /// Header height in points
958
    pub header_height: f32,
959
    /// Footer height in points
960
    pub footer_height: f32,
961
    /// Font size for header/footer text
962
    pub font_size: f32,
963
    /// Text color for header/footer
964
    pub text_color: ColorU,
965
}
966

            
967
impl Default for FakePageConfig {
968
275
    fn default() -> Self {
969
275
        Self {
970
275
            show_header: false,
971
275
            show_footer: false,
972
275
            header_text: None,
973
275
            footer_text: None,
974
275
            header_page_number: false,
975
275
            footer_page_number: false,
976
275
            header_total_pages: false,
977
275
            footer_total_pages: false,
978
275
            number_format: CounterFormat::Decimal,
979
275
            skip_first_page: false,
980
275
            header_height: DEFAULT_HEADER_FOOTER_HEIGHT,
981
275
            footer_height: DEFAULT_HEADER_FOOTER_HEIGHT,
982
275
            font_size: DEFAULT_HEADER_FOOTER_FONT_SIZE,
983
275
            text_color: ColorU {
984
275
                r: 0,
985
275
                g: 0,
986
275
                b: 0,
987
275
                a: 255,
988
275
            },
989
275
        }
990
275
    }
991
}
992

            
993
impl FakePageConfig {
994
    /// Create a new empty configuration (no headers/footers).
995
275
    pub fn new() -> Self {
996
275
        Self::default()
997
275
    }
998

            
999
    /// Enable footer with "Page X of Y" format.
    pub fn with_footer_page_numbers(mut self) -> Self {
        self.show_footer = true;
        self.footer_page_number = true;
        self.footer_total_pages = true;
        self
    }
    /// Enable header with "Page X" format.
    pub fn with_header_page_numbers(mut self) -> Self {
        self.show_header = true;
        self.header_page_number = true;
        self
    }
    /// Enable both header and footer with page numbers.
    pub fn with_header_and_footer_page_numbers(mut self) -> Self {
        self.show_header = true;
        self.show_footer = true;
        self.header_page_number = true;
        self.footer_page_number = true;
        self.footer_total_pages = true;
        self
    }
    /// Set custom header text.
    pub fn with_header_text(mut self, text: impl Into<String>) -> Self {
        self.show_header = true;
        self.header_text = Some(text.into());
        self
    }
    /// Set custom footer text.
    pub fn with_footer_text(mut self, text: impl Into<String>) -> Self {
        self.show_footer = true;
        self.footer_text = Some(text.into());
        self
    }
    /// Set the number format for page counters.
    pub fn with_number_format(mut self, format: CounterFormat) -> Self {
        self.number_format = format;
        self
    }
    /// Skip header/footer on the first page.
    pub fn skip_first_page(mut self, skip: bool) -> Self {
        self.skip_first_page = skip;
        self
    }
    /// Set header height.
    pub fn with_header_height(mut self, height: f32) -> Self {
        self.header_height = height;
        self
    }
    /// Set footer height.
    pub fn with_footer_height(mut self, height: f32) -> Self {
        self.footer_height = height;
        self
    }
    /// Set font size for header/footer text.
    pub fn with_font_size(mut self, size: f32) -> Self {
        self.font_size = size;
        self
    }
    /// Set text color for header/footer.
    pub fn with_text_color(mut self, color: ColorU) -> Self {
        self.text_color = color;
        self
    }
    /// Convert this fake config to the internal HeaderFooterConfig.
    ///
    /// This is the bridge between the user-facing API and the internal
    /// pagination engine.
1925
    pub fn to_header_footer_config(&self) -> HeaderFooterConfig {
1925
        HeaderFooterConfig {
1925
            show_header: self.show_header,
1925
            show_footer: self.show_footer,
1925
            header_height: self.header_height,
1925
            footer_height: self.footer_height,
1925
            header_content: self.build_header_content(),
1925
            footer_content: self.build_footer_content(),
1925
            skip_first_page: self.skip_first_page,
1925
            font_size: self.font_size,
1925
            text_color: self.text_color,
1925
        }
1925
    }
    /// Build the MarginBoxContent for the header.
1925
    fn build_header_content(&self) -> MarginBoxContent {
1925
        Self::build_margin_content(
1925
            &self.header_text,
1925
            self.header_page_number,
1925
            self.header_total_pages,
1925
            self.number_format,
        )
1925
    }
    /// Build the MarginBoxContent for the footer.
1925
    fn build_footer_content(&self) -> MarginBoxContent {
1925
        Self::build_margin_content(
1925
            &self.footer_text,
1925
            self.footer_page_number,
1925
            self.footer_total_pages,
1925
            self.number_format,
        )
1925
    }
    /// Shared helper for building header/footer margin box content.
3850
    fn build_margin_content(
3850
        text: &Option<String>,
3850
        page_number: bool,
3850
        total_pages: bool,
3850
        number_format: CounterFormat,
3850
    ) -> MarginBoxContent {
3850
        let mut parts = Vec::new();
3850
        if let Some(ref text) = text {
            parts.push(MarginBoxContent::Text(text.clone()));
            if page_number {
                parts.push(MarginBoxContent::Text(" - ".to_string()));
            }
3850
        }
3850
        if page_number {
            if number_format == CounterFormat::Decimal {
                parts.push(MarginBoxContent::Text("Page ".to_string()));
                parts.push(MarginBoxContent::PageCounter);
            } else {
                parts.push(MarginBoxContent::Text("Page ".to_string()));
                parts.push(MarginBoxContent::PageCounterFormatted {
                    format: number_format,
                });
            }
            if total_pages {
                parts.push(MarginBoxContent::Text(" of ".to_string()));
                parts.push(MarginBoxContent::PagesCounter);
            }
3850
        }
3850
        if parts.is_empty() {
3850
            MarginBoxContent::None
        } else if parts.len() == 1 {
            parts.pop().unwrap()
        } else {
            MarginBoxContent::Combined(parts)
        }
3850
    }
}
/// Information about a table that may need header repetition.
#[derive(Debug, Clone)]
pub struct TableHeaderInfo {
    /// The table's node index in the layout tree
    pub table_node_index: usize,
    /// The Y position where the table starts
    pub table_start_y: f32,
    /// The Y position where the table ends
    pub table_end_y: f32,
    /// The thead's display list items (captured during initial render)
    pub thead_items: Vec<super::display_list::DisplayListItem>,
    /// Height of the thead
    pub thead_height: f32,
    /// The Y position of the thead relative to table start
    pub thead_offset_y: f32,
}
/// Context for tracking table headers across pages.
#[derive(Debug, Default, Clone)]
pub struct TableHeaderTracker {
    /// All tables with theads that might need repetition
    pub tables: Vec<TableHeaderInfo>,
}
impl TableHeaderTracker {
    pub fn new() -> Self {
        Self::default()
    }
    /// Register a table's thead for potential repetition.
    pub fn register_table_header(&mut self, info: TableHeaderInfo) {
        self.tables.push(info);
    }
    /// Get theads that should be repeated on a specific page.
    ///
    /// Returns the thead items that need to be injected at the top of the page,
    /// along with the Y offset where they should appear.
1925
    pub fn get_repeated_headers_for_page(
1925
        &self,
1925
        page_index: usize,
1925
        page_top_y: f32,
1925
        page_bottom_y: f32,
1925
    ) -> Vec<(f32, &[super::display_list::DisplayListItem], f32)> {
1925
        let mut headers = Vec::new();
1925
        for table in &self.tables {
            // Check if this table spans into this page (but didn't start on this page)
            let table_starts_before_page = table.table_start_y < page_top_y;
            let table_continues_on_page = table.table_end_y > page_top_y;
            if table_starts_before_page && table_continues_on_page {
                // This table needs its header repeated on this page
                // The header should appear at the top of the page content area
                headers.push((
                    0.0, // Y offset from page top (header goes at very top)
                    table.thead_items.as_slice(),
                    table.thead_height,
                ));
            }
        }
1925
        headers
1925
    }
}