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

            
13
use std::collections::{BTreeMap, HashMap};
14

            
15
use azul_core::{
16
    dom::{DomId, NodeId},
17
    geom::{LogicalPosition, LogicalRect, LogicalSize},
18
    hit_test::ScrollPosition,
19
    resources::RendererResources,
20
    selection::TextSelection,
21
    styled_dom::StyledDom,
22
};
23
use azul_css::LayoutDebugMessage;
24

            
25
use crate::{
26
    font_traits::{ParsedFontTrait, TextLayoutCache},
27
    paged::FragmentationContext,
28
    solver3::{
29
        cache::LayoutCache,
30
        display_list::DisplayList,
31
        pagination::FakePageConfig,
32
        LayoutContext, LayoutError, Result,
33
    },
34
};
35

            
36
/// Result of `compute_layout_with_fragmentation`: contains the data
37
/// needed to generate a display list afterwards. The tree and
38
/// calculated_positions are stored in the `LayoutCache` that was passed in.
39
pub struct FragmentationLayoutResult {
40
    pub scroll_ids: HashMap<usize, u64>,
41
}
42

            
43
/// Layout a document with integrated pagination, returning one DisplayList per page.
44
///
45
/// +spec:positioning:a4936a - Absolutely positioned elements positioned relative to containing block ignoring page breaks
46
/// Layout is performed on a continuous document; pages are split afterward by Y position,
47
/// so absolutely positioned elements are positioned as if the document were continuous.
48
///
49
/// This function performs CSS Paged Media layout with fragmentation integrated
50
/// into the layout process itself, using the new architecture where:
51
///
52
/// 1. The FragmentationContext is passed to layout_document via LayoutContext
53
/// 2. Nodes get their page_index assigned during layout based on absolute Y position
54
/// 3. DisplayLists are generated per-page by filtering items based on page bounds
55
///
56
/// Uses default page header/footer configuration (page numbers in footer).
57
/// For custom headers/footers, use `layout_document_paged_with_config`.
58
///
59
/// # Arguments
60
/// * `fragmentation_context` - Controls page size and fragmentation behavior
61
/// * Other arguments same as `layout_document()`
62
///
63
/// # Returns
64
/// A vector of DisplayLists, one per page. Each DisplayList contains the
65
/// elements that fit on that page, with Y-coordinates relative to the page origin.
66
#[cfg(feature = "text_layout")]
67
pub fn layout_document_paged<T, F>(
68
    cache: &mut LayoutCache,
69
    text_cache: &mut TextLayoutCache,
70
    fragmentation_context: FragmentationContext,
71
    new_dom: &StyledDom,
72
    viewport: LogicalRect,
73
    font_manager: &mut crate::font_traits::FontManager<T>,
74
    scroll_offsets: &BTreeMap<NodeId, ScrollPosition>,
75
    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
76
    gpu_value_cache: Option<&azul_core::gpu::GpuValueCache>,
77
    renderer_resources: &RendererResources,
78
    id_namespace: azul_core::resources::IdNamespace,
79
    dom_id: DomId,
80
    font_loader: F,
81
    image_cache: &azul_core::resources::ImageCache,
82
    get_system_time_fn: azul_core::task::GetSystemTimeCallback,
83
) -> Result<Vec<DisplayList>>
84
where
85
    T: ParsedFontTrait + Sync + 'static,
86
    F: Fn(
87
        std::sync::Arc<rust_fontconfig::FontBytes>,
88
        usize,
89
    ) -> std::result::Result<T, crate::text3::cache::LayoutError>,
