azul_layout/paged.rs
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
16use azul_core::geom::LogicalSize;
17
18#[cfg(all(feature = "text_layout", feature = "font_loading"))]
19use 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)]
24pub 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)]
34pub 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)]
88pub 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)]
105pub struct LayoutBox {
106 // Fields to be defined when fragmentation content tracking is implemented
107}
108
109impl Fragmentainer {
110 /// Create a new fragmentainer with the given size.
111 pub fn new(size: LogicalSize, is_fixed_size: bool) -> Self {
112 Self {
113 size,
114 used_block_size: 0.0,
115 is_fixed_size,
116 content: Vec::new(),
117 }
118 }
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
145impl 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 pub fn new_paged(page_size: LogicalSize) -> Self {
159 Self::Paged {
160 page_size,
161 pages: vec![Fragmentainer::new(page_size, true)],
162 }
163 }
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 pub fn page_content_height(&self) -> f32 {
268 match self {
269 Self::Continuous { .. } => f32::MAX,
270 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
276 /// Check if this is paged media.
277 pub fn is_paged(&self) -> bool {
278 matches!(self, Self::Paged { .. })
279 }
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)]
287pub 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
303impl 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}