1
//! Paged media layout engine.
2
//!
3
//! This module provides infrastructure for multi-page document
4
//! layout with CSS Paged Media support.
5
//!
6
//! The core concept is a **FragmentationContext**, which represents
7
//! a series of containers (fragmentainers) that content flows into
8
//! during layout. For continuous media (screens), we use a single
9
//! infinite container. For paged media (print), we use a series of
10
//! page-sized containers.
11
//!
12
//! This approach allows the layout engine to make break decisions
13
//! during layout, respecting CSS properties like `break-before`,
14
//! `break-after`, and `break-inside`.
15

            
16
use azul_core::geom::LogicalSize;
17

            
18
#[cfg(all(feature = "text_layout", feature = "font_loading"))]
19
use crate::solver3::display_list::DisplayList;
20

            
21
// Stub type when text_layout or font_loading is disabled
22
#[cfg(not(all(feature = "text_layout", feature = "font_loading")))]
23
#[derive(Debug, Clone, Default)]
24
pub struct DisplayList;
25

            
26
/// Represents a series of containers that content flows into during layout.
27
///
28
/// This is the core abstraction for fragmentation support. Different media types
29
/// use different fragmentation contexts:
30
/// - Screen rendering: Continuous (single infinite container)
31
/// - Print rendering: Paged (series of fixed-size page containers)
32
/// - Multi-column layout: MultiColumn (series of column containers)
33
#[derive(Debug, Clone)]
34
pub enum FragmentationContext {
35
    /// Continuous media (screen): single infinite container.
36
    ///
37
    /// Used for normal screen rendering where content can scroll indefinitely.
38
    /// The container grows as needed and never forces breaks.
39
    Continuous {
40
        /// Width of the viewport
41
        width: f32,
42
        /// The single fragmentainer (grows infinitely)
43
        container: Fragmentainer,
44
    },
45

            
46
    /// Paged media (print): series of page boxes.
47
    ///
48
    /// Used for PDF generation and print preview. Content flows from one
49
    /// page to the next when a page is full.
50
    Paged {
51
        /// Size of each page
52
        page_size: LogicalSize,
53
        /// All pages (fragmentainers) that have been created
54
        pages: Vec<Fragmentainer>,
55
    },
56

            
57
    /// Multi-column layout: series of column boxes.
58
    ///
59
    /// Future support for CSS multi-column layout.
60
    #[allow(dead_code)]
61
    MultiColumn {
62
        /// Width of each column
63
        column_width: f32,
64
        /// Height of each column
65
        column_height: f32,
66
        /// Gap between columns
67
        gap: f32,
68
        /// All columns that have been created
69
        columns: Vec<Fragmentainer>,
70
    },
71

            
72
    /// CSS Regions: series of region boxes.
73
    ///
74
    /// Future support for CSS Regions specification.
75
    #[allow(dead_code)]
76
    Regions {
77
        /// Pre-defined region boxes
78
        regions: Vec<Fragmentainer>,
79
    },
80
}
81

            
82
/// A single container (fragmentainer) in a fragmentation context.
83
///
84
/// Each fragmentainer has a logical size and tracks how much of that space
85
/// has been used. For continuous media, the fragmentainer can grow infinitely.
86
/// For paged media, fragmentainers have fixed sizes.
87
#[derive(Debug, Clone)]
88
pub struct Fragmentainer {
89
    /// Logical size of this container (width and height)
90
    pub size: LogicalSize,
91

            
92
    /// How much block-axis space has been used (typically vertical space)
93
    pub used_block_size: f32,
94

            
95
    /// Whether this container has a fixed size (true for pages) or can
96
    /// grow (false for continuous)
97
    pub is_fixed_size: bool,
98

            
99
    /// Content that has been placed in this fragmentainer (populated during layout).
100
    pub content: Vec<LayoutBox>,
101
}
102

            
103
/// Layout box content placed within a fragmentainer.
104
#[derive(Debug, Clone)]
105
pub struct LayoutBox {
106
    // Fields to be defined when fragmentation content tracking is implemented
107
}
108

            
109
impl Fragmentainer {
110
    /// Create a new fragmentainer with the given size.
111
1925
    pub fn new(size: LogicalSize, is_fixed_size: bool) -> Self {
112
1925
        Self {
113
1925
            size,
114
1925
            used_block_size: 0.0,
115
1925
            is_fixed_size,
116
1925
            content: Vec::new(),
117
1925
        }
118
1925
    }
119

            
120
    /// Get the remaining block-axis space (infinite for continuous, bounded for paged).
121
    pub fn remaining_space(&self) -> f32 {
122
        if self.is_fixed_size {
123
            (self.size.height - self.used_block_size).max(0.0)
124
        } else {
125
            f32::MAX // Infinite for continuous media
126
        }
127
    }
128

            
129
    /// Check if this fragmentainer is full (less than 1px remaining).
130
    pub fn is_full(&self) -> bool {
131
        self.is_fixed_size && self.remaining_space() < 1.0
132
    }
133

            
134
    /// Check if a block of the given size can fit in this fragmentainer.
135
    pub fn can_fit(&self, block_size: f32) -> bool {
136
        self.remaining_space() >= block_size
137
    }
138

            
139
    /// Record that block-axis space has been used in this fragmentainer.
140
    pub fn use_space(&mut self, size: f32) {
141
        self.used_block_size += size;
142
    }
143
}
144

            
145
impl FragmentationContext {
146
    /// Create a continuous fragmentation context for screen rendering.
147
    pub fn new_continuous(width: f32) -> Self {
148
        Self::Continuous {
149
            width,
150
            container: Fragmentainer::new(
151
                LogicalSize::new(width, f32::MAX),
152
                false, // Not fixed size
153
            ),
154
        }
155
    }
156

            
157
    /// Create a paged fragmentation context for print rendering.
158
1925
    pub fn new_paged(page_size: LogicalSize) -> Self {
159
1925
        Self::Paged {
160
1925
            page_size,
161
1925
            pages: vec![Fragmentainer::new(page_size, true)],
162
1925
        }
163
1925
    }
164

            
165
    /// Get the number of fragmentainers (pages, columns, etc.) in this context.
166
    pub fn fragmentainer_count(&self) -> usize {
167
        match self {
168
            Self::Continuous { .. } => 1,
169
            Self::Paged { pages, .. } => pages.len(),
170
            Self::MultiColumn { columns, .. } => columns.len(),
171
            Self::Regions { regions } => regions.len(),
172
        }
173
    }
174

            
175
    /// Get a reference to the current fragmentainer being filled.
176
    pub fn current(&self) -> &Fragmentainer {
177
        match self {
178
            Self::Continuous { container, .. } => container,
179
            Self::Paged { pages, .. } => pages
180
                .last()
181
                .expect("Paged context must have at least one page"),
182
            Self::MultiColumn { columns, .. } => columns
183
                .last()
184
                .expect("MultiColumn context must have at least one column"),
185
            Self::Regions { regions } => regions
186
                .last()
187
                .expect("Regions context must have at least one region"),
188
        }
189
    }
190

            
191
    /// Get a mutable reference to the current fragmentainer being filled.
192
    pub fn current_mut(&mut self) -> &mut Fragmentainer {
193
        match self {
194
            Self::Continuous { container, .. } => container,
195
            Self::Paged { pages, .. } => pages
196
                .last_mut()
197
                .expect("Paged context must have at least one page"),
198
            Self::MultiColumn { columns, .. } => columns
199
                .last_mut()
200
                .expect("MultiColumn context must have at least one column"),
201
            Self::Regions { regions } => regions
202
                .last_mut()
203
                .expect("Regions context must have at least one region"),
204
        }
205
    }
206

            
207
    /// Advance to the next fragmentainer, creating a new one if necessary.
208
    ///
209
    /// - For continuous media, this is a no-op (continuous media can't advance).
210
    /// - For paged media, this creates a new page.
211
    /// - For regions, this fails if no more regions are available.
212
    ///
213
    /// # Returns
214
    ///
215
    /// - `Ok(())` if the advance succeeded, `Err(String)` if it failed (e.g., no more regions).
216
    pub fn advance(&mut self) -> Result<(), String> {
217
        match self {
218
            Self::Continuous { .. } => {
219
                // Continuous media doesn't advance, it just grows
220
                Ok(())
221
            }
222
            Self::Paged { page_size, pages } => {
223
                // Create a new page
224
                pages.push(Fragmentainer::new(*page_size, true));
225
                Ok(())
226
            }
227
            Self::MultiColumn {
228
                column_width,
229
                column_height,
230
                columns,
231
                ..
232
            } => {
233
                // Create a new column
234
                columns.push(Fragmentainer::new(
235
                    LogicalSize::new(*column_width, *column_height),
236
                    true,
237
                ));
238
                Ok(())
239
            }
240
            Self::Regions { .. } => {
241
                // Regions are pre-defined, can't create more
242
                Err("No more regions available for content overflow".to_string())
243
            }
244
        }
245
    }
246

            
247
    /// Get all fragmentainers in this context.
248
    pub fn fragmentainers(&self) -> Vec<&Fragmentainer> {
249
        match self {
250
            Self::Continuous { container, .. } => vec![container],
251
            Self::Paged { pages, .. } => pages.iter().collect(),
252
            Self::MultiColumn { columns, .. } => columns.iter().collect(),
253
            Self::Regions { regions } => regions.iter().collect(),
254
        }
255
    }
256

            
257
    /// Get the page size for paged media, or None for other contexts.
258
    pub fn page_size(&self) -> Option<LogicalSize> {
259
        match self {
260
            Self::Paged { page_size, .. } => Some(*page_size),
261
            _ => None,
262
        }
263
    }
264

            
265
    /// Get the page content height (page height minus margins).
266
    /// For continuous media, returns f32::MAX.
267
275
    pub fn page_content_height(&self) -> f32 {
268
275
        match self {
269
            Self::Continuous { .. } => f32::MAX,
270
275
            Self::Paged { page_size, .. } => page_size.height,
271
            Self::MultiColumn { column_height, .. } => *column_height,
272
            Self::Regions { regions } => regions.first().map(|r| r.size.height).unwrap_or(f32::MAX),
273
        }
274
275
    }
275

            
276
    /// Check if this is paged media.
277
275
    pub fn is_paged(&self) -> bool {
278
275
        matches!(self, Self::Paged { .. })
279
275
    }
280
}
281

            
282
// Fragmentation State - Tracked During Layout
283

            
284
/// State tracked during layout for fragmentation.
285
/// This is created at the start of paged layout and updated as nodes are laid out.
286
#[derive(Debug, Clone)]
287
pub struct FragmentationState {
288
    /// Current page being laid out (0-indexed)
289
    pub current_page: usize,
290
    /// Y position on current page (relative to page content area)
291
    pub current_page_y: f32,
292
    /// Available height remaining on current page
293
    pub available_height: f32,
294
    /// Full page content height
295
    pub page_content_height: f32,
296
    /// Page margins (not yet used, but needed for future)
297
    pub margins_top: f32,
298
    pub margins_bottom: f32,
299
    /// Total number of pages so far
300
    pub total_pages: usize,
301
}
302

            
303
impl FragmentationState {
304
    /// Create a new fragmentation state for paged layout.
305
    pub fn new(page_content_height: f32, margins_top: f32, margins_bottom: f32) -> Self {
306
        Self {
307
            current_page: 0,
308
            current_page_y: 0.0,
309
            available_height: page_content_height,
310
            page_content_height,
311
            margins_top,
312
            margins_bottom,
313
            total_pages: 1,
314
        }
315
    }
316

            
317
    /// Check if content of the given height can fit on the current page.
318
    pub fn can_fit(&self, height: f32) -> bool {
319
        self.available_height >= height
320
    }
321

            
322
    /// Check if content would fit on an empty page.
323
    pub fn would_fit_on_empty_page(&self, height: f32) -> bool {
324
        height <= self.page_content_height
325
    }
326

            
327
    /// Use space on the current page.
328
    pub fn use_space(&mut self, height: f32) {
329
        self.current_page_y += height;
330
        self.available_height = (self.page_content_height - self.current_page_y).max(0.0);
331
    }
332

            
333
    /// Advance to the next page.
334
    pub fn advance_page(&mut self) {
335
        self.current_page += 1;
336
        self.current_page_y = 0.0;
337
        self.available_height = self.page_content_height;
338
        self.total_pages = self.total_pages.max(self.current_page + 1);
339
    }
340

            
341
    /// Calculate which page a Y position belongs to.
342
    pub fn page_for_y(&self, y: f32) -> usize {
343
        if self.page_content_height <= 0.0 {
344
            return 0;
345
        }
346
        (y / self.page_content_height).floor() as usize
347
    }
348

            
349
    /// Calculate the Y offset for a given page (to convert to page-relative coordinates).
350
    pub fn page_y_offset(&self, page: usize) -> f32 {
351
        page as f32 * self.page_content_height
352
    }
353
}