90
{
91
    // Use default page config (page numbers in footer)
92
    let page_config = FakePageConfig::new().with_footer_page_numbers();
93

            
94
    layout_document_paged_with_config(
95
        cache,
96
        text_cache,
97
        fragmentation_context,
98
        new_dom,
99
        viewport,
100
        font_manager,
101
        scroll_offsets,
102
        debug_messages,
103
        gpu_value_cache,
104
        renderer_resources,
105
        id_namespace,
106
        dom_id,
107
        font_loader,
108
        page_config,
109
        image_cache,
110
        get_system_time_fn,
111
        false,
112
    )
113
}
114

            
115
/// Layout a document with integrated pagination and custom page configuration.
116
///
117
/// This function is the same as `layout_document_paged` but allows you to
118
/// specify custom headers and footers via `FakePageConfig`.
119
///
120
/// # Arguments
121
/// * `page_config` - Configuration for page headers/footers (see `FakePageConfig`)
122
/// * Other arguments same as `layout_document_paged()`
123
#[cfg(feature = "text_layout")]
124
55
pub fn layout_document_paged_with_config<T, F>(
125
55
    cache: &mut LayoutCache,
126
55
    text_cache: &mut TextLayoutCache,
127
55
    mut fragmentation_context: FragmentationContext,
128
55
    new_dom: &StyledDom,
129
55
    viewport: LogicalRect,
130
55
    font_manager: &mut crate::font_traits::FontManager<T>,
131
55
    scroll_offsets: &BTreeMap<NodeId, ScrollPosition>,
132
55
    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
133
55
    gpu_value_cache: Option<&azul_core::gpu::GpuValueCache>,
134
55
    renderer_resources: &RendererResources,
135
55
    id_namespace: azul_core::resources::IdNamespace,
136
55
    dom_id: DomId,
137
55
    font_loader: F,
138
55
    page_config: FakePageConfig,
139
55
    image_cache: &azul_core::resources::ImageCache,
140
55
    get_system_time_fn: azul_core::task::GetSystemTimeCallback,
141
55
    print_timing: bool,
142
55
) -> Result<Vec<DisplayList>>
143
55
where
144
55
    T: ParsedFontTrait + Sync + 'static,
145
55
    F: Fn(
146
55
        std::sync::Arc<rust_fontconfig::FontBytes>,
147
55
        usize,
148
55
    ) -> std::result::Result<T, crate::text3::cache::LayoutError>,
