Skip to main content

azul_layout/solver3/
paged_layout.rs

1//! CSS Paged Media layout integration with integrated fragmentation
2//!
3//! This module provides functionality for laying out documents with pagination,
4//! such as for PDF generation. It uses the new integrated architecture where:
5//!
6//! 1. page_index is assigned to nodes DURING layout based on Y position
7//! 2. generate_display_lists_paged() creates per-page DisplayLists by filtering
8//! 3. No post-hoc fragmentation is needed
9//!
10//! **Note**: Full CSS `@page` rule parsing is not yet implemented. The `FakePageConfig`
11//! provides programmatic control over page decoration as a temporary solution.
12
13use std::collections::BTreeMap;
14
15use azul_core::{
16    dom::{DomId, NodeId},
17    geom::{LogicalPosition, LogicalRect, LogicalSize},
18    hit_test::ScrollPosition,
19    resources::RendererResources,
20    selection::{SelectionState, TextSelection},
21    styled_dom::StyledDom,
22};
23use azul_css::LayoutDebugMessage;
24
25use crate::{
26    font_traits::{ParsedFontTrait, TextLayoutCache},
27    fragmentation::PageMargins,
28    paged::FragmentationContext,
29    solver3::{
30        cache::LayoutCache,
31        display_list::DisplayList,
32        getters::{get_break_after, get_break_before, get_break_inside},
33        pagination::FakePageConfig,
34        LayoutContext, LayoutError, Result,
35    },
36};
37
38/// Result of `compute_layout_with_fragmentation`: contains the data
39/// needed to generate a display list afterwards. The tree and
40/// calculated_positions are stored in the `LayoutCache` that was passed in.
41pub struct FragmentationLayoutResult {
42    pub scroll_ids: BTreeMap<usize, u64>,
43}
44
45/// Layout a document with integrated pagination, returning one DisplayList per page.
46///
47/// This function performs CSS Paged Media layout with fragmentation integrated
48/// into the layout process itself, using the new architecture where:
49///
50/// 1. The FragmentationContext is passed to layout_document via LayoutContext
51/// 2. Nodes get their page_index assigned during layout based on absolute Y position
52/// 3. DisplayLists are generated per-page by filtering items based on page bounds
53///
54/// Uses default page header/footer configuration (page numbers in footer).
55/// For custom headers/footers, use `layout_document_paged_with_config`.
56///
57/// # Arguments
58/// * `fragmentation_context` - Controls page size and fragmentation behavior
59/// * Other arguments same as `layout_document()`
60///
61/// # Returns
62/// A vector of DisplayLists, one per page. Each DisplayList contains the
63/// elements that fit on that page, with Y-coordinates relative to the page origin.
64#[cfg(feature = "text_layout")]
65pub fn layout_document_paged<T, F>(
66    cache: &mut LayoutCache,
67    text_cache: &mut TextLayoutCache,
68    fragmentation_context: FragmentationContext,
69    new_dom: &StyledDom,
70    viewport: LogicalRect,
71    font_manager: &mut crate::font_traits::FontManager<T>,
72    scroll_offsets: &BTreeMap<NodeId, ScrollPosition>,
73    selections: &BTreeMap<DomId, SelectionState>,
74    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
75    gpu_value_cache: Option<&azul_core::gpu::GpuValueCache>,
76    renderer_resources: &RendererResources,
77    id_namespace: azul_core::resources::IdNamespace,
78    dom_id: DomId,
79    font_loader: F,
80    get_system_time_fn: azul_core::task::GetSystemTimeCallback,
81) -> Result<Vec<DisplayList>>
82where
83    T: ParsedFontTrait + Sync + 'static,
84    F: Fn(&[u8], usize) -> std::result::Result<T, crate::text3::cache::LayoutError>,
85{
86    // Use default page config (page numbers in footer)
87    let page_config = FakePageConfig::new().with_footer_page_numbers();
88
89    layout_document_paged_with_config(
90        cache,
91        text_cache,
92        fragmentation_context,
93        new_dom,
94        viewport,
95        font_manager,
96        scroll_offsets,
97        selections,
98        debug_messages,
99        gpu_value_cache,
100        renderer_resources,
101        id_namespace,
102        dom_id,
103        font_loader,
104        page_config,
105        get_system_time_fn,
106    )
107}
108
109/// Layout a document with integrated pagination and custom page configuration.
110///
111/// This function is the same as `layout_document_paged` but allows you to
112/// specify custom headers and footers via `FakePageConfig`.
113///
114/// # Arguments
115/// * `page_config` - Configuration for page headers/footers (see `FakePageConfig`)
116/// * Other arguments same as `layout_document_paged()`
117#[cfg(feature = "text_layout")]
118pub fn layout_document_paged_with_config<T, F>(
119    cache: &mut LayoutCache,
120    text_cache: &mut TextLayoutCache,
121    mut fragmentation_context: FragmentationContext,
122    new_dom: &StyledDom,
123    viewport: LogicalRect,
124    font_manager: &mut crate::font_traits::FontManager<T>,
125    scroll_offsets: &BTreeMap<NodeId, ScrollPosition>,
126    selections: &BTreeMap<DomId, SelectionState>,
127    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
128    gpu_value_cache: Option<&azul_core::gpu::GpuValueCache>,
129    renderer_resources: &RendererResources,
130    id_namespace: azul_core::resources::IdNamespace,
131    dom_id: DomId,
132    font_loader: F,
133    page_config: FakePageConfig,
134    get_system_time_fn: azul_core::task::GetSystemTimeCallback,
135) -> Result<Vec<DisplayList>>
136where
137    T: ParsedFontTrait + Sync + 'static,
138    F: Fn(&[u8], usize) -> std::result::Result<T, crate::text3::cache::LayoutError>,
139{
140    // Font Resolution And Loading
141    {
142        use crate::solver3::getters::{
143            collect_and_resolve_font_chains, collect_font_ids_from_chains, compute_fonts_to_load,
144            load_fonts_from_disk, register_embedded_fonts_from_styled_dom,
145        };
146
147        // TODO: Accept platform as parameter instead of using ::current()
148        let platform = azul_css::system::Platform::current();
149
150        // Register embedded FontRefs (e.g. Material Icons) before resolving chains
151        register_embedded_fonts_from_styled_dom(new_dom, font_manager, &platform);
152
153        let chains = collect_and_resolve_font_chains(new_dom, &font_manager.fc_cache, &platform);
154
155        let required_fonts = collect_font_ids_from_chains(&chains);
156        let already_loaded = font_manager.get_loaded_font_ids();
157        let fonts_to_load = compute_fonts_to_load(&required_fonts, &already_loaded);
158
159
160        if !fonts_to_load.is_empty() {
161            let load_result =
162                load_fonts_from_disk(&fonts_to_load, &font_manager.fc_cache, &font_loader);
163
164            font_manager.insert_fonts(load_result.loaded);
165            for (font_id, error) in &load_result.failed {
166                if let Some(msgs) = debug_messages {
167                    msgs.push(LayoutDebugMessage::warning(format!(
168                        "[FontLoading] Failed to load font {:?}: {}",
169                        font_id, error
170                    )));
171                }
172            }
173        }
174        font_manager.set_font_chain_cache(chains.into_fontconfig_chains());
175    }
176
177
178    // Get page dimensions from fragmentation context
179    let page_content_height = fragmentation_context.page_content_height();
180
181    // Handle continuous media (no pagination)
182    if !fragmentation_context.is_paged() {
183        let _result = compute_layout_with_fragmentation(
184            cache,
185            text_cache,
186            &mut fragmentation_context,
187            new_dom,
188            viewport,
189            font_manager,
190            selections,
191            debug_messages,
192            get_system_time_fn,
193        )?;
194
195        // Generate display list from cached tree/positions
196        let tree = cache.tree.as_ref().ok_or(LayoutError::InvalidTree)?;
197        let mut counter_values = cache.counters.clone();
198        let empty_text_selections: BTreeMap<DomId, TextSelection> = BTreeMap::new();
199        let mut ctx = LayoutContext {
200            styled_dom: new_dom,
201            font_manager: &*font_manager,
202            selections,
203            text_selections: &empty_text_selections,
204            debug_messages,
205            counters: &mut counter_values,
206            viewport_size: viewport.size,
207            fragmentation_context: Some(&mut fragmentation_context),
208            cursor_is_visible: true,
209            cursor_location: None,
210            cache_map: std::mem::take(&mut cache.cache_map),
211            system_style: None,
212            get_system_time_fn,
213        };
214
215        use crate::solver3::display_list::generate_display_list;
216        let display_list = generate_display_list(
217            &mut ctx,
218            tree,
219            &cache.calculated_positions,
220            scroll_offsets,
221            &cache.scroll_ids,
222            gpu_value_cache,
223            renderer_resources,
224            id_namespace,
225            dom_id,
226        )?;
227        cache.cache_map = std::mem::take(&mut ctx.cache_map);
228        return Ok(vec![display_list]);
229    }
230
231    // Paged Layout
232
233    // Perform layout with fragmentation context (layout only, no display list)
234    let _result = compute_layout_with_fragmentation(
235        cache,
236        text_cache,
237        &mut fragmentation_context,
238        new_dom,
239        viewport,
240        font_manager,
241        selections,
242        debug_messages,
243        get_system_time_fn,
244    )?;
245
246
247    // Get the layout tree and positions
248    let tree = cache.tree.as_ref().ok_or(LayoutError::InvalidTree)?;
249    let calculated_positions = &cache.calculated_positions;
250
251    // Debug: log page layout info
252    if let Some(msgs) = debug_messages {
253        msgs.push(LayoutDebugMessage::info(format!(
254            "[PagedLayout] Page content height: {}",
255            page_content_height
256        )));
257    }
258
259    // Use scroll IDs computed by compute_layout_with_fragmentation (stored in cache)
260    let scroll_ids = &cache.scroll_ids;
261
262    // Create temporary context for display list generation
263    let mut counter_values = cache.counters.clone();
264    let empty_text_selections: BTreeMap<DomId, TextSelection> = BTreeMap::new();
265    let mut ctx = LayoutContext {
266        styled_dom: new_dom,
267        font_manager: &*font_manager,
268        selections,
269        text_selections: &empty_text_selections,
270        debug_messages,
271        counters: &mut counter_values,
272        viewport_size: viewport.size,
273        fragmentation_context: Some(&mut fragmentation_context),
274        cursor_is_visible: true, // Paged layout: cursor always visible
275        cursor_location: None,   // Paged layout: no cursor
276        cache_map: std::mem::take(&mut cache.cache_map),
277        system_style: None,
278        get_system_time_fn,
279    };
280
281    // NEW: Use the commitment-based pagination approach with CSS break properties
282    //
283    // This treats pages as viewports into a single infinite canvas:
284    // 1. Generate ONE complete display list on infinite vertical strip
285    // 2. Analyze CSS break properties (break-before, break-after, break-inside)
286    // 3. Calculate page boundaries based on break properties
287    // 4. Slice content to page boundaries (items are NEVER shifted, only clipped)
288    // 5. Headers and footers are injected per-page
289    //
290    // Benefits over the old approach:
291    // - No coordinate desynchronization between page_index and actual Y position
292    // - Backgrounds render correctly (clipped, not torn/duplicated)
293    // - Simple mental model: pages are just views into continuous content
294    // - Headers/footers with page numbers are automatically generated
295    // - CSS fragmentation properties are respected
296
297    use crate::solver3::display_list::{
298        generate_display_list, paginate_display_list_with_slicer_and_breaks,
299        SlicerConfig,
300    };
301
302    // Step 1: Generate ONE complete display list (infinite canvas)
303    let full_display_list = generate_display_list(
304        &mut ctx,
305        tree,
306        calculated_positions,
307        scroll_offsets,
308        scroll_ids,
309        gpu_value_cache,
310        renderer_resources,
311        id_namespace,
312        dom_id,
313    )?;
314
315
316    if let Some(msgs) = ctx.debug_messages {
317        msgs.push(LayoutDebugMessage::info(format!(
318            "[PagedLayout] Generated master display list with {} items",
319            full_display_list.items.len()
320        )));
321    }
322
323    // Step 2: Configure the slicer with page dimensions and headers/footers
324    let page_width = viewport.size.width;
325    let header_footer = page_config.to_header_footer_config();
326
327    if let Some(msgs) = ctx.debug_messages {
328        msgs.push(LayoutDebugMessage::info(format!(
329            "[PagedLayout] Page config: header={}, footer={}, skip_first={}",
330            header_footer.show_header, header_footer.show_footer, header_footer.skip_first_page
331        )));
332    }
333
334    let slicer_config = SlicerConfig {
335        page_content_height,
336        page_gap: 0.0,
337        allow_clipping: true,
338        header_footer,
339        page_width,
340        table_headers: Default::default(),
341    };
342
343    // Step 3: Paginate with CSS break property support
344    // Break properties (break-before, break-after) are now collected during display list
345    // generation and stored in DisplayList::forced_page_breaks
346    let pages = paginate_display_list_with_slicer_and_breaks(
347        full_display_list,
348        &slicer_config,
349    )?;
350
351
352    if let Some(msgs) = ctx.debug_messages {
353        msgs.push(LayoutDebugMessage::info(format!(
354            "[PagedLayout] Paginated into {} pages with CSS break support",
355            pages.len()
356        )));
357    }
358
359    cache.cache_map = std::mem::take(&mut ctx.cache_map);
360
361    Ok(pages)
362}
363
364/// Internal helper: Perform layout with a fragmentation context (layout only, no display list)
365///
366/// Returns a `FragmentationLayoutResult` containing the computed scroll IDs.
367/// The tree & positions are stored in `cache`. To generate a display list,
368/// call `generate_display_list` separately using the tree/positions from the cache.
369#[cfg(feature = "text_layout")]
370fn compute_layout_with_fragmentation<T: ParsedFontTrait + Sync + 'static>(
371    cache: &mut LayoutCache,
372    text_cache: &mut TextLayoutCache,
373    fragmentation_context: &mut FragmentationContext,
374    new_dom: &StyledDom,
375    viewport: LogicalRect,
376    font_manager: &crate::font_traits::FontManager<T>,
377    selections: &BTreeMap<DomId, SelectionState>,
378    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
379    get_system_time_fn: azul_core::task::GetSystemTimeCallback,
380) -> Result<FragmentationLayoutResult> {
381    use crate::solver3::{
382        cache, getters::get_writing_mode,
383        layout_tree::DirtyFlag,
384    };
385
386    // Create temporary context without counters for tree generation
387    let mut counter_values = BTreeMap::new();
388    let empty_text_selections: BTreeMap<DomId, TextSelection> = BTreeMap::new();
389    let mut ctx_temp = LayoutContext {
390        styled_dom: new_dom,
391        font_manager,
392        selections,
393        text_selections: &empty_text_selections,
394        debug_messages,
395        counters: &mut counter_values,
396        viewport_size: viewport.size,
397        fragmentation_context: Some(fragmentation_context),
398        cursor_is_visible: true, // Paged layout: cursor always visible
399        cursor_location: None,   // Paged layout: no cursor
400        cache_map: cache::LayoutCacheMap::default(),
401        system_style: None,
402        get_system_time_fn,
403    };
404
405    // --- Step 1: Tree Building & Invalidation ---
406    let is_fresh_dom = cache.tree.is_none();
407    let (mut new_tree, mut recon_result) = if is_fresh_dom {
408        // Fast path: no old tree to diff against — build tree directly.
409        // This avoids the per-node hash comparison in reconcile_and_invalidate().
410        use crate::solver3::layout_tree::generate_layout_tree;
411        let new_tree = generate_layout_tree(&mut ctx_temp)?;
412        let n = new_tree.nodes.len();
413        let mut result = cache::ReconciliationResult::default();
414        result.layout_roots.insert(new_tree.root);
415        result.intrinsic_dirty = (0..n).collect::<std::collections::BTreeSet<_>>();
416        (new_tree, result)
417    } else {
418        // Incremental path: diff old tree vs new DOM
419        cache::reconcile_and_invalidate(&mut ctx_temp, cache, viewport)?
420    };
421
422    // Step 1.2: Clear Taffy Caches for Dirty Nodes
423    for &node_idx in &recon_result.intrinsic_dirty {
424        if let Some(node) = new_tree.get_mut(node_idx) {
425            node.taffy_cache.clear();
426        }
427    }
428
429    // Step 1.3: Compute CSS Counters
430    cache::compute_counters(new_dom, &new_tree, &mut counter_values);
431
432    // Step 1.4: Resize and invalidate per-node cache (Taffy-inspired 9+1 slot cache)
433    // Move cache_map out of LayoutCache for the duration of layout.
434    let mut cache_map = std::mem::take(&mut cache.cache_map);
435    cache_map.resize_to_tree(new_tree.nodes.len());
436    for &node_idx in &recon_result.intrinsic_dirty {
437        cache_map.mark_dirty(node_idx, &new_tree.nodes);
438    }
439    for &node_idx in &recon_result.layout_roots {
440        cache_map.mark_dirty(node_idx, &new_tree.nodes);
441    }
442
443    // Now create the real context with computed counters and fragmentation
444    let mut ctx = LayoutContext {
445        styled_dom: new_dom,
446        font_manager,
447        selections,
448        text_selections: &empty_text_selections,
449        debug_messages,
450        counters: &mut counter_values,
451        viewport_size: viewport.size,
452        fragmentation_context: Some(fragmentation_context),
453        cursor_is_visible: true, // Paged layout: cursor always visible
454        cursor_location: None,   // Paged layout: no cursor
455        cache_map,
456        system_style: None,
457        get_system_time_fn,
458    };
459
460    // --- Step 1.5: Early Exit Optimization ---
461    if recon_result.is_clean() {
462        ctx.debug_log("No changes, layout cache is clean");
463        let tree = cache.tree.as_ref().ok_or(LayoutError::InvalidTree)?;
464
465        use crate::window::LayoutWindow;
466        let (scroll_ids, scroll_id_to_node_id) = LayoutWindow::compute_scroll_ids(tree, new_dom);
467        cache.scroll_ids = scroll_ids.clone();
468        cache.scroll_id_to_node_id = scroll_id_to_node_id;
469
470        return Ok(FragmentationLayoutResult {
471            scroll_ids,
472        });
473    }
474
475    // --- Step 2: Incremental Layout Loop ---
476    let mut calculated_positions = cache.calculated_positions.clone();
477    let mut loop_count = 0;
478    loop {
479        loop_count += 1;
480        if loop_count > 10 {
481            break;
482        }
483
484        calculated_positions = cache.calculated_positions.clone();
485        let mut reflow_needed_for_scrollbars = false;
486
487        crate::solver3::sizing::calculate_intrinsic_sizes(
488            &mut ctx,
489            &mut new_tree,
490            &recon_result.intrinsic_dirty,
491        )?;
492
493        for &root_idx in &recon_result.layout_roots {
494            let (cb_pos, cb_size) = get_containing_block_for_node(
495                &new_tree,
496                new_dom,
497                root_idx,
498                &calculated_positions,
499                viewport,
500            );
501
502            // For ROOT nodes (no parent), we need to account for their margin.
503            // The containing block position from viewport is (0, 0), but the root's
504            // content starts at (margin + border + padding, margin + border + padding).
505            let root_node = &new_tree.nodes[root_idx];
506            let is_root_with_margin = root_node.parent.is_none()
507                && (root_node.box_props.margin.left != 0.0 || root_node.box_props.margin.top != 0.0);
508
509            let adjusted_cb_pos = if is_root_with_margin {
510                LogicalPosition::new(
511                    cb_pos.x + root_node.box_props.margin.left,
512                    cb_pos.y + root_node.box_props.margin.top,
513                )
514            } else {
515                cb_pos
516            };
517
518            cache::calculate_layout_for_subtree(
519                &mut ctx,
520                &mut new_tree,
521                text_cache,
522                root_idx,
523                adjusted_cb_pos,
524                cb_size,
525                &mut calculated_positions,
526                &mut reflow_needed_for_scrollbars,
527                &mut cache.float_cache,
528                cache::ComputeMode::PerformLayout,
529            )?;
530
531            // For root nodes, the position should be at (margin.left, margin.top) relative
532            // to the viewport origin, because the margin creates space between the viewport
533            // edge and the element's border-box.
534            if !super::pos_contains(&calculated_positions, root_idx) {
535                let root_position = if is_root_with_margin {
536                    adjusted_cb_pos
537                } else {
538                    cb_pos
539                };
540                super::pos_set(&mut calculated_positions, root_idx, root_position);
541            }
542        }
543
544        cache::reposition_clean_subtrees(
545            new_dom,
546            &new_tree,
547            &recon_result.layout_roots,
548            &mut calculated_positions,
549        );
550
551        if reflow_needed_for_scrollbars {
552            ctx.debug_log("Scrollbars changed container size, starting full reflow...");
553            recon_result.layout_roots.clear();
554            recon_result.layout_roots.insert(new_tree.root);
555            recon_result.intrinsic_dirty = (0..new_tree.nodes.len()).collect();
556            continue;
557        }
558
559        break;
560    }
561
562
563    // --- Step 3: Adjust Positions ---
564    crate::solver3::positioning::adjust_relative_positions(
565        &mut ctx,
566        &new_tree,
567        &mut calculated_positions,
568        viewport,
569    )?;
570
571    crate::solver3::positioning::position_out_of_flow_elements(
572        &mut ctx,
573        &mut new_tree,
574        &mut calculated_positions,
575        viewport,
576    )?;
577
578
579    // --- Step 3.75: Compute Stable Scroll IDs ---
580    use crate::window::LayoutWindow;
581    let (scroll_ids, scroll_id_to_node_id) = LayoutWindow::compute_scroll_ids(&new_tree, new_dom);
582
583    // --- Step 4: Update Cache ---
584    let cache_map_back = std::mem::take(&mut ctx.cache_map);
585
586    cache.tree = Some(new_tree);
587    cache.calculated_positions = calculated_positions;
588    cache.viewport = Some(viewport);
589    cache.scroll_ids = scroll_ids.clone();
590    cache.scroll_id_to_node_id = scroll_id_to_node_id;
591    cache.counters = counter_values;
592    cache.cache_map = cache_map_back;
593
594    Ok(FragmentationLayoutResult {
595        scroll_ids,
596    })
597}
598
599// Helper function (copy from mod.rs)
600fn get_containing_block_for_node(
601    tree: &crate::solver3::layout_tree::LayoutTree,
602    styled_dom: &StyledDom,
603    node_idx: usize,
604    calculated_positions: &super::PositionVec,
605    viewport: LogicalRect,
606) -> (LogicalPosition, LogicalSize) {
607    use crate::solver3::getters::get_writing_mode;
608
609    if let Some(parent_idx) = tree.get(node_idx).and_then(|n| n.parent) {
610        if let Some(parent_node) = tree.get(parent_idx) {
611            let pos = calculated_positions
612                .get(parent_idx)
613                .copied()
614                .unwrap_or_default();
615            let size = parent_node.used_size.unwrap_or_default();
616            let content_pos = LogicalPosition::new(
617                pos.x + parent_node.box_props.border.left + parent_node.box_props.padding.left,
618                pos.y + parent_node.box_props.border.top + parent_node.box_props.padding.top,
619            );
620
621            if let Some(dom_id) = parent_node.dom_node_id {
622                let styled_node_state = &styled_dom
623                    .styled_nodes
624                    .as_container()
625                    .get(dom_id)
626                    .map(|n| &n.styled_node_state)
627                    .cloned()
628                    .unwrap_or_default();
629                let writing_mode =
630                    get_writing_mode(styled_dom, dom_id, styled_node_state).unwrap_or_default();
631                let content_size = parent_node.box_props.inner_size(size, writing_mode);
632                return (content_pos, content_size);
633            }
634
635            return (content_pos, size);
636        }
637    }
638    
639    // For ROOT nodes: the containing block is the viewport.
640    // Do NOT subtract margin here - margins are handled in calculate_used_size().
641    (viewport.origin, viewport.size)
642}