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
59
pub fn layout_document_paged_with_config<T, F>(
125
59
    cache: &mut LayoutCache,
126
59
    text_cache: &mut TextLayoutCache,
127
59
    mut fragmentation_context: FragmentationContext,
128
59
    new_dom: &StyledDom,
129
59
    viewport: LogicalRect,
130
59
    font_manager: &mut crate::font_traits::FontManager<T>,
131
59
    scroll_offsets: &BTreeMap<NodeId, ScrollPosition>,
132
59
    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
133
59
    gpu_value_cache: Option<&azul_core::gpu::GpuValueCache>,
134
59
    renderer_resources: &RendererResources,
135
59
    id_namespace: azul_core::resources::IdNamespace,
136
59
    dom_id: DomId,
137
59
    font_loader: F,
138
59
    page_config: FakePageConfig,
139
59
    image_cache: &azul_core::resources::ImageCache,
140
59
    get_system_time_fn: azul_core::task::GetSystemTimeCallback,
141
59
    print_timing: bool,
142
59
) -> Result<Vec<DisplayList>>
143
59
where
144
59
    T: ParsedFontTrait + Sync + 'static,
145
59
    F: Fn(
146
59
        std::sync::Arc<rust_fontconfig::FontBytes>,
147
59
        usize,
148
59
    ) -> 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
59
        let platform = azul_css::system::Platform::current();
159

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

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

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

            
172
53
            font_manager.insert_fonts(load_result.loaded);
173
53
            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
6
        }
182
59
        font_manager.set_font_chain_cache(chains.into_fontconfig_chains());
183
    }
184

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

            
188
    // Handle continuous media (no pagination)
189
59
    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
59
    }
241

            
242
    // Paged Layout
243

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

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

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

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

            
273
    // Create temporary context for display list generation
274
59
    let mut counter_values = cache.counters.clone();
275
59
    let empty_text_selections: BTreeMap<DomId, TextSelection> = BTreeMap::new();
276
59
    let mut ctx = LayoutContext {
277
59
        scrollbar_style_cache: core::cell::RefCell::new(std::collections::HashMap::new()),
278
59
        styled_dom: new_dom,
279
59
        font_manager: &*font_manager,
280
59
        text_selections: &empty_text_selections,
281
59
        debug_messages,
282
59
        counters: &mut counter_values,
283
59
        viewport_size: viewport.size,
284
59
        fragmentation_context: Some(&mut fragmentation_context),
285
59
        cursor_is_visible: true, // Paged layout: cursor always visible
286
59
        cursor_locations: Vec::new(),   // Paged layout: no cursor
287
59
        preedit_text: None,
288
59
        dirty_text_overrides: BTreeMap::new(),
289
59
        cache_map: std::mem::take(&mut cache.cache_map),
290
59
        image_cache,
291
59
        system_style: None,
292
59
        get_system_time_fn,
293
59
    };
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
59
    let full_display_list = generate_display_list(
318
59
        &mut ctx,
319
59
        tree,
320
59
        calculated_positions,
321
59
        scroll_offsets,
322
59
        scroll_ids,
323
59
        gpu_value_cache,
324
59
        renderer_resources,
325
59
        id_namespace,
326
59
        dom_id,
327
    )?;
328

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

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

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

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

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

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

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

            
371
59
    Ok(pages)
372
59
}
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
59
fn compute_layout_with_fragmentation<T: ParsedFontTrait + Sync + 'static>(
381
59
    cache: &mut LayoutCache,
382
59
    text_cache: &mut TextLayoutCache,
383
59
    fragmentation_context: &mut FragmentationContext,
384
59
    new_dom: &StyledDom,
385
59
    viewport: LogicalRect,
386
59
    font_manager: &crate::font_traits::FontManager<T>,
387
59
    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
388
59
    image_cache: &azul_core::resources::ImageCache,
389
59
    get_system_time_fn: azul_core::task::GetSystemTimeCallback,
390
59
    _print_timing: bool,
391
59
) -> Result<FragmentationLayoutResult> {
392
    use crate::solver3::cache;
393

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

            
416
    // --- Step 1: Tree Building & Invalidation ---
417
59
    let is_fresh_dom = cache.tree.is_none();
418
59
    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
59
        let new_tree = generate_layout_tree(&mut ctx_temp)?;
422
59
        let n = new_tree.nodes.len();
423
59
        let mut result = cache::ReconciliationResult::default();
424
59
        result.layout_roots.insert(new_tree.root);
425
59
        result.intrinsic_dirty = (0..n).collect::<std::collections::BTreeSet<_>>();
426
59
        (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
360
    for &node_idx in &recon_result.intrinsic_dirty {
434
301
        if let Some(warm) = new_tree.warm_mut(node_idx) {
435
301
            warm.taffy_cache.clear();
436
301
        }
437
    }
438

            
439
    // Step 1.3: Compute CSS Counters
440
59
    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
59
    let mut cache_map = std::mem::take(&mut cache.cache_map);
445
59
    cache_map.resize_to_tree(new_tree.nodes.len());
446
360
    for &node_idx in &recon_result.intrinsic_dirty {
447
301
        cache_map.mark_dirty(node_idx, &new_tree.nodes);
448
301
    }
449
118
    for &node_idx in &recon_result.layout_roots {
450
59
        cache_map.mark_dirty(node_idx, &new_tree.nodes);
451
59
    }
452

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

            
473
    // --- Step 1.5: Early Exit Optimization ---
474
59
    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
59
    }
487

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

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

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

            
507
118
        for &root_idx in &recon_result.layout_roots {
508
59
            let (cb_pos, cb_size) = get_containing_block_for_node(
509
59
                &new_tree,
510
59
                new_dom,
511
59
                root_idx,
512
59
                &calculated_positions,
513
59
                viewport,
514
59
            );
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
59
            let root_node = &new_tree.nodes[root_idx];
520
59
            let root_bp = root_node.box_props.unpack();
521
59
            let is_root_with_margin = root_node.parent.is_none()
522
59
                && (root_bp.margin.left != 0.0 || root_bp.margin.top != 0.0);
523

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

            
533
59
            cache::calculate_layout_for_subtree(
534
59
                &mut ctx,
535
59
                &mut new_tree,
536
59
                text_cache,
537
59
                root_idx,
538
59
                adjusted_cb_pos,
539
59
                cb_size,
540
59
                &mut calculated_positions,
541
59
                &mut reflow_needed_for_scrollbars,
542
59
                &mut cache.float_cache,
543
59
                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
59
            if !super::pos_contains(&calculated_positions, root_idx) {
550
59
                let root_position = if is_root_with_margin {
551
1
                    adjusted_cb_pos
552
                } else {
553
58
                    cb_pos
554
                };
555
59
                super::pos_set(&mut calculated_positions, root_idx, root_position);
556
            }
557
        }
558

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

            
566
59
        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
59
        }
573

            
574
59
        break;
575
    }
576

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

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

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

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

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

            
608
59
    Ok(FragmentationLayoutResult {
609
59
        scroll_ids,
610
59
    })
611
59
}
612

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

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

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

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