149
{
150
    // Font Resolution And Loading
151
    {
152
        use crate::solver3::getters::{
153
            collect_and_resolve_font_chains_with_registration, collect_font_ids_from_chains,
154
            compute_fonts_to_load, load_fonts_from_disk,
155
        };
156

            
157
        // TODO: Accept platform as parameter instead of using ::current()
158
55
        let platform = azul_css::system::Platform::current();
159

            
160
55
        let chains = collect_and_resolve_font_chains_with_registration(
161
55
            new_dom, &font_manager.fc_cache, font_manager, &platform,
162
        );
163

            
164
55
        let required_fonts = collect_font_ids_from_chains(&chains);
165
55
        let already_loaded = font_manager.get_loaded_font_ids();
166
55
        let fonts_to_load = compute_fonts_to_load(&required_fonts, &already_loaded);
167

            
168
55
        if !fonts_to_load.is_empty() {
169
50
            let load_result =
170
50
                load_fonts_from_disk(&fonts_to_load, &font_manager.fc_cache, &font_loader);
171

            
172
50
            font_manager.insert_fonts(load_result.loaded);
173
50
            for (font_id, error) in &load_result.failed {
174
                if let Some(msgs) = debug_messages {
175
                    msgs.push(LayoutDebugMessage::warning(format!(
176
                        "[FontLoading] Failed to load font {:?}: {}",
177
                        font_id, error
178
                    )));
179
                }
180
            }
181
5
        }
182
55
        font_manager.set_font_chain_cache(chains.into_fontconfig_chains());
183
    }
184

            
185
    // Get page dimensions from fragmentation context
186
55
    let page_content_height = fragmentation_context.page_content_height();
187

            
188
    // Handle continuous media (no pagination)
189
55
    if !fragmentation_context.is_paged() {
190
        let _result = compute_layout_with_fragmentation(
191
            cache,
192
            text_cache,
193
            &mut fragmentation_context,
194
            new_dom,
195
            viewport,
196
            font_manager,
197
            debug_messages,
198
            image_cache,
199
            get_system_time_fn,
200
            print_timing,
201
        )?;
202

            
203
        // Generate display list from cached tree/positions
204
        let tree = cache.tree.as_ref().ok_or(LayoutError::InvalidTree)?;
205
        let mut counter_values = cache.counters.clone();
206
        let empty_text_selections: BTreeMap<DomId, TextSelection> = BTreeMap::new();
207
        let mut ctx = LayoutContext {
208
            scrollbar_style_cache: core::cell::RefCell::new(std::collections::HashMap::new()),
209
            styled_dom: new_dom,
210
            font_manager: &*font_manager,
211
            text_selections: &empty_text_selections,
212
            debug_messages,
213
            counters: &mut counter_values,
214
            viewport_size: viewport.size,
215
            fragmentation_context: Some(&mut fragmentation_context),
216
            cursor_is_visible: true,
217
            cursor_locations: Vec::new(),
218
            preedit_text: None,
219
            dirty_text_overrides: BTreeMap::new(),
220
            cache_map: std::mem::take(&mut cache.cache_map),
221
            image_cache,
222
            system_style: None,
223
            get_system_time_fn,
224
        };
225

            
226
        use crate::solver3::display_list::generate_display_list;
227
        let display_list = generate_display_list(
228
            &mut ctx,
229
            tree,
230
            &cache.calculated_positions,
231
            scroll_offsets,
232
            &cache.scroll_ids,
233
            gpu_value_cache,
234
            renderer_resources,
235
            id_namespace,
236
            dom_id,
237
        )?;
238
        cache.cache_map = std::mem::take(&mut ctx.cache_map);
239
        return Ok(vec![display_list]);
240
55
    }
241

            
242
    // Paged Layout
243

            
244
    // Perform layout with fragmentation context (layout only, no display list)
245
55
    let _result = compute_layout_with_fragmentation(
246
55
        cache,
247
55
        text_cache,
248
55
        &mut fragmentation_context,
249
55
        new_dom,
250
55
        viewport,
251
55
        font_manager,
252
55
        debug_messages,
253
55
        image_cache,
254
55
        get_system_time_fn,
255
55
        print_timing,
256
    )?;
257

            
258
    // Get the layout tree and positions
259
55
    let tree = cache.tree.as_ref().ok_or(LayoutError::InvalidTree)?;
260
55
    let calculated_positions = &cache.calculated_positions;
261

            
262
    // Debug: log page layout info
263
55
    if let Some(msgs) = debug_messages {
264
55
        msgs.push(LayoutDebugMessage::info(format!(
265
55
            "[PagedLayout] Page content height: {}",
266
55
            page_content_height
267
55
        )));
268
55
    }
269

            
270
    // Use scroll IDs computed by compute_layout_with_fragmentation (stored in cache)
271
55
    let scroll_ids = &cache.scroll_ids;
272

            
273
    // Create temporary context for display list generation
274
55
    let mut counter_values = cache.counters.clone();
275
55
    let empty_text_selections: BTreeMap<DomId, TextSelection> = BTreeMap::new();
276
55
    let mut ctx = LayoutContext {
277
55
        scrollbar_style_cache: core::cell::RefCell::new(std::collections::HashMap::new()),
278
55
        styled_dom: new_dom,
279
55
        font_manager: &*font_manager,
280
55
        text_selections: &empty_text_selections,
281
55
        debug_messages,
282
55
        counters: &mut counter_values,
283
55
        viewport_size: viewport.size,
284
55
        fragmentation_context: Some(&mut fragmentation_context),
285
55
        cursor_is_visible: true, // Paged layout: cursor always visible
286
55
        cursor_locations: Vec::new(),   // Paged layout: no cursor
287
55
        preedit_text: None,
288
55
        dirty_text_overrides: BTreeMap::new(),
289
55
        cache_map: std::mem::take(&mut cache.cache_map),
290
55
        image_cache,
291
55
        system_style: None,
292
55
        get_system_time_fn,
293
55
    };
294

            
295
    // NEW: Use the commitment-based pagination approach with CSS break properties
296
    //
297
    // This treats pages as viewports into a single infinite canvas:
298
    // 1. Generate ONE complete display list on infinite vertical strip
299
    // 2. Analyze CSS break properties (break-before, break-after, break-inside)
300
    // 3. Calculate page boundaries based on break properties
301
    // 4. Slice content to page boundaries (items are NEVER shifted, only clipped)
302
    // 5. Headers and footers are injected per-page
303
    //
304
    // Benefits over the old approach:
305
    // - No coordinate desynchronization between page_index and actual Y position
306
    // - Backgrounds render correctly (clipped, not torn/duplicated)
307
    // - Simple mental model: pages are just views into continuous content
308
    // - Headers/footers with page numbers are automatically generated
309
    // - CSS fragmentation properties are respected
310

            
311
    use crate::solver3::display_list::{
312
        generate_display_list, paginate_display_list_with_slicer_and_breaks,
313
        SlicerConfig,
314
    };
315

            
316
    // Step 1: Generate ONE complete display list (infinite canvas)
317
55
    let full_display_list = generate_display_list(
318
55
        &mut ctx,
319
55
        tree,
320
55
        calculated_positions,
321
55
        scroll_offsets,
322
55
        scroll_ids,
323
55
        gpu_value_cache,
324
55
        renderer_resources,
325
55
        id_namespace,
326
55
        dom_id,
327
    )?;
328

            
329
55
    if let Some(msgs) = ctx.debug_messages {
330
55
        msgs.push(LayoutDebugMessage::info(format!(
331
55
            "[PagedLayout] Generated master display list with {} items",
332
55
            full_display_list.items.len()
333
55
        )));
334
55
    }
335

            
336
    // Step 2: Configure the slicer with page dimensions and headers/footers
337
55
    let page_width = viewport.size.width;
338
55
    let header_footer = page_config.to_header_footer_config();
339

            
340
55
    if let Some(msgs) = ctx.debug_messages {
341
55
        msgs.push(LayoutDebugMessage::info(format!(
342
55
            "[PagedLayout] Page config: header={}, footer={}, skip_first={}",
343
55
            header_footer.show_header, header_footer.show_footer, header_footer.skip_first_page
344
55
        )));
345
55
    }
346

            
347
55
    let slicer_config = SlicerConfig {
348
55
        page_content_height,
349
55
        page_gap: 0.0,
350
55
        allow_clipping: true,
351
55
        header_footer,
352
55
        page_width,
353
55
        table_headers: Default::default(),
354
55
    };
355

            
356
    // Step 3: Paginate with CSS break property support
357
55
    let pages = paginate_display_list_with_slicer_and_breaks(
358
55
        full_display_list,
359
55
        &slicer_config,
360
    )?;
361

            
362
55
    if let Some(msgs) = ctx.debug_messages {
363
55
        msgs.push(LayoutDebugMessage::info(format!(
364
55
            "[PagedLayout] Paginated into {} pages with CSS break support",
365
55
            pages.len()
366
55
        )));
367
55
    }
368

            
369
55
    cache.cache_map = std::mem::take(&mut ctx.cache_map);
370

            
371
55
    Ok(pages)
372
55
}
373

            
374
/// Internal helper: Perform layout with a fragmentation context (layout only, no display list)
375
///
376
/// Returns a `FragmentationLayoutResult` containing the computed scroll IDs.
377
/// The tree & positions are stored in `cache`. To generate a display list,
378
/// call `generate_display_list` separately using the tree/positions from the cache.
379
#[cfg(feature = "text_layout")]
380
55
fn compute_layout_with_fragmentation<T: ParsedFontTrait + Sync + 'static>(
381
55
    cache: &mut LayoutCache,
382
55
    text_cache: &mut TextLayoutCache,
383
55
    fragmentation_context: &mut FragmentationContext,
384
55
    new_dom: &StyledDom,
385
55
    viewport: LogicalRect,
386
55
    font_manager: &crate::font_traits::FontManager<T>,
387
55
    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
388
55
    image_cache: &azul_core::resources::ImageCache,
389
55
    get_system_time_fn: azul_core::task::GetSystemTimeCallback,
390
55
    _print_timing: bool,
391
55
) -> Result<FragmentationLayoutResult> {
392
    use crate::solver3::cache;
393

            
394
    // Create temporary context without counters for tree generation
395
55
    let mut counter_values = HashMap::new();
396
55
    let empty_text_selections: BTreeMap<DomId, TextSelection> = BTreeMap::new();
397
55
    let mut ctx_temp = LayoutContext {
398
55
        scrollbar_style_cache: core::cell::RefCell::new(std::collections::HashMap::new()),
399
55
        styled_dom: new_dom,
400
55
        font_manager,
401
55
        text_selections: &empty_text_selections,
402
55
        debug_messages,
403
55
        counters: &mut counter_values,
404
55
        viewport_size: viewport.size,
405
55
        fragmentation_context: Some(fragmentation_context),
406
55
        cursor_is_visible: true, // Paged layout: cursor always visible
407
55
        cursor_locations: Vec::new(),   // Paged layout: no cursor
408
55
        preedit_text: None,
409
55
        dirty_text_overrides: BTreeMap::new(),
410
55
        cache_map: cache::LayoutCacheMap::default(),
411
55
        image_cache,
412
55
        system_style: None,
413
55
        get_system_time_fn,
414
55
    };
415

            
416
    // --- Step 1: Tree Building & Invalidation ---
417
55
    let is_fresh_dom = cache.tree.is_none();
418
55
    let (mut new_tree, mut recon_result) = if is_fresh_dom {
419
        // Fast path: no old tree to diff against — build tree directly.
420
        use crate::solver3::layout_tree::generate_layout_tree;
421
55
        let new_tree = generate_layout_tree(&mut ctx_temp)?;
422
55
        let n = new_tree.nodes.len();
423
55
        let mut result = cache::ReconciliationResult::default();
424
55
        result.layout_roots.insert(new_tree.root);
425
55
        result.intrinsic_dirty = (0..n).collect::<std::collections::BTreeSet<_>>();
426
55
        (new_tree, result)
427
    } else {
428
        // Incremental path: diff old tree vs new DOM
429
        cache::reconcile_and_invalidate(&mut ctx_temp, cache, viewport)?
430
    };
431

            
432
    // Step 1.2: Clear Taffy Caches for Dirty Nodes
433
329
    for &node_idx in &recon_result.intrinsic_dirty {
434
274
        if let Some(warm) = new_tree.warm_mut(node_idx) {
435
274
            warm.taffy_cache.clear();
436
274
        }
437
    }
438

            
439
    // Step 1.3: Compute CSS Counters
440
55
    cache::compute_counters(new_dom, &new_tree, &mut counter_values);
441

            
442
    // Step 1.4: Resize and invalidate per-node cache (Taffy-inspired 9+1 slot cache)
443
    // Move cache_map out of LayoutCache for the duration of layout.
444
55
    let mut cache_map = std::mem::take(&mut cache.cache_map);
445
55
    cache_map.resize_to_tree(new_tree.nodes.len());
446
329
    for &node_idx in &recon_result.intrinsic_dirty {
447
274
        cache_map.mark_dirty(node_idx, &new_tree.nodes);
448
274
    }
449
110
    for &node_idx in &recon_result.layout_roots {
450
55
        cache_map.mark_dirty(node_idx, &new_tree.nodes);
451
55
    }
452

            
453
    // Now create the real context with computed counters and fragmentation
454
55
    let mut ctx = LayoutContext {
455
55
        scrollbar_style_cache: core::cell::RefCell::new(std::collections::HashMap::new()),
456
55
        styled_dom: new_dom,
457
55
        font_manager,
458
55
        text_selections: &empty_text_selections,
459
55
        debug_messages,
460
55
        counters: &mut counter_values,
461
55
        viewport_size: viewport.size,
462
55
        fragmentation_context: Some(fragmentation_context),
463
55
        cursor_is_visible: true, // Paged layout: cursor always visible
464
55
        cursor_locations: Vec::new(),   // Paged layout: no cursor
465
55
        preedit_text: None,
466
55
        dirty_text_overrides: BTreeMap::new(),
467
55
        cache_map,
468
55
        image_cache,
469
55
        system_style: None,
470
55
        get_system_time_fn,
471
55
    };
472

            
473
    // --- Step 1.5: Early Exit Optimization ---
474
55
    if recon_result.is_clean() {
475
        ctx.debug_log("No changes, layout cache is clean");
476
        let tree = cache.tree.as_ref().ok_or(LayoutError::InvalidTree)?;
477

            
478
        use crate::window::LayoutWindow;
479
        let (scroll_ids, scroll_id_to_node_id) = LayoutWindow::compute_scroll_ids(tree, new_dom);
480
        cache.scroll_ids = scroll_ids.clone();
481
        cache.scroll_id_to_node_id = scroll_id_to_node_id;
482

            
483
        return Ok(FragmentationLayoutResult {
484
            scroll_ids,
485
        });
486
55
    }
487

            
488
    // --- Step 2: Incremental Layout Loop ---
489
55
    let mut calculated_positions = cache.calculated_positions.clone();
490
55
    let mut loop_count = 0;
491
    loop {
492
55
        loop_count += 1;
493
55
        if loop_count > 10 {
494
            break;
495
55
        }
496

            
497
55
        calculated_positions = cache.calculated_positions.clone();
498
55
        let mut reflow_needed_for_scrollbars = false;
499

            
500
55
        crate::solver3::sizing::calculate_intrinsic_sizes(
501
55
            &mut ctx,
502
55
            &mut new_tree,
503
55
            text_cache,
504
55
            &recon_result.intrinsic_dirty,
505
        )?;
506

            
507
110
        for &root_idx in &recon_result.layout_roots {
508
55
            let (cb_pos, cb_size) = get_containing_block_for_node(
509
55
                &new_tree,
510
55
                new_dom,
511
55
                root_idx,
512
55
                &calculated_positions,
513
55
                viewport,
514
55
            );
515

            
516
            // For ROOT nodes (no parent), we need to account for their margin.
517
            // The containing block position from viewport is (0, 0), but the root's
518
            // content starts at (margin + border + padding, margin + border + padding).
519
55
            let root_node = &new_tree.nodes[root_idx];
520
55
            let root_bp = root_node.box_props.unpack();
521
55
            let is_root_with_margin = root_node.parent.is_none()
522
55
                && (root_bp.margin.left != 0.0 || root_bp.margin.top != 0.0);
523

            
524
55
            let adjusted_cb_pos = if is_root_with_margin {
525
                LogicalPosition::new(
526
                    cb_pos.x + root_bp.margin.left,
527
                    cb_pos.y + root_bp.margin.top,
528
                )
529
            } else {
530
55
                cb_pos
531
            };
532

            
533
55
            cache::calculate_layout_for_subtree(
534
55
                &mut ctx,
535
55
                &mut new_tree,
536
55
                text_cache,
537
55
                root_idx,
538
55
                adjusted_cb_pos,
539
55
                cb_size,
540
55
                &mut calculated_positions,
541
55
                &mut reflow_needed_for_scrollbars,
542
55
                &mut cache.float_cache,
543
55
                cache::ComputeMode::PerformLayout,
544
            )?;
545

            
546
            // For root nodes, the position should be at (margin.left, margin.top) relative
547
            // to the viewport origin, because the margin creates space between the viewport
548
            // edge and the element's border-box.
549
55
            if !super::pos_contains(&calculated_positions, root_idx) {
550
55
                let root_position = if is_root_with_margin {
551
                    adjusted_cb_pos
552
                } else {
553
55
                    cb_pos
554
                };
555
55
                super::pos_set(&mut calculated_positions, root_idx, root_position);
556
            }
557
        }
558

            
559
55
        cache::reposition_clean_subtrees(
560
55
            new_dom,
561
55
            &new_tree,
562
55
            &recon_result.layout_roots,
563
55
            &mut calculated_positions,
564
        );
565

            
566
55
        if reflow_needed_for_scrollbars {
567
            ctx.debug_log("Scrollbars changed container size, starting full reflow...");
568
            recon_result.layout_roots.clear();
569
            recon_result.layout_roots.insert(new_tree.root);
570
            recon_result.intrinsic_dirty = (0..new_tree.nodes.len()).collect();
571
            continue;
572
55
        }
573

            
574
55
        break;
575
    }
576

            
577
    // --- Step 3: Adjust Positions ---
578
55
    crate::solver3::positioning::adjust_relative_positions(
579
55
        &mut ctx,
580
55
        &new_tree,
581
55
        &mut calculated_positions,
582
55
        viewport,
583
    )?;
584

            
585
55
    crate::solver3::positioning::position_out_of_flow_elements(
586
55
        &mut ctx,
587
55
        &mut new_tree,
588
55
        &mut calculated_positions,
589
55
        viewport,
590
    )?;
591

            
592
    // --- Step 3.75: Compute Stable Scroll IDs ---
593
    use crate::window::LayoutWindow;
594
55
    let (scroll_ids, scroll_id_to_node_id) = LayoutWindow::compute_scroll_ids(&new_tree, new_dom);
595

            
596
    // --- Step 4: Update Cache ---
597
55
    let cache_map_back = std::mem::take(&mut ctx.cache_map);
598

            
599
55
    cache.tree = Some(new_tree);
600
55
    cache.previous_positions = std::mem::replace(&mut cache.calculated_positions, calculated_positions);
601
55
    cache.viewport = Some(viewport);
602
55
    cache.scroll_ids = scroll_ids.clone();
603
55
    cache.scroll_id_to_node_id = scroll_id_to_node_id;
604
55
    cache.counters = counter_values;
605
55
    cache.cache_map = cache_map_back;
606

            
607
55
    Ok(FragmentationLayoutResult {
608
55
        scroll_ids,
609
55
    })
610
55
}
611

            
612
// Helper function (copy from mod.rs)
613
1925
fn get_containing_block_for_node(
614
1925
    tree: &crate::solver3::layout_tree::LayoutTree,
615
1925
    styled_dom: &StyledDom,
616
1925
    node_idx: usize,
617
1925
    calculated_positions: &super::PositionVec,
618
1925
    viewport: LogicalRect,
619
1925
) -> (LogicalPosition, LogicalSize) {
620
    use crate::solver3::getters::get_writing_mode;
621

            
622
1925
    if let Some(parent_idx) = tree.get(node_idx).and_then(|n| n.parent) {
623
        if let Some(parent_node) = tree.get(parent_idx) {
624
            let pos = calculated_positions
625
                .get(parent_idx)
626
                .copied()
627
                .unwrap_or_default();
628
            let size = parent_node.used_size.unwrap_or_default();
629
            let pbp = parent_node.box_props.unpack();
630
            let content_pos = LogicalPosition::new(
631
                pos.x + pbp.border.left + pbp.padding.left,
632
                pos.y + pbp.border.top + pbp.padding.top,
633
            );
634

            
635
            if let Some(dom_id) = parent_node.dom_node_id {
636
                let styled_node_state = &styled_dom
637
                    .styled_nodes
638
                    .as_container()
639
                    .get(dom_id)
640
                    .map(|n| &n.styled_node_state)
641
                    .cloned()
642
                    .unwrap_or_default();
643
                let writing_mode =
644
                    get_writing_mode(styled_dom, dom_id, styled_node_state).unwrap_or_default();
645
                let content_size = pbp.inner_size(size, writing_mode);
646
                return (content_pos, content_size);
647
            }
648

            
649
            return (content_pos, size);
650
        }
651
1925
    }
652
    
653
    // For ROOT nodes: the containing block is the viewport.
654
    // Do NOT subtract margin here - margins are handled in calculate_used_size().
655
1925
    (viewport.origin, viewport.size)
656
1925
}