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 crate::solver3::layout_tree::LayoutNodeId;
14
use crate::debug_log;
15
use std::collections::BTreeMap;
16

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

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

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

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

            
113
/// The full result of a paged layout: the analysis alongside the pages.
114
///
115
/// `pages[i].node_mapping` carries the per-item source `NodeId`s of page `i`
116
/// (paged hit-testing / diagnostics); `breaks` is the same analysis a
117
/// document editor gets from [`compute_document_pagination`] without
118
/// materializing any page.
119
#[derive(Debug)]
120
pub struct PagedLayoutResult {
121
    /// One display list per page.
122
    pub pages: Vec<DisplayList>,
123
    /// The break analysis the pages were sliced by (empty for continuous media).
124
    pub breaks: Vec<crate::solver3::page_breaks::PageBreakPosition>,
125
    /// Total document-space content height of the un-sliced document.
126
    pub total_content_height: f32,
127
}
128

            
129
/// Layout a document with integrated pagination and custom page configuration.
130
///
131
/// This function is the same as `layout_document_paged` but allows you to
132
/// specify custom headers and footers via `FakePageConfig`.
133
///
134
/// # Arguments
135
/// * `page_config` - Configuration for page headers/footers (see `FakePageConfig`)
136
/// * Other arguments same as `layout_document_paged()`
137
#[cfg(feature = "text_layout")]
138
// page_config is a small owned config struct passed once per paged-layout invocation by the
139
// dll PDF backend and the test suite; taking it by value keeps that one-shot API ergonomic.
140
#[allow(clippy::needless_pass_by_value)]
141
#[allow(clippy::too_many_arguments)]
142
/// # Errors
143
///
144
/// Returns a `LayoutError` if paged layout fails.
145
151
pub fn layout_document_paged_with_config<T, F>(
146
151
    cache: &mut LayoutCache,
147
151
    text_cache: &mut TextLayoutCache,
148
151
    fragmentation_context: FragmentationContext,
149
151
    new_dom: &StyledDom,
150
151
    viewport: LogicalRect,
151
151
    font_manager: &mut crate::font_traits::FontManager<T>,
152
151
    scroll_offsets: &BTreeMap<NodeId, ScrollPosition>,
153
151
    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
154
151
    gpu_value_cache: Option<&azul_core::gpu::GpuValueCache>,
155
151
    renderer_resources: &RendererResources,
156
151
    id_namespace: azul_core::resources::IdNamespace,
157
151
    dom_id: DomId,
158
151
    font_loader: F,
159
151
    page_config: FakePageConfig,
160
151
    image_cache: &azul_core::resources::ImageCache,
161
151
    get_system_time_fn: azul_core::task::GetSystemTimeCallback,
162
151
    print_timing: bool,
163
151
) -> Result<Vec<DisplayList>>
164
151
where
165
151
    T: ParsedFontTrait + Sync + 'static,
166
151
    F: Fn(
167
151
        std::sync::Arc<rust_fontconfig::FontBytes>,
168
151
        usize,
169
151
    ) -> std::result::Result<T, crate::text3::cache::LayoutError>,
170
{
171
    // Thin wrapper over the analysis-returning entry so printpdf 0.12.x
172
    // compiles unchanged; migrate to `layout_document_paged_v2` to get the
173
    // break analysis alongside the pages.
174
151
    layout_document_paged_v2(
175
151
        cache,
176
151
        text_cache,
177
151
        fragmentation_context,
178
151
        new_dom,
179
151
        viewport,
180
151
        font_manager,
181
151
        scroll_offsets,
182
151
        debug_messages,
183
151
        gpu_value_cache,
184
151
        renderer_resources,
185
151
        id_namespace,
186
151
        dom_id,
187
151
        font_loader,
188
151
        page_config,
189
151
        image_cache,
190
151
        get_system_time_fn,
191
151
        print_timing,
192
    )
193
151
    .map(|r| r.pages)
194
151
}
195

            
196
/// [`layout_document_paged_with_config`], returning the break ANALYSIS
197
/// alongside the pages (the document-editor/printpdf-diagnostics upgrade).
198
#[cfg(feature = "text_layout")]
199
#[allow(clippy::too_many_arguments)]
200
/// # Errors
201
///
202
/// Returns a `LayoutError` if paged layout fails.
203
151
pub fn layout_document_paged_v2<T, F>(
204
151
    cache: &mut LayoutCache,
205
151
    text_cache: &mut TextLayoutCache,
206
151
    fragmentation_context: FragmentationContext,
207
151
    new_dom: &StyledDom,
208
151
    viewport: LogicalRect,
209
151
    font_manager: &mut crate::font_traits::FontManager<T>,
210
151
    scroll_offsets: &BTreeMap<NodeId, ScrollPosition>,
211
151
    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
212
151
    gpu_value_cache: Option<&azul_core::gpu::GpuValueCache>,
213
151
    renderer_resources: &RendererResources,
214
151
    id_namespace: azul_core::resources::IdNamespace,
215
151
    dom_id: DomId,
216
151
    font_loader: F,
217
151
    page_config: FakePageConfig,
218
151
    image_cache: &azul_core::resources::ImageCache,
219
151
    get_system_time_fn: azul_core::task::GetSystemTimeCallback,
220
151
    print_timing: bool,
221
151
) -> Result<PagedLayoutResult>
222
151
where
223
151
    T: ParsedFontTrait + Sync + 'static,
224
151
    F: Fn(
225
151
        std::sync::Arc<rust_fontconfig::FontBytes>,
226
151
        usize,
227
151
    ) -> std::result::Result<T, crate::text3::cache::LayoutError>,
228
{
229
151
    layout_document_paged_impl(
230
151
        cache, text_cache, fragmentation_context, new_dom, viewport, font_manager,
231
151
        scroll_offsets, debug_messages, gpu_value_cache, renderer_resources,
232
151
        id_namespace, dom_id, font_loader, page_config, image_cache,
233
151
        get_system_time_fn, print_timing, true,
234
    )
235
151
}
236

            
237
#[cfg(feature = "text_layout")]
238
#[allow(clippy::needless_pass_by_value)]
239
#[allow(clippy::too_many_arguments)]
240
#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
241
184
fn layout_document_paged_impl<T, F>(
242
184
    cache: &mut LayoutCache,
243
184
    text_cache: &mut TextLayoutCache,
244
184
    mut fragmentation_context: FragmentationContext,
245
184
    new_dom: &StyledDom,
246
184
    viewport: LogicalRect,
247
184
    font_manager: &mut crate::font_traits::FontManager<T>,
248
184
    scroll_offsets: &BTreeMap<NodeId, ScrollPosition>,
249
184
    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
250
184
    gpu_value_cache: Option<&azul_core::gpu::GpuValueCache>,
251
184
    renderer_resources: &RendererResources,
252
184
    id_namespace: azul_core::resources::IdNamespace,
253
184
    dom_id: DomId,
254
184
    font_loader: F,
255
184
    page_config: FakePageConfig,
256
184
    image_cache: &azul_core::resources::ImageCache,
257
184
    get_system_time_fn: azul_core::task::GetSystemTimeCallback,
258
184
    print_timing: bool,
259
184
    materialize_pages: bool,
260
184
) -> Result<PagedLayoutResult>
261
184
where
262
184
    T: ParsedFontTrait + Sync + 'static,
263
184
    F: Fn(
264
184
        std::sync::Arc<rust_fontconfig::FontBytes>,
265
184
        usize,
266
184
    ) -> std::result::Result<T, crate::text3::cache::LayoutError>,
267
{
268
    use crate::solver3::display_list::{
269
        calculate_display_list_height, generate_display_list, paginate_display_list_with_breaks,
270
        SlicerConfig,
271
    };
272
    use crate::solver3::page_breaks;
273

            
274
    // Font Resolution And Loading
275
    {
276
        use crate::solver3::getters::{
277
            collect_and_resolve_font_chains_with_registration, collect_font_ids_from_chains,
278
            compute_fonts_to_load, load_fonts_from_disk,
279
        };
280

            
281
        // SKIP THE RESOLVER when this DOM asks for the same font stacks the
282
        // manager already resolved. `LayoutWindow` has done this since the
283
        // beginning (window.rs, `font_requirements_unchanged`) via a rolling
284
        // hash of the compact cache's `prev_font_hashes`; the pagination
285
        // entry points did not — and worse, called the plain
286
        // `set_font_chain_cache`, which CLEARS the recorded signature, so
287
        // even a caller reusing one FontManager re-resolved a 160-family
288
        // chain on EVERY pagination (measured 8 ms/call, ~8% of a warm one).
289
184
        let font_stacks_sig = new_dom
290
184
            .css_property_cache
291
184
            .ptr
292
184
            .compact_cache
293
184
            .as_ref()
294
184
            .map(|cc| {
295
183
                let mut h: u64 = 0xcbf2_9ce4_8422_2325;
296
3491
                for &fh in &cc.prev_font_hashes {
297
3308
                    h = h.rotate_left(13) ^ fh;
298
3308
                    h = h.wrapping_mul(0x0100_0000_01b3);
299
3308
                }
300
183
                h
301
183
            });
302
184
        let font_requirements_unchanged = font_stacks_sig.is_some()
303
183
            && font_stacks_sig == font_manager.last_resolved_font_stacks_sig
304
18
            && !font_manager.font_chain_cache.is_empty();
305

            
306
184
        if !font_requirements_unchanged {
307
167
            let _p = crate::probe::Probe::span("font_chain_resolve");
308
167
            let trace = std::env::var_os("AZ_PAGINATE_TRACE").is_some();
309
            // Clock reads are GATED ON `trace`, and use azul_core's clock rather
310
            // than std's, for two independent reasons:
311
            //
312
            //   * `std::time::Instant::now()` PANICS on wasm32-unknown-unknown,
313
            //     and azul-layout is built for wasm with `text_layout` (which
314
            //     turns std on, so a `feature = "std"` gate would not save it).
315
            //   * `azul_core::task::Instant` is FFI-shaped: it owns a
316
            //     `ManuallyDrop<Box<StdInstant>>`, so every `now()` is a heap
317
            //     allocation. Taking one unconditionally on this path made
318
            //     `regenerate_layout` grow 1112 B/iter under resize stress and
319
            //     tripped the leak regression test — which is exactly what that
320
            //     test is for.
321
            //
322
            // Tracing is off in every normal run, so this costs nothing there.
323
167
            let t0 = trace.then(azul_core::task::Instant::now);
324
167
            let platform = azul_css::system::Platform::current();
325

            
326
167
            let chains = collect_and_resolve_font_chains_with_registration(
327
167
                new_dom, &font_manager.fc_cache, font_manager, &platform,
328
            );
329
167
            let t_resolve =
330
167
                t0.map(|t0| azul_core::task::Instant::now().duration_since(&t0));
331

            
332
167
            let required_fonts = collect_font_ids_from_chains(&chains);
333
167
            let already_loaded = font_manager.get_loaded_font_ids();
334
167
            let fonts_to_load = compute_fonts_to_load(&required_fonts, &already_loaded);
335
167
            if trace {
336
                eprintln!(
337
                    "[paginate] font_chain_resolve {t_resolve:?}: {} chain(s), {} font(s) \
338
                     required, {} already loaded, {} to load",
339
                    chains.chains.len(),
340
                    required_fonts.len(),
341
                    already_loaded.len(),
342
                    fonts_to_load.len(),
343
                );
344
167
            }
345

            
346
167
            if !fonts_to_load.is_empty() {
347
129
                let t1 = trace.then(azul_core::task::Instant::now);
348
129
                let load_result =
349
129
                    load_fonts_from_disk(&fonts_to_load, &font_manager.fc_cache, &font_loader);
350
129
                if trace {
351
                    eprintln!(
352
                        "[paginate] load_fonts_from_disk {:?}: {} loaded, {} failed",
353
                        t1.map(|t1| azul_core::task::Instant::now().duration_since(&t1)),
354
                        load_result.loaded.len(),
355
                        load_result.failed.len(),
356
                    );
357
129
                }
358

            
359
129
                font_manager.insert_fonts(load_result.loaded);
360
145
                for (font_id, error) in &load_result.failed {
361
16
                    if let Some(msgs) = debug_messages {
362
16
                        msgs.push(LayoutDebugMessage::warning(format!(
363
16
                            "[FontLoading] Failed to load font {font_id:?}: {error}"
364
16
                        )));
365
16
                    }
366
                }
367
38
            }
368
167
            font_manager
369
167
                .set_font_chain_cache_with_sig(chains.into_fontconfig_chains(), font_stacks_sig);
370
17
        }
371
    }
372

            
373
    // Get page dimensions from fragmentation context
374
184
    let page_content_height = fragmentation_context.page_content_height();
375

            
376
    // Handle continuous media (no pagination)
377
184
    if !fragmentation_context.is_paged() {
378
1
        let _p = crate::probe::Probe::span("paged_layout_pass");
379
1
        compute_layout_with_fragmentation(
380
1
            cache,
381
1
            text_cache,
382
1
            &mut fragmentation_context,
383
1
            new_dom,
384
1
            viewport,
385
1
            font_manager,
386
1
            debug_messages,
387
1
            image_cache,
388
1
            get_system_time_fn,
389
1
            print_timing,
390
        )?;
391

            
392
        // Generate display list from cached tree/positions
393
1
        let tree = cache.tree.as_ref().ok_or(LayoutError::InvalidTree)?;
394
1
        let mut counter_values = cache.counters.clone();
395
1
        let empty_text_selections: BTreeMap<DomId, TextSelection> = BTreeMap::new();
396
1
        let mut ctx = LayoutContext {
397
1
            reflowed_ifcs: std::collections::BTreeSet::new(),
398
1
            style_cache: Default::default(),
399
1
            scrollbar_style_cache: core::cell::RefCell::new(std::collections::HashMap::new()),
400
1
            styled_dom: new_dom,
401
1
            font_manager: &*font_manager,
402
1
            text_selections: &empty_text_selections,
403
1
            debug_messages,
404
1
            counters: &mut counter_values,
405
1
            viewport_size: viewport.size,
406
1
            fragmentation_context: Some(&mut fragmentation_context),
407
1
            cursor_is_visible: true,
408
1
            cursor_locations: Vec::new(),
409
1
            preedit_text: None,
410
1
            cache_map: std::mem::take(&mut cache.cache_map),
411
1
            image_cache,
412
1
            content_overlay: None,
413
1
            system_style: None,
414
1
            get_system_time_fn,
415
1
        };
416

            
417
1
        let _p = crate::probe::Probe::span("paged_display_list");
418
1
        let display_list = generate_display_list(
419
1
            &mut ctx,
420
1
            tree,
421
1
            &cache.calculated_positions,
422
1
            scroll_offsets,
423
1
            &cache.scroll_ids,
424
1
            gpu_value_cache,
425
1
            renderer_resources,
426
1
            id_namespace,
427
1
            dom_id,
428
        )?;
429
1
        cache.cache_map = std::mem::take(&mut ctx.cache_map);
430
1
        let total_content_height = calculate_display_list_height(&display_list);
431
1
        return Ok(PagedLayoutResult {
432
1
            pages: vec![display_list],
433
1
            breaks: Vec::new(),
434
1
            total_content_height,
435
1
        });
436
183
    }
437

            
438
    // Paged Layout
439

            
440
    // Perform layout with fragmentation context (layout only, no display list)
441
183
    let p_layout = crate::probe::Probe::span("paged_layout_pass");
442
183
    compute_layout_with_fragmentation(
443
183
        cache,
444
183
        text_cache,
445
183
        &mut fragmentation_context,
446
183
        new_dom,
447
183
        viewport,
448
183
        font_manager,
449
183
        debug_messages,
450
183
        image_cache,
451
183
        get_system_time_fn,
452
183
        print_timing,
453
    )?;
454

            
455
    // Get the layout tree and positions
456
183
    let tree = cache.tree.as_ref().ok_or(LayoutError::InvalidTree)?;
457
183
    let calculated_positions = &cache.calculated_positions;
458

            
459
    // Debug: log page layout info
460
183
    if let Some(msgs) = debug_messages {
461
132
        msgs.push(LayoutDebugMessage::info(format!(
462
132
            "[PagedLayout] Page content height: {page_content_height}"
463
132
        )));
464
162
    }
465

            
466
    // Use scroll IDs computed by compute_layout_with_fragmentation (stored in cache)
467
183
    let scroll_ids = &cache.scroll_ids;
468

            
469
    // Create temporary context for display list generation
470
183
    let mut counter_values = cache.counters.clone();
471
183
    let empty_text_selections: BTreeMap<DomId, TextSelection> = BTreeMap::new();
472
183
    let mut ctx = LayoutContext {
473
183
            reflowed_ifcs: std::collections::BTreeSet::new(),
474
183
        style_cache: Default::default(),
475
183
        scrollbar_style_cache: core::cell::RefCell::new(std::collections::HashMap::new()),
476
183
        styled_dom: new_dom,
477
183
        font_manager: &*font_manager,
478
183
        text_selections: &empty_text_selections,
479
183
        debug_messages,
480
183
        counters: &mut counter_values,
481
183
        viewport_size: viewport.size,
482
183
        fragmentation_context: Some(&mut fragmentation_context),
483
183
        cursor_is_visible: true, // Paged layout: cursor always visible
484
183
        cursor_locations: Vec::new(),   // Paged layout: no cursor
485
183
        preedit_text: None,
486
183
        cache_map: std::mem::take(&mut cache.cache_map),
487
183
        image_cache,
488
183
        content_overlay: None,
489
183
        system_style: None,
490
183
        get_system_time_fn,
491
183
    };
492

            
493
    // NEW: Use the commitment-based pagination approach with CSS break properties
494
    //
495
    // This treats pages as viewports into a single infinite canvas:
496
    // 1. Generate ONE complete display list on infinite vertical strip
497
    // 2. Analyze CSS break properties (break-before, break-after, break-inside)
498
    // 3. Calculate page boundaries based on break properties
499
    // 4. Slice content to page boundaries (items are NEVER shifted, only clipped)
500
    // 5. Headers and footers are injected per-page
501
    //
502
    // Benefits over the old approach:
503
    // - No coordinate desynchronization between page_index and actual Y position
504
    // - Backgrounds render correctly (clipped, not torn/duplicated)
505
    // - Simple mental model: pages are just views into continuous content
506
    // - Headers/footers with page numbers are automatically generated
507
    // - CSS fragmentation properties are respected
508

            
509
    // Step 1: Generate ONE complete display list (infinite canvas)
510
183
    drop(p_layout);
511
183
    let _p_dl = crate::probe::Probe::span("paged_display_list");
512
183
    let full_display_list = generate_display_list(
513
183
        &mut ctx,
514
183
        tree,
515
183
        calculated_positions,
516
183
        scroll_offsets,
517
183
        scroll_ids,
518
183
        gpu_value_cache,
519
183
        renderer_resources,
520
183
        id_namespace,
521
183
        dom_id,
522
    )?;
523

            
524
183
    if let Some(msgs) = ctx.debug_messages {
525
132
        msgs.push(LayoutDebugMessage::info(format!(
526
132
            "[PagedLayout] Generated master display list with {} items",
527
132
            full_display_list.items.len()
528
132
        )));
529
162
    }
530

            
531
    // Step 2: Configure the slicer with page dimensions and headers/footers
532
183
    let page_width = viewport.size.width;
533
183
    let header_footer = page_config.to_header_footer_config();
534

            
535
183
    if let Some(msgs) = ctx.debug_messages {
536
132
        msgs.push(LayoutDebugMessage::info(format!(
537
132
            "[PagedLayout] Page config: header={}, footer={}, skip_first={}",
538
132
            header_footer.show_header, header_footer.show_footer, header_footer.skip_first_page
539
132
        )));
540
162
    }
541

            
542
    // B3c: with repeat_table_headers on, capture every table's thead from
543
    // the master display list — the registration side the tracker lacked.
544
183
    let table_headers = if page_config.break_policy.repeat_table_headers {
545
        crate::solver3::pagination::collect_table_headers(&full_display_list, new_dom)
546
    } else {
547
183
        crate::solver3::pagination::TableHeaderTracker::default()
548
    };
549

            
550
183
    let slicer_config = SlicerConfig {
551
183
        page_content_height,
552
183
        page_gap: 0.0,
553
183
        allow_clipping: true,
554
183
        header_footer,
555
183
        page_width,
556
183
        table_headers,
557
183
        break_policy: page_config.break_policy,
558
183
        page_sequence: page_config.page_sequence,
559
183
    };
560

            
561
    // Step 3: Analyze the breaks, THEN paginate against them — the analysis
562
    // is part of the result (document editors consume it without the pages).
563
    // Break-awareness runs per `slicer_config.break_policy` (all-off default
564
    // = the plain interval algorithm).
565
183
    let break_input = page_breaks::PageBreakInput {
566
183
        display_list: &full_display_list,
567
183
        layout_tree: cache.tree.as_ref(),
568
183
        styled_dom: new_dom,
569
183
        table_headers: Some(&slicer_config.table_headers),
570
183
    };
571
183
    let breaks = if let Some(sequence) = &slicer_config.page_sequence {
572
        // classic office suites model: every page's height from ITS setup.
573
3
        page_breaks::compute_page_breaks_with_sequence(
574
3
            &break_input,
575
3
            sequence,
576
3
            &slicer_config.break_policy,
577
        )
578
    } else {
579
180
        let constraints = page_breaks::PageConstraints::from_slicer_config(&slicer_config);
580
180
        page_breaks::compute_page_breaks(&break_input, &constraints, &slicer_config.break_policy)
581
    };
582
183
    let total_content_height = calculate_display_list_height(&full_display_list);
583

            
584
183
    let pages = if materialize_pages {
585
150
        paginate_display_list_with_breaks(
586
150
            full_display_list,
587
150
            &slicer_config,
588
150
            &breaks,
589
150
            renderer_resources,
590
        )?
591
    } else {
592
        // Precalculation-only: the analysis IS the result; no page is sliced.
593
33
        Vec::new()
594
    };
595

            
596
183
    if let Some(msgs) = ctx.debug_messages {
597
132
        msgs.push(LayoutDebugMessage::info(format!(
598
132
            "[PagedLayout] Paginated into {} pages with CSS break support",
599
132
            pages.len()
600
132
        )));
601
162
    }
602

            
603
183
    cache.cache_map = std::mem::take(&mut ctx.cache_map);
604

            
605
183
    Ok(PagedLayoutResult {
606
183
        pages,
607
183
        breaks,
608
183
        total_content_height,
609
183
    })
610
184
}
611

            
612
/// The PRECALCULATION-ONLY path (the document-editor requirement).
613
///
614
/// Full layout + display-list generation + break analysis, but NO per-page
615
/// display list is ever materialized. Pair with
616
/// [`crate::solver3::display_list::paginate_single_page`] to materialize
617
/// only visible pages, and [`crate::solver3::page_breaks::page_of_y`] to map
618
/// a node's Y to its page.
619
#[cfg(feature = "text_layout")]
620
#[allow(clippy::needless_pass_by_value)]
621
#[allow(clippy::too_many_arguments)]
622
/// # Errors
623
///
624
/// Returns a `LayoutError` if layout fails.
625
33
pub fn compute_document_pagination<T, F>(
626
33
    cache: &mut LayoutCache,
627
33
    text_cache: &mut TextLayoutCache,
628
33
    fragmentation_context: FragmentationContext,
629
33
    new_dom: &StyledDom,
630
33
    viewport: LogicalRect,
631
33
    font_manager: &mut crate::font_traits::FontManager<T>,
632
33
    scroll_offsets: &BTreeMap<NodeId, ScrollPosition>,
633
33
    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
634
33
    gpu_value_cache: Option<&azul_core::gpu::GpuValueCache>,
635
33
    renderer_resources: &RendererResources,
636
33
    id_namespace: azul_core::resources::IdNamespace,
637
33
    dom_id: DomId,
638
33
    font_loader: F,
639
33
    page_config: FakePageConfig,
640
33
    image_cache: &azul_core::resources::ImageCache,
641
33
    get_system_time_fn: azul_core::task::GetSystemTimeCallback,
642
33
) -> Result<crate::solver3::page_breaks::PaginationInfo>
643
33
where
644
33
    T: ParsedFontTrait + Sync + 'static,
645
33
    F: Fn(
646
33
        std::sync::Arc<rust_fontconfig::FontBytes>,
647
33
        usize,
648
33
    ) -> std::result::Result<T, crate::text3::cache::LayoutError>,
649
{
650
    use crate::solver3::page_breaks;
651

            
652
33
    let result = layout_document_paged_impl(
653
33
        cache,
654
33
        text_cache,
655
33
        fragmentation_context,
656
33
        new_dom,
657
33
        viewport,
658
33
        font_manager,
659
33
        scroll_offsets,
660
33
        debug_messages,
661
33
        gpu_value_cache,
662
33
        renderer_resources,
663
33
        id_namespace,
664
33
        dom_id,
665
33
        font_loader,
666
33
        page_config,
667
33
        image_cache,
668
33
        get_system_time_fn,
669
        false,
670
        false, // NO page is materialized — the acceptance criterion of this entry
671
    )?;
672
33
    let page_count = page_breaks::page_spans(&result.breaks, result.total_content_height)
673
33
        .len()
674
33
        .max(1);
675
33
    Ok(page_breaks::PaginationInfo {
676
33
        page_count,
677
33
        breaks: result.breaks,
678
33
        total_content_height: result.total_content_height,
679
33
    })
680
33
}
681

            
682
/// Internal helper: Perform layout with a fragmentation context (layout only, no display list)
683
///
684
/// The tree, positions, and scroll IDs are stored in `cache`. To generate a display list,
685
/// call `generate_display_list` separately using the tree/positions from the cache.
686
#[cfg(feature = "text_layout")]
687
#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
688
210
fn compute_layout_with_fragmentation<T: ParsedFontTrait + Sync + 'static>(
689
210
    cache: &mut LayoutCache,
690
210
    text_cache: &mut TextLayoutCache,
691
210
    fragmentation_context: &mut FragmentationContext,
692
210
    new_dom: &StyledDom,
693
210
    viewport: LogicalRect,
694
210
    font_manager: &crate::font_traits::FontManager<T>,
695
210
    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
696
210
    image_cache: &azul_core::resources::ImageCache,
697
210
    get_system_time_fn: azul_core::task::GetSystemTimeCallback,
698
210
    _print_timing: bool,
699
210
) -> Result<()> {
700
    use crate::solver3::cache;
701
    use crate::window::LayoutWindow;
702

            
703
    // Create temporary context without counters for tree generation
704
210
    let mut counter_values = std::collections::HashMap::new();
705
210
    let empty_text_selections: BTreeMap<DomId, TextSelection> = BTreeMap::new();
706
210
    let mut ctx_temp = LayoutContext {
707
210
            reflowed_ifcs: std::collections::BTreeSet::new(),
708
210
        style_cache: Default::default(),
709
210
        scrollbar_style_cache: core::cell::RefCell::new(std::collections::HashMap::new()),
710
210
        styled_dom: new_dom,
711
210
        font_manager,
712
210
        text_selections: &empty_text_selections,
713
210
        debug_messages,
714
210
        counters: &mut counter_values,
715
210
        viewport_size: viewport.size,
716
210
        fragmentation_context: Some(fragmentation_context),
717
210
        cursor_is_visible: true, // Paged layout: cursor always visible
718
210
        cursor_locations: Vec::new(),   // Paged layout: no cursor
719
210
        preedit_text: None,
720
210
        cache_map: cache::LayoutCacheMap::default(),
721
210
        image_cache,
722
210
        content_overlay: None,
723
210
        system_style: None,
724
210
        get_system_time_fn,
725
210
    };
726

            
727
    // --- Step 1: Tree Building & Invalidation ---
728
210
    let is_fresh_dom = cache.tree.is_none();
729
210
    let (mut new_tree, mut recon_result) = if is_fresh_dom {
730
        // Fast path: no old tree to diff against — build tree directly.
731
        use crate::solver3::layout_tree::generate_layout_tree;
732
196
        let new_tree = generate_layout_tree(&mut ctx_temp)?;
733
196
        let n = new_tree.nodes.len();
734
196
        let mut result = cache::ReconciliationResult::default();
735
196
        result.layout_roots.insert(new_tree.root);
736
196
        result.intrinsic_dirty = (0..n).collect::<std::collections::BTreeSet<_>>();
737
196
        (new_tree, result)
738
    } else {
739
        // Incremental path: diff old tree vs new DOM
740
14
        cache::reconcile_and_invalidate(&mut ctx_temp, cache, viewport, None)?
741
    };
742

            
743
    // Step 1.2: Clear Taffy Caches for Dirty Nodes
744
2578
    for &node_idx in &recon_result.intrinsic_dirty {
745
2368
        if let Some(warm) = new_tree.warm_mut(LayoutNodeId::new(node_idx)) {
746
2368
            warm.taffy_cache.clear();
747
2368
            warm.measured_content_sizes = (None, None);
748
2368
        }
749
    }
750

            
751
    // Step 1.3: Compute CSS Counters
752
210
    {
753
210
        let _p = crate::probe::Probe::span("frag_compute_counters");
754
210
        cache::compute_counters(new_dom, &new_tree, &mut counter_values);
755
210
    }
756

            
757
    // Step 1.4: Resize and invalidate per-node cache (Taffy-inspired 9+1 slot cache)
758
    // Move cache_map out of LayoutCache for the duration of layout.
759
210
    let mut cache_map = std::mem::take(&mut cache.cache_map);
760
210
    cache_map.resize_to_tree(new_tree.nodes.len());
761
2578
    for &node_idx in &recon_result.intrinsic_dirty {
762
2368
        cache_map.mark_dirty(node_idx, &new_tree.nodes);
763
2368
    }
764
416
    for &node_idx in &recon_result.layout_roots {
765
206
        cache_map.mark_dirty(node_idx, &new_tree.nodes);
766
206
    }
767

            
768
    // Now create the real context with computed counters and fragmentation
769
210
    let mut ctx = LayoutContext {
770
210
            reflowed_ifcs: std::collections::BTreeSet::new(),
771
210
        style_cache: Default::default(),
772
210
        scrollbar_style_cache: core::cell::RefCell::new(std::collections::HashMap::new()),
773
210
        styled_dom: new_dom,
774
210
        font_manager,
775
210
        text_selections: &empty_text_selections,
776
210
        debug_messages,
777
210
        counters: &mut counter_values,
778
210
        viewport_size: viewport.size,
779
210
        fragmentation_context: Some(fragmentation_context),
780
210
        cursor_is_visible: true, // Paged layout: cursor always visible
781
210
        cursor_locations: Vec::new(),   // Paged layout: no cursor
782
210
        preedit_text: None,
783
210
        cache_map,
784
210
        image_cache,
785
210
        content_overlay: None,
786
210
        system_style: None,
787
210
        get_system_time_fn,
788
210
    };
789

            
790
    // --- Step 1.5: Early Exit Optimization ---
791
210
    if recon_result.is_clean() {
792
4
        debug_log!(ctx, "No changes, layout cache is clean");
793
4
        let tree = cache.tree.as_ref().ok_or(LayoutError::InvalidTree)?;
794

            
795
4
        let (scroll_ids, scroll_id_to_node_id) = LayoutWindow::compute_scroll_ids(tree, new_dom);
796
4
        cache.scroll_ids = scroll_ids;
797
4
        cache.scroll_id_to_node_id = scroll_id_to_node_id;
798

            
799
4
        return Ok(());
800
206
    }
801

            
802
    // --- Step 2: Incremental Layout Loop ---
803
206
    let p_clone_pos = crate::probe::Probe::span("frag_clone_positions");
804
206
    let mut calculated_positions = cache.calculated_positions.clone();
805
206
    drop(p_clone_pos);
806
206
    let mut loop_count = 0;
807
    loop {
808
206
        loop_count += 1;
809
206
        if loop_count > 10 {
810
            break;
811
206
        }
812

            
813
206
        calculated_positions.clone_from(&cache.calculated_positions);
814
206
        let mut reflow_needed_for_scrollbars = false;
815

            
816
206
        let _p_intrinsic = crate::probe::Probe::span("frag_intrinsic_sizes");
817
206
        crate::solver3::sizing::calculate_intrinsic_sizes(
818
206
            &mut ctx,
819
206
            &mut new_tree,
820
206
            text_cache,
821
206
            &recon_result.intrinsic_dirty,
822
        )?;
823

            
824
412
        for &root_idx in &recon_result.layout_roots {
825
206
            let (cb_pos, cb_size) = super::get_containing_block_for_node(
826
206
                &new_tree,
827
206
                new_dom,
828
206
                root_idx,
829
206
                &calculated_positions,
830
206
                viewport,
831
206
            );
832

            
833
            // For ROOT nodes (no parent), we need to account for their margin.
834
            // The containing block position from viewport is (0, 0), but the root's
835
            // content starts at (margin + border + padding, margin + border + padding).
836
206
            let root_node = &new_tree.nodes[root_idx];
837
206
            let root_bp = root_node.box_props.unpack();
838
206
            let is_root_with_margin = root_node.parent.is_none()
839
206
                && (root_bp.margin.left != 0.0 || root_bp.margin.top != 0.0);
840

            
841
206
            let adjusted_cb_pos = if is_root_with_margin {
842
48
                LogicalPosition::new(
843
48
                    cb_pos.x + root_bp.margin.left,
844
48
                    cb_pos.y + root_bp.margin.top,
845
                )
846
            } else {
847
158
                cb_pos
848
            };
849

            
850
206
            cache::calculate_layout_for_subtree(
851
206
                &mut ctx,
852
206
                &mut new_tree,
853
206
                text_cache,
854
206
                root_idx,
855
206
                adjusted_cb_pos,
856
206
                cb_size,
857
206
                &mut calculated_positions,
858
206
                &mut reflow_needed_for_scrollbars,
859
206
                &mut cache.float_cache,
860
206
                cache::ComputeMode::PerformLayout,
861
            )?;
862

            
863
            // For root nodes, the position should be at (margin.left, margin.top) relative
864
            // to the viewport origin, because the margin creates space between the viewport
865
            // edge and the element's border-box.
866
206
            if !super::pos_contains(&calculated_positions, root_idx) {
867
196
                let root_position = if is_root_with_margin {
868
45
                    adjusted_cb_pos
869
                } else {
870
151
                    cb_pos
871
                };
872
196
                super::pos_set(&mut calculated_positions, root_idx, root_position);
873
10
            }
874
        }
875

            
876
206
        cache::reposition_clean_subtrees(
877
206
            new_dom,
878
206
            &new_tree,
879
206
            &recon_result.layout_roots,
880
206
            &mut calculated_positions,
881
        );
882

            
883
206
        if reflow_needed_for_scrollbars {
884
            debug_log!(ctx, "Scrollbars changed container size, starting full reflow...");
885
            recon_result.layout_roots.clear();
886
            recon_result.layout_roots.insert(new_tree.root);
887
            recon_result.intrinsic_dirty = (0..new_tree.nodes.len()).collect();
888
            continue;
889
206
        }
890

            
891
206
        break;
892
    }
893

            
894
    // --- Step 3: Adjust Positions ---
895
206
    crate::solver3::positioning::adjust_relative_positions(
896
206
        &mut ctx,
897
206
        &new_tree,
898
206
        &mut calculated_positions,
899
206
        viewport,
900
    );
901

            
902
206
    crate::solver3::positioning::position_out_of_flow_elements(
903
206
        &mut ctx,
904
206
        &mut new_tree,
905
206
        text_cache,
906
206
        &mut calculated_positions,
907
206
        viewport,
908
    );
909

            
910
    // --- Step 3.75: Compute Stable Scroll IDs ---
911
206
    let (scroll_ids, scroll_id_to_node_id) = LayoutWindow::compute_scroll_ids(&new_tree, new_dom);
912

            
913
    // --- Step 4: Update Cache ---
914
206
    let cache_map_back = std::mem::take(&mut ctx.cache_map);
915

            
916
206
    cache.tree = Some(new_tree);
917
206
    cache.previous_positions = std::mem::replace(&mut cache.calculated_positions, calculated_positions);
918
206
    cache.viewport = Some(viewport);
919
206
    cache.scroll_ids = scroll_ids;
920
206
    cache.scroll_id_to_node_id = scroll_id_to_node_id;
921
206
    cache.counters = counter_values;
922
206
    cache.cache_map = cache_map_back;
923

            
924
206
    Ok(())
925
210
}
926

            
927
/// One width-section's pagination within a sectioned document.
928
#[derive(Debug, Clone)]
929
pub struct SectionPagination {
930
    /// 0-based GLOBAL index of the section's first page.
931
    pub first_page: usize,
932
    /// The width this section's content was laid out (re-wrapped) against.
933
    pub content_width: f32,
934
    /// Pagination of the section's OWN content (Y coordinates are local to
935
    /// the section's layout, page indices local to the section).
936
    pub info: crate::solver3::page_breaks::PaginationInfo,
937
}
938

            
939
/// Result of [`compute_sectioned_pagination`]: per-width-section pagination.
940
#[derive(Debug, Clone)]
941
pub struct SectionedPaginationInfo {
942
    pub sections: Vec<SectionPagination>,
943
}
944

            
945
impl SectionedPaginationInfo {
946
    /// Total page count across all sections.
947
    #[must_use]
948
2
    pub fn page_count(&self) -> usize {
949
2
        self.sections.iter().map(|s| s.info.page_count).sum::<usize>().max(1)
950
2
    }
951
}
952

            
953
/// The child-index path (root → node) of the first block-level box whose top
954
/// edge sits at/after `y` — the SPINE the fragmentainer cut runs along.
955
///
956
/// `document_edit::split_dom_at_path` consumes this path to cut the
957
/// reconstructed document for the next section's re-wrap. Ties (equal Y)
958
/// resolve to the SHALLOWEST node so the cut spine stays as high as possible.
959
#[must_use]
960
163
pub fn spine_path_at_y(
961
163
    tree: &crate::solver3::layout_tree::LayoutTree,
962
163
    positions: &crate::solver3::PositionVec,
963
163
    styled_dom: &StyledDom,
964
163
    y: f32,
965
163
) -> Option<Vec<u32>> {
966
163
    let hierarchy = styled_dom.node_hierarchy.as_container();
967
167
    let depth_of = |mut n: NodeId| -> u32 {
968
104
        let mut d = 0;
969
289
        while let Some(p) = hierarchy.get(n).and_then(azul_core::styled_dom::NodeHierarchyItem::parent_id) {
970
185
            d += 1;
971
185
            n = p;
972
185
        }
973
104
        d
974
104
    };
975

            
976
163
    let mut best: Option<(f32, u32, NodeId)> = None;
977
1062
    for idx in 0..tree.nodes.len() {
978
1062
        let Some(node) = tree.get(LayoutNodeId::new(idx)) else { continue };
979
1062
        let Some(dom_id) = node.dom_node_id else { continue };
980
1062
        if !crate::solver3::layout_tree::is_block_level(styled_dom, dom_id) {
981
378
            continue;
982
684
        }
983
684
        let Some(pos) = crate::solver3::pos_get(positions, idx) else {
984
            continue;
985
        };
986
684
        if pos.y < y - 0.5 {
987
580
            continue;
988
104
        }
989
104
        let d = depth_of(dom_id);
990
104
        let better = match &best {
991
82
            None => true,
992
22
            Some((by, bd, _)) => pos.y < *by - 0.01 || ((pos.y - by).abs() <= 0.01 && d < *bd),
993
        };
994
104
        if better {
995
82
            best = Some((pos.y, d, dom_id));
996
85
        }
997
    }
998

            
999
163
    let (_, _, node) = best?;
    // Child-index path root → node.
82
    let mut path: Vec<u32> = Vec::new();
82
    let mut cur = node;
227
    while let Some(parent) = hierarchy.get(cur).and_then(azul_core::styled_dom::NodeHierarchyItem::parent_id) {
145
        let mut i: u32 = 0;
145
        let mut c = hierarchy.get(parent).and_then(|h| h.first_child_id(parent));
409
        while let Some(cc) = c {
409
            if cc == cur {
145
                break;
264
            }
264
            i += 1;
264
            c = hierarchy
264
                .get(cc)
264
                .and_then(azul_core::styled_dom::NodeHierarchyItem::next_sibling_id);
        }
145
        path.push(i);
145
        cur = parent;
    }
82
    path.reverse();
82
    Some(path)
163
}
/// Materialize the tail of a [`PageSequence`] starting at `first_page` as a
/// standalone sequence (local page 0 = global `first_page`). Bounded
/// override copying; parity/first-page variation collapses into explicit
/// overrides so no offset arithmetic leaks into the break pass.
#[must_use]
3
fn materialize_sequence_tail(
3
    seq: &crate::solver3::pagination::PageSequence,
3
    first_page: usize,
3
    scan: usize,
3
) -> crate::solver3::pagination::PageSequence {
3
    let mut out =
3
        crate::solver3::pagination::PageSequence::uniform(seq.default.clone());
1025
    for local in 0..scan {
1025
        let setup = seq.setup_for_page(first_page + local);
        // Geometry decides pagination; decoration differences don't need an
        // override entry (the slicer reads decoration off the ORIGINAL
        // sequence by global page index).
1025
        let differs = (setup.content_width() - out.default.content_width()).abs() >= 0.5
1024
            || (setup.content_height() - out.default.content_height()).abs() >= 0.5;
1025
        if differs {
1
            out.overrides.insert(local, setup.clone());
1024
        }
    }
3
    out
3
}
/// Fragmentainer-flow pagination with PER-SECTION WIDTH RE-WRAP.
///
/// [`PageSequence::width_sections`] partitions pages into maximal
/// equal-width runs (the classic office suites model: page setup changes at section
/// breaks). Content lays out ONCE per section at that section's width; when
/// a section's page budget fills, the document is CUT along the spine of
/// the first block on the next page ([`spine_path_at_y`] +
/// [`crate::document_edit::split_dom_at_path`]), the tail re-styles against
/// the retained author css, and the flow continues in the next section at
/// its width. This replaces the `has_uniform_width()` degradation for the
/// document-pagination path.
///
/// Limits (staged): the cut is block-granular (a paragraph straddling a
/// section boundary moves wholly to the next section rather than splitting
/// mid-line — the classic office-suite behavior for section breaks); floats/positioned boxes
/// do not carry across sections.
///
/// # Errors
///
/// Returns a `LayoutError` if any section's layout fails.
// xml: `document_edit` (the spine-cut applier) lives behind text_layout+xml.
#[cfg(all(feature = "text_layout", feature = "xml"))]
#[allow(clippy::too_many_arguments)]
2
pub fn compute_sectioned_pagination<T, F>(
2
    styled_dom: &StyledDom,
2
    page_height: f32,
2
    font_manager: &mut crate::font_traits::FontManager<T>,
2
    renderer_resources: &RendererResources,
2
    id_namespace: azul_core::resources::IdNamespace,
2
    dom_id: DomId,
2
    font_loader: F,
2
    page_config: &FakePageConfig,
2
    sequence: &crate::solver3::pagination::PageSequence,
2
    image_cache: &azul_core::resources::ImageCache,
2
    get_system_time_fn: azul_core::task::GetSystemTimeCallback,
2
) -> Result<SectionedPaginationInfo>
2
where
2
    T: ParsedFontTrait + Sync + 'static,
2
    F: Fn(
2
            std::sync::Arc<rust_fontconfig::FontBytes>,
2
            usize,
2
        ) -> std::result::Result<T, crate::text3::cache::LayoutError>
2
        + Copy,
{
    use azul_core::geom::{LogicalPosition, LogicalRect, LogicalSize};
    const SECTION_SCAN: usize = 512;
2
    let sections = sequence.width_sections(SECTION_SCAN);
    // Tails re-style against the ORIGINAL document's author css — a Dom
    // reconstructed from a StyledDom carries it in `.css`, but `create`'s
    // css parameter is the reliable non-lossy channel.
2
    let author_css = styled_dom
2
        .get_css_property_cache()
2
        .retained_author_css
2
        .clone();
2
    let mut out = SectionedPaginationInfo { sections: Vec::new() };
    // The working document: exact styled_dom for section 0; reconstructed +
    // cut tails afterwards. The reconstruction happens lazily (only when a
    // second section actually receives content).
2
    let mut working: Option<azul_core::dom::Dom> = None;
2
    let mut working_styled: Option<StyledDom> = None;
3
    for (k, sec) in sections.iter().enumerate() {
3
        let is_last = k + 1 == sections.len();
3
        let section_styled: &StyledDom = working_styled.as_ref().map_or(styled_dom, |s| s);
3
        let content_size = LogicalSize::new(sec.content_width, page_height);
3
        let viewport = LogicalRect {
3
            origin: LogicalPosition::zero(),
3
            size: content_size,
3
        };
3
        let mut cache = LayoutCache::default();
3
        let mut text_cache = TextLayoutCache::new();
3
        let frag = FragmentationContext::new_paged(content_size);
3
        let mut cfg = page_config.clone();
3
        cfg.page_sequence = Some(materialize_sequence_tail(
3
            sequence,
3
            sec.first_page,
3
            sec.page_count.unwrap_or(SECTION_SCAN).min(SECTION_SCAN),
3
        ));
3
        let info = compute_document_pagination(
3
            &mut cache,
3
            &mut text_cache,
3
            frag,
3
            section_styled,
3
            viewport,
3
            font_manager,
3
            &BTreeMap::new(),
3
            &mut None,
3
            None,
3
            renderer_resources,
3
            id_namespace,
3
            dom_id,
3
            font_loader,
3
            cfg,
3
            image_cache,
3
            get_system_time_fn,
        )?;
3
        let budget = sec.page_count.unwrap_or(usize::MAX);
3
        if is_last || info.page_count <= budget {
            // Content ends inside this section.
2
            out.sections.push(SectionPagination {
2
                first_page: sec.first_page,
2
                content_width: sec.content_width,
2
                info,
2
            });
2
            return Ok(out);
1
        }
        // Section overflows its page budget: cut at the end of its last page
        // and flow the tail into the next section at ITS width.
1
        let cut_y = info.breaks.get(budget - 1).map_or(info.total_content_height, |b| b.y);
1
        let tree = cache.tree.as_ref().ok_or(LayoutError::InvalidTree)?;
1
        let spine = spine_path_at_y(tree, &cache.calculated_positions, section_styled, cut_y);
1
        let working_dom = working
1
            .take()
1
            .map_or_else(|| section_styled.reconstruct_dom_subtree(None), |d| d);
        // No block starts after the cut: everything fits after all.
1
        let Some(path) = &spine else {
            out.sections.push(SectionPagination {
                first_page: sec.first_page,
                content_width: sec.content_width,
                info,
            });
            return Ok(out);
        };
        // The head's pages come from `info` (trimmed below); only the tail
        // flows on.
1
        let (_head, tail) = crate::document_edit::split_dom_at_path(&working_dom, path);
1
        let mut trimmed = info;
1
        trimmed.page_count = budget;
1
        trimmed.breaks.truncate(budget.saturating_sub(1));
1
        trimmed.total_content_height = cut_y;
1
        out.sections.push(SectionPagination {
1
            first_page: sec.first_page,
1
            content_width: sec.content_width,
1
            info: trimmed,
1
        });
        // StyledDom::create consumes the Dom's node data — style a CLONE and
        // keep the pristine tail as the working document for the next cut.
1
        let mut style_me = tail.clone();
1
        working_styled = Some(StyledDom::create(&mut style_me, author_css.clone()));
1
        working = Some(tail);
    }
    Ok(out)
2
}
#[cfg(all(test, feature = "text_layout"))]
#[allow(clippy::float_cmp)]
mod autotest_generated {
    use azul_core::{
        dom::Dom,
        resources::{IdNamespace, ImageCache},
        task::{get_system_time_libstd, GetSystemTimeCallback},
    };
    use rust_fontconfig::FcFontCache;
    use super::*;
    use crate::{font_traits::FontManager, text3::default::PathLoader};
    // ---------------------------------------------------------------------
    // Harness
    //
    // Every DOM below is deliberately TEXT-FREE, so no font ever has to be
    // resolved and the font cache can stay empty (no system-font I/O, so the
    // tests are hermetic and identical on every machine).
    // ---------------------------------------------------------------------
    /// The crate's only `ParsedFontTrait` impl (`text3::default`).
    type TestFont = azul_css::props::basic::FontRef;
    fn time_fn() -> GetSystemTimeCallback {
        GetSystemTimeCallback {
            cb: get_system_time_libstd,
        }
    }
    fn font_manager() -> FontManager<TestFont> {
        FontManager::new(FcFontCache::default()).expect("FontManager::new must not fail")
    }
    fn viewport(width: f32, height: f32) -> LogicalRect {
        LogicalRect {
            origin: LogicalPosition::zero(),
            size: LogicalSize::new(width, height),
        }
    }
    fn paged(width: f32, height: f32) -> FragmentationContext {
        FragmentationContext::new_paged(LogicalSize::new(width, height))
    }
    /// `<body>` with `n` painted, 200px-tall divs — a document ~`n * 200`px tall.
    /// The background is what makes each div emit a display-list item, and the
    /// paginator derives the document height from those items.
    fn doc(n: usize) -> StyledDom {
        let children: Vec<Dom> = (0..n).map(|_| Dom::create_div()).collect();
        let mut dom = Dom::create_body().with_children(children.into());
        let css = azul_css::parser2::new_from_str(
            "div { height: 200px; width: 100px; background-color: red; }",
        )
        .0;
        StyledDom::create(&mut dom, css)
    }
    fn run_with(
        cache: &mut LayoutCache,
        font_manager: &mut FontManager<TestFont>,
        fragmentation_context: FragmentationContext,
        dom: &StyledDom,
        vp: LogicalRect,
        page_config: FakePageConfig,
    ) -> Result<Vec<DisplayList>> {
        let loader = PathLoader::new();
        let mut text_cache = TextLayoutCache::new();
        let mut debug_messages = None;
        layout_document_paged_with_config(
            cache,
            &mut text_cache,
            fragmentation_context,
            dom,
            vp,
            font_manager,
            &BTreeMap::new(),
            &mut debug_messages,
            None,
            &RendererResources::default(),
            IdNamespace(0),
            DomId::ROOT_ID,
            |bytes: std::sync::Arc<rust_fontconfig::FontBytes>, index: usize| {
                loader.load_font_shared(bytes, index)
            },
            page_config,
            &ImageCache::default(),
            time_fn(),
            false,
        )
    }
    /// One-shot paged layout against a fresh cache.
    fn run(
        fragmentation_context: FragmentationContext,
        dom: &StyledDom,
        vp: LogicalRect,
        page_config: FakePageConfig,
    ) -> Result<Vec<DisplayList>> {
        let mut cache = LayoutCache::default();
        let mut font_manager = font_manager();
        run_with(
            &mut cache,
            &mut font_manager,
            fragmentation_context,
            dom,
            vp,
            page_config,
        )
    }
    /// Number of pages for a fresh, default-configured paged layout.
    fn page_count(fragmentation_context: FragmentationContext, dom: &StyledDom, vp: LogicalRect) -> usize {
        run(fragmentation_context, dom, vp, FakePageConfig::new())
            .expect("paged layout must not fail")
            .len()
    }
    fn item_counts(pages: &[DisplayList]) -> Vec<usize> {
        pages.iter().map(|p| p.items.len()).collect()
    }
    fn compute(
        cache: &mut LayoutCache,
        fragmentation_context: &mut FragmentationContext,
        dom: &StyledDom,
        vp: LogicalRect,
    ) -> Result<()> {
        let font_manager = font_manager();
        let mut text_cache = TextLayoutCache::new();
        let mut debug_messages = None;
        compute_layout_with_fragmentation(
            cache,
            &mut text_cache,
            fragmentation_context,
            dom,
            vp,
            &font_manager,
            &mut debug_messages,
            &ImageCache::default(),
            time_fn(),
            false,
        )
    }
    fn tree_node_count(cache: &LayoutCache) -> usize {
        cache.tree.as_ref().expect("layout must cache a tree").nodes.len()
    }
    // ---------------------------------------------------------------------
    // Baseline invariants
    //
    // NOTE (not tested here — the assertions would hang the suite):
    // `calculate_page_break_positions` (display_list.rs) advances by
    // `y += normal_page_height` while `y < total_height`. Two reachable
    // inputs make that loop non-terminating while pushing into an unbounded
    // Vec (hang → OOM), and both are reachable from these two entry points:
    //   1. a tiny positive page height (e.g. 1e-30) — it clears the
    //      `page_content_height <= 0.0` guard, but `y += 1e-30` stops moving
    //      `y` as soon as the step falls below `y`'s ULP;
    //   2. `skip_first_page(true)` with `header_height + footer_height`
    //      >= the page height — `normal_page_height` goes negative, so `y`
    //      walks *backwards* away from `total_height` forever.
    // The tests below stay strictly on the safe side of both, and the guarded
    // variants (0 / negative / NaN / inf / f32::MAX heights, and an oversized
    // header WITHOUT skip_first_page) are asserted instead.
    // ---------------------------------------------------------------------
    #[test]
    fn continuous_context_returns_exactly_one_display_list() {
        let dom = doc(5);
        let pages = run(
            FragmentationContext::new_continuous(600.0),
            &dom,
            viewport(600.0, 400.0),
            FakePageConfig::new(),
        )
        .expect("continuous layout must not fail");
        assert_eq!(pages.len(), 1, "continuous media is never paginated");
        assert!(
            !pages[0].items.is_empty(),
            "painted divs must produce display-list items — the rest of this \
             module's page-count assertions depend on it"
        );
    }
    #[test]
    fn tall_document_splits_into_multiple_pages() {
        // ~1000px of content, 200px pages.
        let pages = run(
            paged(600.0, 200.0),
            &doc(5),
            viewport(600.0, 400.0),
            FakePageConfig::new(),
        )
        .expect("paged layout must not fail");
        assert!(
            pages.len() >= 2,
            "1000px of content on 200px pages must paginate, got {} page(s)",
            pages.len()
        );
    }
    #[test]
    fn empty_document_still_yields_one_page() {
        // A document with nothing to paint has height 0 — the paginator must
        // still hand back a page rather than an empty vec (a zero-page PDF).
        let pages = run(
            paged(600.0, 200.0),
            &StyledDom::default(),
            viewport(600.0, 400.0),
            FakePageConfig::new(),
        )
        .expect("empty document must lay out");
        assert_eq!(pages.len(), 1, "a zero-height document is still one page");
    }
    // ---------------------------------------------------------------------
    // Numeric: degenerate page sizes (zero / negative / NaN / inf / MIN / MAX)
    // ---------------------------------------------------------------------
    #[test]
    fn zero_page_height_yields_a_single_page() {
        let pages = run(
            paged(600.0, 0.0),
            &doc(5),
            viewport(600.0, 400.0),
            FakePageConfig::new(),
        )
        .expect("a zero-height page must not fail layout");
        assert_eq!(
            pages.len(),
            1,
            "a page of height 0 cannot be filled — the slicer must bail out to \
             a single unpaginated page instead of dividing by zero"
        );
    }
    #[test]
    fn zero_page_size_in_both_axes_does_not_panic() {
        let pages = run(
            paged(0.0, 0.0),
            &doc(3),
            viewport(0.0, 0.0),
            FakePageConfig::new(),
        )
        .expect("a fully degenerate 0x0 page must not fail layout");
        assert_eq!(pages.len(), 1);
    }
    #[test]
    fn negative_page_height_yields_a_single_page() {
        let pages = run(
            paged(600.0, -500.0),
            &doc(5),
            viewport(600.0, 400.0),
            FakePageConfig::new(),
        )
        .expect("a negative page height must not fail layout");
        assert_eq!(
            pages.len(),
            1,
            "a negative page height must not produce a negative/infinite page count"
        );
    }
    #[test]
    fn nan_page_size_does_not_panic_and_yields_at_least_one_page() {
        // NaN slips past BOTH `<= 0.0` and `>= f32::MAX` guards (every NaN
        // comparison is false), so this is the case most likely to reach the
        // break-position math with a poisoned step.
        let pages = run(
            paged(f32::NAN, f32::NAN),
            &doc(5),
            viewport(600.0, 400.0),
            FakePageConfig::new(),
        )
        .expect("a NaN page size must not fail layout");
        assert_eq!(
            pages.len(),
            1,
            "a NaN page height cannot advance the break cursor, so the whole \
             document must stay on one page (and the break sort must not see a NaN)"
        );
    }
    #[test]
    fn infinite_page_height_yields_a_single_page() {
        let pages = run(
            paged(600.0, f32::INFINITY),
            &doc(5),
            viewport(600.0, 400.0),
            FakePageConfig::new(),
        )
        .expect("an infinite page height must not fail layout");
        assert_eq!(pages.len(), 1, "an infinitely tall page holds everything");
    }
    #[test]
    fn f32_max_page_height_yields_a_single_page() {
        // f32::MAX is the sentinel `FragmentationContext::Continuous` reports,
        // so a *paged* context carrying it must degrade to the same behaviour
        // rather than attempting MAX/step pages.
        let pages = run(
            paged(600.0, f32::MAX),
            &doc(5),
            viewport(600.0, 400.0),
            FakePageConfig::new(),
        )
        .expect("f32::MAX page height must not fail layout");
        assert_eq!(pages.len(), 1);
    }
    #[test]
    fn f32_min_page_height_yields_a_single_page() {
        // f32::MIN is the most-negative finite float, not the smallest positive.
        let pages = run(
            paged(f32::MIN, f32::MIN),
            &doc(5),
            viewport(600.0, 400.0),
            FakePageConfig::new(),
        )
        .expect("f32::MIN page size must not fail layout");
        assert_eq!(pages.len(), 1);
    }
    // ---------------------------------------------------------------------
    // Numeric: degenerate viewports
    // ---------------------------------------------------------------------
    #[test]
    fn negative_viewport_size_does_not_panic() {
        let pages = run(
            paged(600.0, 200.0),
            &doc(5),
            viewport(-100.0, -100.0),
            FakePageConfig::new(),
        )
        .expect("a negative viewport must not fail layout");
        assert!(!pages.is_empty(), "layout must always emit at least one page");
    }
    #[test]
    fn nan_viewport_does_not_panic() {
        let pages = run(
            paged(600.0, 200.0),
            &doc(5),
            viewport(f32::NAN, f32::NAN),
            FakePageConfig::new(),
        )
        .expect("a NaN viewport must not fail layout");
        assert!(!pages.is_empty(), "layout must always emit at least one page");
    }
    #[test]
    fn huge_viewport_does_not_panic() {
        // Paired with an f32::MAX page height on purpose: pagination short-circuits,
        // so this exercises the layout/display-list path at the numeric limit
        // without asking the slicer to walk MAX-sized content in finite steps.
        let result = run(
            paged(f32::MAX, f32::MAX),
            &doc(3),
            viewport(f32::MAX, f32::MAX),
            FakePageConfig::new(),
        );
        match result {
            Ok(pages) => assert_eq!(pages.len(), 1),
            // Failing cleanly at the numeric limit is acceptable; panicking is not.
            Err(e) => {
                let _ = e.to_string();
            }
        }
    }
    // ---------------------------------------------------------------------
    // Numeric: monotonicity of the page count
    // ---------------------------------------------------------------------
    #[test]
    fn shorter_pages_never_produce_fewer_pages() {
        let dom = doc(5);
        let vp = viewport(600.0, 400.0);
        let tall = page_count(paged(600.0, 400.0), &dom, vp);
        let short = page_count(paged(600.0, 100.0), &dom, vp);
        assert!(
            short >= tall,
            "halving the page height must not shrink the page count ({short} < {tall})"
        );
    }
    #[test]
    fn more_content_never_produces_fewer_pages() {
        let vp = viewport(600.0, 400.0);
        let frag = paged(600.0, 200.0);
        let few = page_count(frag, &doc(3), vp);
        let many = page_count(frag, &doc(12), vp);
        assert!(
            many >= few,
            "4x the content must not shrink the page count ({many} < {few})"
        );
    }
    // ---------------------------------------------------------------------
    // Headers / footers
    // ---------------------------------------------------------------------
    #[test]
    fn header_and_footer_taller_than_the_page_yield_a_single_page() {
        // header + footer >= page height leaves negative room for content.
        // Without `skip_first_page`, the first-page height goes <= 0 and the
        // slicer must bail out to one page rather than dividing the document
        // into a negative-height grid.
        let config = FakePageConfig::new()
            .with_header_page_numbers()
            .with_footer_page_numbers()
            .with_header_height(f32::MAX)
            .with_footer_height(f32::MAX);
        let pages = run(paged(600.0, 200.0), &doc(5), viewport(600.0, 400.0), config)
            .expect("an oversized header/footer must not fail layout");
        assert_eq!(
            pages.len(),
            1,
            "no content fits once the header/footer exceed the page — one page, not zero, \
             not an unbounded number"
        );
    }
    #[test]
    fn skip_first_page_with_sane_header_and_footer_still_paginates() {
        let config = FakePageConfig::new()
            .with_header_and_footer_page_numbers()
            .with_header_height(20.0)
            .with_footer_height(20.0)
            .skip_first_page(true);
        let pages = run(paged(600.0, 300.0), &doc(5), viewport(600.0, 400.0), config)
            .expect("paged layout with headers/footers must not fail");
        assert!(
            pages.len() >= 2,
            "1000px of content on 300px pages (260px usable after the first) must \
             paginate, got {} page(s)",
            pages.len()
        );
    }
    // ---------------------------------------------------------------------
    // Determinism / cache reuse / wrapper equivalence
    // ---------------------------------------------------------------------
    #[test]
    fn paged_layout_is_deterministic_across_fresh_runs() {
        let dom = doc(5);
        let vp = viewport(600.0, 400.0);
        let frag = paged(600.0, 200.0);
        let first = run(frag, &dom, vp, FakePageConfig::new()).expect("layout must not fail");
        let second = run(frag, &dom, vp, FakePageConfig::new()).expect("layout must not fail");
        assert_eq!(first.len(), second.len(), "page count must be deterministic");
        assert_eq!(
            item_counts(&first),
            item_counts(&second),
            "per-page item counts must be deterministic"
        );
    }
    #[test]
    fn reusing_a_warm_cache_reproduces_the_cold_result() {
        // Adversarial: the second call takes the incremental/early-exit path
        // through `compute_layout_with_fragmentation`. Same DOM, same viewport,
        // same page size => byte-identical pagination, or the cache is stale.
        let dom = doc(5);
        let vp = viewport(600.0, 400.0);
        let frag = paged(600.0, 200.0);
        let mut cache = LayoutCache::default();
        let mut fm = font_manager();
        let cold = run_with(&mut cache, &mut fm, frag, &dom, vp, FakePageConfig::new())
            .expect("cold layout must not fail");
        let warm = run_with(&mut cache, &mut fm, frag, &dom, vp, FakePageConfig::new())
            .expect("warm layout must not fail");
        assert_eq!(cold.len(), warm.len(), "cache reuse changed the page count");
        assert_eq!(
            item_counts(&cold),
            item_counts(&warm),
            "cache reuse changed the per-page item counts"
        );
    }
    #[test]
    fn a_reused_cache_relaid_out_with_a_different_dom_matches_a_cold_run() {
        // Adversarial: feed a cache warmed on a SHORT document a much longer
        // one. The reconciled result must equal what a cold cache produces —
        // page count must not depend on layout history.
        let vp = viewport(600.0, 400.0);
        let frag = paged(600.0, 200.0);
        let short = doc(2);
        let long = doc(9);
        let mut cache = LayoutCache::default();
        let mut fm = font_manager();
        let _ = run_with(&mut cache, &mut fm, frag, &short, vp, FakePageConfig::new())
            .expect("first layout must not fail");
        let reused = run_with(&mut cache, &mut fm, frag, &long, vp, FakePageConfig::new())
            .expect("relayout must not fail");
        let cold = page_count(frag, &long, vp);
        assert_eq!(
            reused.len(),
            cold,
            "a cache warmed on a 2-div document produced {} page(s) for the 9-div \
             document, but a cold cache produces {}",
            reused.len(),
            cold
        );
    }
    #[test]
    fn layout_document_paged_matches_its_documented_default_config() {
        // `layout_document_paged` is documented as `..._with_config` with
        // footer page numbers and no timing output. Assert that equivalence
        // holds, so the wrapper can't silently drift from the delegate.
        let dom = doc(5);
        let vp = viewport(600.0, 400.0);
        let frag = paged(600.0, 200.0);
        let mut cache = LayoutCache::default();
        let mut text_cache = TextLayoutCache::new();
        let mut fm = font_manager();
        let mut debug_messages = None;
        let loader = PathLoader::new();
        let via_wrapper = layout_document_paged(
            &mut cache,
            &mut text_cache,
            frag,
            &dom,
            vp,
            &mut fm,
            &BTreeMap::new(),
            &mut debug_messages,
            None,
            &RendererResources::default(),
            IdNamespace(0),
            DomId::ROOT_ID,
            |bytes: std::sync::Arc<rust_fontconfig::FontBytes>, index: usize| {
                loader.load_font_shared(bytes, index)
            },
            &ImageCache::default(),
            time_fn(),
        )
        .expect("layout_document_paged must not fail");
        let via_config = run(
            frag,
            &dom,
            vp,
            FakePageConfig::new().with_footer_page_numbers(),
        )
        .expect("layout_document_paged_with_config must not fail");
        assert_eq!(via_wrapper.len(), via_config.len());
        assert_eq!(item_counts(&via_wrapper), item_counts(&via_config));
    }
    // ---------------------------------------------------------------------
    // compute_layout_with_fragmentation (private)
    // ---------------------------------------------------------------------
    #[test]
    fn compute_layout_with_fragmentation_populates_the_cache() {
        let dom = doc(3);
        let vp = viewport(600.0, 400.0);
        let mut cache = LayoutCache::default();
        let mut frag = paged(600.0, 200.0);
        compute(&mut cache, &mut frag, &dom, vp).expect("layout must not fail");
        assert!(cache.tree.is_some(), "the layout tree must be cached");
        assert!(
            tree_node_count(&cache) >= 4,
            "<body> plus 3 <div>s is at least 4 layout nodes"
        );
        assert!(
            !cache.calculated_positions.is_empty(),
            "positions must be cached alongside the tree"
        );
        assert_eq!(cache.viewport, Some(vp), "the layout viewport must be recorded");
        assert!(
            crate::solver3::pos_get(&cache.calculated_positions, 0).is_some(),
            "the root node must have a position"
        );
    }
    #[test]
    fn compute_layout_with_fragmentation_is_idempotent() {
        let dom = doc(3);
        let vp = viewport(600.0, 400.0);
        let mut cache = LayoutCache::default();
        let mut frag = paged(600.0, 200.0);
        compute(&mut cache, &mut frag, &dom, vp).expect("first layout must not fail");
        let nodes = tree_node_count(&cache);
        let positions = cache.calculated_positions.clone();
        // Second pass takes the "cache is clean" early-exit branch.
        compute(&mut cache, &mut frag, &dom, vp).expect("second layout must not fail");
        assert_eq!(tree_node_count(&cache), nodes, "relayout changed the tree size");
        assert_eq!(
            cache.calculated_positions, positions,
            "relayout of an unchanged DOM moved nodes"
        );
    }
    #[test]
    fn compute_layout_with_fragmentation_tree_shape_is_independent_of_pagination() {
        // Layout is continuous; pages are sliced afterwards by Y position. So a
        // paged context must not add, drop, or split any layout node.
        let dom = doc(4);
        let vp = viewport(600.0, 400.0);
        let mut continuous_cache = LayoutCache::default();
        let mut continuous = FragmentationContext::new_continuous(600.0);
        compute(&mut continuous_cache, &mut continuous, &dom, vp)
            .expect("continuous layout must not fail");
        let mut paged_cache = LayoutCache::default();
        let mut paged_ctx = paged(600.0, 50.0);
        compute(&mut paged_cache, &mut paged_ctx, &dom, vp).expect("paged layout must not fail");
        assert_eq!(
            tree_node_count(&continuous_cache),
            tree_node_count(&paged_cache),
            "fragmentation must not change the layout tree"
        );
        assert_eq!(
            continuous_cache.calculated_positions, paged_cache.calculated_positions,
            "fragmentation must not move nodes — pages are sliced from the same \
             continuous canvas"
        );
    }
    #[test]
    fn compute_layout_with_fragmentation_survives_degenerate_viewports() {
        let dom = doc(3);
        for vp in [
            viewport(0.0, 0.0),
            viewport(-1.0, -1.0),
            viewport(f32::NAN, f32::NAN),
            viewport(f32::MIN, f32::MIN),
        ] {
            let mut cache = LayoutCache::default();
            let mut frag = paged(600.0, 200.0);
            compute(&mut cache, &mut frag, &dom, vp)
                .unwrap_or_else(|e| panic!("viewport {vp:?} failed layout: {e}"));
            assert!(
                cache.tree.is_some(),
                "viewport {vp:?} must still produce a layout tree"
            );
        }
    }
    #[test]
    fn compute_layout_with_fragmentation_survives_degenerate_page_sizes() {
        let dom = doc(3);
        let vp = viewport(600.0, 400.0);
        for mut frag in [
            paged(0.0, 0.0),
            paged(600.0, -1.0),
            paged(f32::NAN, f32::NAN),
            paged(f32::INFINITY, f32::INFINITY),
            paged(f32::MAX, f32::MAX),
            paged(f32::MIN, f32::MIN),
        ] {
            let mut cache = LayoutCache::default();
            compute(&mut cache, &mut frag, &dom, vp)
                .unwrap_or_else(|e| panic!("page size {frag:?} failed layout: {e}"));
            assert!(
                cache.tree.is_some(),
                "page size {frag:?} must still produce a layout tree"
            );
        }
    }
    // ==================================================================
    // Sectioned pagination — fragmentainer WIDTH re-wrap
    // ==================================================================
    fn setup(w: f32, h: f32) -> crate::solver3::pagination::PageSetup {
        crate::solver3::pagination::PageSetup {
            page_size: LogicalSize::new(w, h),
            margins: crate::solver3::pagination::PageMargins {
                top: 0.0,
                right: 0.0,
                bottom: 0.0,
                left: 0.0,
            },
            header_footer: Default::default(),
        }
    }
    #[test]
    fn width_sections_partition_by_content_width() {
        use crate::solver3::pagination::PageSequence;
        let mut seq = PageSequence::uniform(setup(600.0, 400.0));
        seq.overrides.insert(0, setup(300.0, 400.0));
        let sections = seq.width_sections(64);
        assert_eq!(sections.len(), 2);
        assert_eq!(sections[0].first_page, 0);
        assert_eq!(sections[0].page_count, Some(1));
        assert!((sections[0].content_width - 300.0).abs() < 0.5);
        assert_eq!(sections[1].first_page, 1);
        assert_eq!(sections[1].page_count, None, "tail is open-ended");
        assert!((sections[1].content_width - 600.0).abs() < 0.5);
        // Uniform sequence: one open-ended section.
        let uni = PageSequence::uniform(setup(600.0, 400.0)).width_sections(64);
        assert_eq!(uni.len(), 1);
        assert_eq!(uni[0].page_count, None);
    }
    /// The re-wrap acceptance: content whose height DEPENDS on the page
    /// width (aspect-ratio boxes — no fonts needed) paginates differently
    /// once the tail re-measures at the wider section's width.
    #[test]
    fn sectioned_pagination_rewraps_the_tail_at_the_new_width() {
        use crate::solver3::pagination::PageSequence;
        fn aspect_doc(n: usize) -> StyledDom {
            let children: Vec<Dom> = (0..n).map(|_| Dom::create_div()).collect();
            let mut dom = Dom::create_body().with_children(children.into());
            let css = azul_css::parser2::new_from_str(
                "div { width: 100%; aspect-ratio: 2; background-color: red; }",
            )
            .0;
            StyledDom::create(&mut dom, css)
        }
        let loader = PathLoader::new();
        let font_loader = |bytes: std::sync::Arc<rust_fontconfig::FontBytes>, index: usize| {
            loader.load_font_shared(bytes, index)
        };
        let mut font_manager: FontManager<TestFont> =
            FontManager::new(FcFontCache::default()).unwrap();
        let rr = RendererResources::default();
        let ic = ImageCache::default();
        // Page 0: 300 wide → divs 150 tall. Later pages: 600 wide → 300 tall.
        let mut seq = PageSequence::uniform(setup(600.0, 400.0));
        seq.overrides.insert(0, setup(300.0, 400.0));
        let sectioned = compute_sectioned_pagination(
            &aspect_doc(8),
            400.0,
            &mut font_manager,
            &rr,
            IdNamespace(0),
            DomId::ROOT_ID,
            font_loader,
            &FakePageConfig::new(),
            &seq,
            &ic,
            time_fn(),
        )
        .expect("sectioned pagination");
        assert_eq!(sectioned.sections.len(), 2, "narrow first page + wide tail");
        assert!((sectioned.sections[1].content_width - 600.0).abs() < 0.5);
        // The same document on a UNIFORM 300-wide sequence for comparison:
        // its divs stay 150 tall everywhere.
        let uniform = compute_sectioned_pagination(
            &aspect_doc(8),
            400.0,
            &mut font_manager,
            &rr,
            IdNamespace(0),
            DomId::ROOT_ID,
            font_loader,
            &FakePageConfig::new(),
            &PageSequence::uniform(setup(300.0, 400.0)),
            &ic,
            time_fn(),
        )
        .expect("uniform pagination");
        assert_eq!(uniform.sections.len(), 1);
        // 600-wide divs are 300 tall → ~1/page; 300-wide divs are 150 tall
        // → ~2/page. If the tail had NOT re-measured at 600, the totals
        // would match — more pages proves the re-wrap happened.
        assert!(
            sectioned.page_count() > uniform.page_count(),
            "sectioned={} uniform={}: the tail must RE-MEASURE at the wide width",
            sectioned.page_count(),
            uniform.page_count()
        );
    }
}
/// A page break mapped to a STRUCTURAL position in the DOM — the keystone of
/// the DOM-materialized-breaks editor architecture: the estimator computes
/// break Y coordinates, the application inserts its break nodes at DOM
/// positions. This type carries both.
#[derive(Debug, Clone, PartialEq)]
pub struct StructuralBreak {
    /// Document-space Y where the page ends (same value as the
    /// corresponding [`PageBreakPosition::y`](crate::solver3::page_breaks::PageBreakPosition)).
    pub y: f32,
    /// Why the break happened.
    pub kind: crate::solver3::page_breaks::BreakKind,
    /// For forced breaks: the node whose break property caused it.
    pub causing_node: Option<NodeId>,
    /// Root-to-node child-index path of the first block-level box at/after
    /// `y` — the position where a break node inserted BEFORE the addressed
    /// node reproduces this page boundary structurally
    /// (`azul_core::dom::Dom` child indices, consumable by
    /// `split_dom_at_path`). `None` when no block-level box sits at/after
    /// `y` (a break in trailing whitespace / past the last block).
    pub path: Option<Vec<u32>>,
    /// Ledger #2 (line-granular option): when the addressed block is an
    /// IFC whose LINE BOXES straddle `y`, the (run, byte) of the first
    /// line that moves to the next page — the app can split the paragraph
    /// text there instead of moving the whole block. `None` = block
    /// boundary (the entire addressed block moves), the v1 contract.
    pub line_start: Option<azul_core::selection::ContentIndex>,
}
/// Map every break of a [`PaginationInfo`](crate::solver3::page_breaks::PaginationInfo)
/// to its structural DOM position, using the layout tree and positions that
/// [`compute_document_pagination`] left in `cache`.
///
/// Call this immediately after [`compute_document_pagination`] with the SAME
/// `cache` and `styled_dom` — the mapping reads `cache.tree` and
/// `cache.calculated_positions`, which every further layout pass may
/// invalidate. Returns `None` when the cache holds no tree (pagination was
/// never computed, or the cache was cleared).
#[cfg(feature = "text_layout")]
#[must_use]
72
pub fn pagination_to_dom_breaks(
72
    cache: &LayoutCache,
72
    styled_dom: &StyledDom,
72
    pagination: &crate::solver3::page_breaks::PaginationInfo,
72
) -> Option<Vec<StructuralBreak>> {
72
    let tree = cache.tree.as_ref()?;
72
    let positions = &cache.calculated_positions;
    Some(
72
        pagination
72
            .breaks
72
            .iter()
72
            .map(|b| StructuralBreak {
162
                y: b.y,
162
                kind: b.kind,
162
                causing_node: b.causing_node,
162
                path: spine_path_at_y(tree, positions, styled_dom, b.y),
162
                line_start: spine_line_start_at_y(tree, positions, styled_dom, b.y),
162
            })
72
            .collect(),
    )
72
}
/// The DEEPEST block-level box whose border-box vertically CONTAINS `y`
/// (the spine path addresses the first block AT/AFTER `y`; a mid-block
/// break's line lookup needs the box the break lands IN).
162
fn spine_layout_hit_at_y(
162
    tree: &crate::solver3::layout_tree::LayoutTree,
162
    positions: &crate::solver3::PositionVec,
162
    styled_dom: &StyledDom,
162
    y: f32,
162
) -> Option<(usize, f32)> {
162
    let hierarchy = styled_dom.node_hierarchy.as_container();
459
    let depth_of = |mut n: NodeId| -> u32 {
459
        let mut d = 0;
891
        while let Some(p) = hierarchy
891
            .get(n)
891
            .and_then(azul_core::styled_dom::NodeHierarchyItem::parent_id)
432
        {
432
            d += 1;
432
            n = p;
432
        }
459
        d
459
    };
162
    let mut best: Option<(u32, usize, f32)> = None;
1053
    for idx in 0..tree.nodes.len() {
1053
        let Some(node) = tree.get(LayoutNodeId::new(idx)) else { continue };
1053
        let Some(dom_id) = node.dom_node_id else { continue };
1053
        if !crate::solver3::layout_tree::is_block_level(styled_dom, dom_id) {
378
            continue;
675
        }
675
        let Some(pos) = crate::solver3::pos_get(positions, idx) else {
            continue;
        };
675
        let h = node.used_size.map_or(0.0, |sz| sz.height);
675
        if !(pos.y - 0.5 <= y && y < pos.y + h - 0.5) {
216
            continue;
459
        }
459
        let d = depth_of(dom_id);
459
        if best.as_ref().is_none_or(|(bd, ..)| d > *bd) {
459
            best = Some((d, idx, pos.y));
459
        }
    }
162
    best.map(|(_, idx, top)| (idx, top))
162
}
/// Ledger #2: the line-granular refinement of [`spine_path_at_y`]. When the
/// block the break lands IN is an IFC with line boxes on both sides of `y`,
/// returns the (run, byte) starting the first line at/after `y` — measured
/// in the block's content box. `None` when the break sits at a block
/// boundary, the block has no inline layout, or every line is below `y`
/// (then the whole block moves — the block-granular contract).
162
#[must_use] pub fn spine_line_start_at_y(
162
    tree: &crate::solver3::layout_tree::LayoutTree,
162
    positions: &crate::solver3::PositionVec,
162
    styled_dom: &StyledDom,
162
    y: f32,
162
) -> Option<azul_core::selection::ContentIndex> {
    use crate::text3::cache::ShapedItem;
162
    let hierarchy = styled_dom.node_hierarchy.as_container();
    // Re-find the spine block the path addresses (same selection rule).
162
    let (layout_idx, node_top) = spine_layout_hit_at_y(tree, positions, styled_dom, y)?;
162
    let node = tree.get(LayoutNodeId::new(layout_idx))?;
162
    let bp = node.box_props.unpack();
162
    let content_top = node_top + bp.padding.top + bp.border.top;
162
    let rel_y = y - content_top;
162
    if rel_y <= 0.5 {
36
        return None; // block-boundary break: whole block moves
126
    }
126
    let layout = tree.get_inline_layout_for_node(layout_idx)?;
    // (d6h) Dense-first: the stored sparse may be the retirement
    // sentinel. LineRecords give the tops in O(lines); the sparse arm
    // remains as the flag-off path and the verify oracle.
126
    if let Some(dense) = tree.get_dense_for_node(layout_idx) {
126
        if !dense.clusters.is_empty() {
126
            let result = spine_line_start_dense(dense, rel_y);
126
            if crate::solver3::layout_tree::dense_text_mode() == 2 {
                let sparse = spine_line_start_sparse(layout, rel_y);
                assert_eq!(
                    result, sparse,
                    "d6h verify: spine_line_start dense vs sparse diverged at rel_y {rel_y}"
                );
126
            }
126
            return result;
        }
    }
    spine_line_start_sparse(layout, rel_y)
162
}
/// The sparse fold of [`spine_line_start_at_y`] — the pre-d6h body,
/// kept as the flag-off path and the verify oracle.
fn spine_line_start_sparse(
    layout: &crate::text3::cache::UnifiedLayout,
    rel_y: f32,
) -> Option<azul_core::selection::ContentIndex> {
    use crate::text3::cache::ShapedItem;
    // The line the break lands ON moves to the next page (a sliced line is
    // atomic; a break AT a line top moves that line). Identify it purely by
    // LINE TOPS — per-item heights are not trustworthy on this path — as
    // the line with the largest top not above the break.
    let mut line_tops: BTreeMap<usize, f32> =
        BTreeMap::new();
    for item in &layout.items {
        let entry = line_tops.entry(item.line_index).or_insert(f32::MAX);
        *entry = entry.min(item.position.y);
    }
    let straddler = line_tops
        .iter()
        .filter(|(_, top)| **top <= rel_y + 0.5)
        .max_by(|a, b| a.1.total_cmp(b.1))
        .map(|(line, _)| *line)?;
    // A first-line hit means the whole block moves: block-granular None.
    if straddler == 0 {
        return None;
    }
    let mut best: Option<azul_core::selection::ContentIndex> = None;
    for item in &layout.items {
        if item.line_index != straddler {
            continue;
        }
        // Clusters carry their identity in `source_cluster_id` (the same
        // GraphemeClusterId the cursor/editing pipeline keys on);
        // `source_content_index` is not populated on the paged shaping
        // path. Non-cluster items fall back to their ContentIndex.
        let src = match &item.item {
            ShapedItem::Cluster(c) => azul_core::selection::ContentIndex {
                run_index: c.source_cluster_id.source_run,
                item_index: c.source_cluster_id.start_byte_in_run,
            },
            ShapedItem::CombinedBlock { source, .. }
            | ShapedItem::Object { source, .. }
            | ShapedItem::Tab { source, .. }
            | ShapedItem::Break { source, .. } => *source,
        };
        if best
            .as_ref()
            .is_none_or(|s| (src.run_index, src.item_index) < (s.run_index, s.item_index))
        {
            best = Some(src);
        }
    }
    best
}
/// (d6h) The dense twin of [`spine_line_start_sparse`]: line tops from
/// `LineRecord` in O(lines). A line's sparse "top" is the MIN per-item y
/// — on mixed-size lines that is `shared_baseline - max ascent over the
/// line's runs`, reconstructed here exactly as the expander does.
126
fn spine_line_start_dense(
126
    dense: &crate::text3::dense::DenseText,
126
    rel_y: f32,
126
) -> Option<azul_core::selection::ContentIndex> {
    use crate::text3::dense::DenseText;
7686
    let line_top = |l: &crate::text3::dense::LineRecord| -> f32 {
7686
        let Some(first_run) = dense.run_of(l.clusters.0) else {
            return l.baseline_y;
        };
7686
        let base = l.baseline_y + DenseText::resolved_run_ascent(first_run);
7686
        let mut top = f32::MAX;
7686
        let mut ci = l.clusters.0;
15372
        while ci < l.clusters.1 {
7686
            let Some(r) = dense.run_of(ci) else { break };
7686
            top = top.min(base - DenseText::resolved_run_ascent(r));
7686
            ci = r.clusters.end.max(ci + 1);
        }
7686
        if top == f32::MAX { l.baseline_y } else { top }
7686
    };
126
    let straddler = dense
126
        .lines
126
        .iter()
3690
        .filter(|l| line_top(l) <= rel_y + 0.5)
1998
        .max_by(|a, b| line_top(a).total_cmp(&line_top(b)))?;
126
    if straddler.source_index == 0 {
72
        return None;
54
    }
54
    let mut best: Option<azul_core::selection::ContentIndex> = None;
1566
    for ci in straddler.clusters.0..straddler.clusters.1 {
1566
        let c = &dense.clusters[ci as usize];
1566
        let run = dense.run_of(ci)?;
1566
        let src = azul_core::selection::ContentIndex {
1566
            run_index: run.source_run,
1566
            item_index: c.start_byte,
1566
        };
1566
        if best
1566
            .as_ref()
1566
            .is_none_or(|s| (src.run_index, src.item_index) < (s.run_index, s.item_index))
54
        {
54
            best = Some(src);
1512
        }
    }
54
    best
126
}
/// One fragmentainer's outcome from [`layout_document_tokenized`].
#[derive(Debug, Clone)]
pub struct TokenizedPage {
    /// Block-size of the content laid INTO this fragmentainer (the root's
    /// fitted used height for this pass).
    pub content_block_size: f32,
    /// The outgoing resume token (`None` = the document finished here).
    pub outgoing: Option<crate::solver3::break_token::BreakToken>,
    /// This page's display list, GENERATED from the fragment pass (never
    /// sliced): only nodes laid on this page have assigned positions —
    /// everything else sits at the unassigned sentinel and is dropped by
    /// `push_item`. Page-local coordinates (the fragmentainer origin is 0).
    pub display_list: DisplayList,
}
/// K30b part 2 / K30c skeleton: the NG-style page loop. Lays the document
/// out one fragmentainer at a time — page N's outgoing token is page N+1's
/// incoming token; layout re-descends the tree each page, skipping finished
/// subtrees via the token (design doc §4.5). No display lists yet (that is
/// the rest of K30c); the output pins the token algebra: progress,
/// conservation, nested resume.
///
/// # Errors
/// Propagates layout errors; the internal no-progress guard turns the
/// NG infinite-loop class into loop termination instead.
#[allow(clippy::too_many_arguments)]
/// K34 — token convergence: what a previous tokenized run left behind, so
/// an incremental re-pagination can stop as soon as it re-synchronizes with
/// it.
///
/// Tokens are owned value types with reliable `Eq`, so the invariant is
/// exact: **if the token entering page N is unchanged, pages ≥ N are
/// unchanged.** An edit therefore only has to re-lay pages until an
/// outgoing token matches the cached one for that page; everything after
/// is reused verbatim. Typing converges in ≤ 2 pages, which is what makes
/// live repagination affordable on a long document.
#[derive(Debug, Clone, Default)]
pub struct TokenCache {
    /// Per-page outgoing token from the previous run (`None` = the document
    /// ended on that page).
    pub outgoing: Vec<Option<crate::solver3::break_token::BreakToken>>,
    /// The pages themselves, reused verbatim from the convergence point on.
    pub pages: Vec<TokenizedPage>,
}
/// Outcome of an incremental (convergence-aware) pagination.
#[derive(Debug)]
pub struct IncrementalPagination {
    /// The full page list — freshly laid pages followed by any reused tail.
    pub pages: Vec<TokenizedPage>,
    /// How many pages this run actually laid out. `pages.len() - laid_out`
    /// is what convergence saved.
    pub laid_out: usize,
    /// The page index at which the run re-synchronized with the cache, if it
    /// did (`None` = it ran to the end of the document).
    pub converged_at: Option<usize>,
}
/// What the shared page loop returns (see `layout_document_tokenized_from`).
struct PageLoopOutcome {
    pages: Vec<TokenizedPage>,
    laid_out: usize,
    converged_at: Option<usize>,
}
/// The public full-document entry point: lay every page from the start.
#[allow(clippy::too_many_arguments)]
11
pub fn layout_document_tokenized<T, F>(
11
    cache: &mut LayoutCache,
11
    text_cache: &mut TextLayoutCache,
11
    new_dom: &StyledDom,
11
    viewport: LogicalRect,
11
    font_manager: &mut crate::font_traits::FontManager<T>,
11
    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
11
    image_cache: &azul_core::resources::ImageCache,
11
    get_system_time_fn: azul_core::task::GetSystemTimeCallback,
11
    font_loader: F,
11
    renderer_resources: &RendererResources,
11
    id_namespace: azul_core::resources::IdNamespace,
11
    dom_id: DomId,
11
    page_height: f32,
11
    max_pages: usize,
11
) -> Result<Vec<TokenizedPage>>
11
where
11
    T: ParsedFontTrait + Sync + 'static,
11
    F: Fn(
11
            std::sync::Arc<rust_fontconfig::FontBytes>,
11
            usize,
11
        ) -> std::result::Result<T, crate::text3::cache::LayoutError>
11
        + Copy,
{
11
    Ok(layout_document_tokenized_from(
11
        cache, text_cache, new_dom, viewport, font_manager, debug_messages,
11
        image_cache, get_system_time_fn, font_loader, renderer_resources,
11
        id_namespace, dom_id, page_height, max_pages, None, None,
    )?
    .pages)
11
}
/// The shared page loop. `start_token` resumes mid-document (K34 incremental
/// re-pagination); `converge_against` lets it stop as soon as it
/// re-synchronizes with a previous run.
#[allow(clippy::too_many_arguments)]
11
fn layout_document_tokenized_from<T, F>(
11
    cache: &mut LayoutCache,
11
    text_cache: &mut TextLayoutCache,
11
    new_dom: &StyledDom,
11
    viewport: LogicalRect,
11
    font_manager: &mut crate::font_traits::FontManager<T>,
11
    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
11
    image_cache: &azul_core::resources::ImageCache,
11
    get_system_time_fn: azul_core::task::GetSystemTimeCallback,
11
    font_loader: F,
11
    renderer_resources: &RendererResources,
11
    id_namespace: azul_core::resources::IdNamespace,
11
    dom_id: DomId,
11
    page_content_height: f32,
11
    max_pages: usize,
11
    start_token: Option<crate::solver3::break_token::BreakToken>,
11
    converge_against: Option<(&TokenCache, usize)>,
11
) -> Result<PageLoopOutcome>
11
where
11
    T: ParsedFontTrait + Sync + 'static,
11
    F: Fn(
11
        std::sync::Arc<rust_fontconfig::FontBytes>,
11
        usize,
11
    ) -> std::result::Result<T, crate::text3::cache::LayoutError>,
{
    use crate::solver3::break_token::{token_fingerprint, BreakToken};
11
    let mut laid_out: usize = 0;
11
    let mut converged_at: Option<usize> = None;
    use crate::solver3::cache::{calculate_layout_for_subtree_fragment, ComputeMode};
    use crate::solver3::fc::FragmentainerSpace;
    // 1. Build the tree + shape text once via a CONTINUOUS pass (the page
    // loop re-descends this structure; fonts resolve the same way the
    // paged estimator does).
11
    let mut frag = FragmentationContext::new_continuous(viewport.size.width);
    {
        // Font resolution identical to the paged path.
        use crate::solver3::getters::{
            collect_and_resolve_font_chains_with_registration, collect_font_ids_from_chains,
            compute_fonts_to_load, load_fonts_from_disk,
        };
11
        let _p = crate::probe::Probe::span("font_chain_resolve");
        // SKIP THE RESOLVER when this DOM asks for the same font stacks the
        // manager already resolved. `LayoutWindow` has done this since the
        // beginning (window.rs, `font_requirements_unchanged`) via a rolling
        // hash of the compact cache's `prev_font_hashes`; the pagination
        // entry points did not — and worse, called the plain
        // `set_font_chain_cache`, which CLEARS the recorded signature, so
        // even a caller reusing one FontManager re-resolved a 160-family
        // chain on EVERY pagination (measured 8 ms/call, ~8% of a warm one).
11
        let font_stacks_sig = new_dom
11
            .css_property_cache
11
            .ptr
11
            .compact_cache
11
            .as_ref()
11
            .map(|cc| {
11
                let mut h: u64 = 0xcbf2_9ce4_8422_2325;
161
                for &fh in &cc.prev_font_hashes {
150
                    h = h.rotate_left(13) ^ fh;
150
                    h = h.wrapping_mul(0x0100_0000_01b3);
150
                }
11
                h
11
            });
11
        let font_requirements_unchanged = font_stacks_sig.is_some()
11
            && font_stacks_sig == font_manager.last_resolved_font_stacks_sig
            && !font_manager.font_chain_cache.is_empty();
11
        if !font_requirements_unchanged {
11
            let _p = crate::probe::Probe::span("font_chain_resolve");
11
            let platform = azul_css::system::Platform::current();
11
            let chains = collect_and_resolve_font_chains_with_registration(
11
                new_dom, &font_manager.fc_cache, font_manager, &platform,
            );
11
            let required = collect_font_ids_from_chains(&chains);
11
            let loaded = font_manager.get_loaded_font_ids();
11
            let to_load = compute_fonts_to_load(&required, &loaded);
11
            if !to_load.is_empty() {
11
                let res = load_fonts_from_disk(&to_load, &font_manager.fc_cache, &font_loader);
11
                font_manager.insert_fonts(res.loaded);
11
            }
11
            font_manager
11
                .set_font_chain_cache_with_sig(chains.into_fontconfig_chains(), font_stacks_sig);
        }
    }
11
    compute_layout_with_fragmentation(
11
        cache,
11
        text_cache,
11
        &mut frag,
11
        new_dom,
11
        viewport,
11
        font_manager,
11
        debug_messages,
11
        image_cache,
11
        get_system_time_fn,
        false,
    )?;
    // 2. The page loop.
11
    let mut pages: Vec<TokenizedPage> = Vec::new();
11
    let mut incoming: Option<BreakToken> = start_token;
27
    for page_idx in 0..max_pages {
27
        let resume = match incoming.as_ref() {
11
            None => None,
16
            Some(BreakToken::Block(b)) => Some(b),
            // The ROOT is a block box; an inline token cannot reach here.
            Some(BreakToken::Inline(_)) => None,
        };
27
        let space = FragmentainerSpace {
27
            remaining_block_extent: page_content_height,
27
            next_fragmentainer_extent: page_content_height,
27
            is_first: page_idx == 0 && incoming.is_none(),
27
            resume,
        };
27
        let tree = cache.tree.as_mut().ok_or(LayoutError::InvalidTree)?;
27
        let mut counter_values = cache.counters.clone();
27
        let empty_text_selections: BTreeMap<DomId, TextSelection> = BTreeMap::new();
27
        let mut ctx = LayoutContext {
27
            style_cache: Default::default(),
27
            scrollbar_style_cache: core::cell::RefCell::new(std::collections::HashMap::new()),
27
            styled_dom: new_dom,
27
            font_manager: &*font_manager,
27
            text_selections: &empty_text_selections,
27
            debug_messages,
27
            counters: &mut counter_values,
27
            viewport_size: viewport.size,
27
            fragmentation_context: None,
27
            reflowed_ifcs: std::collections::BTreeSet::new(),
27
            cursor_is_visible: false,
27
            cursor_locations: Vec::new(),
27
            preedit_text: None,
27
            cache_map: std::mem::take(&mut cache.cache_map),
27
            image_cache,
27
            content_overlay: None,
27
            system_style: None,
27
            get_system_time_fn,
27
        };
27
        let mut outgoing: Option<BreakToken> = None;
        // Positions pre-filled with the UNASSIGNED sentinel: only nodes the
        // fragment pass actually lays on THIS page receive positions; the
        // display-list builder drops everything else (its existing
        // unassigned-position guard) — pages are generated, never sliced.
27
        let node_count = tree.nodes.len();
27
        let mut page_positions: crate::solver3::PositionVec =
27
            alloc::vec![crate::solver3::POSITION_UNSET; node_count];
27
        let mut tmp_scrollbars = false;
27
        let mut tmp_floats = std::collections::HashMap::new();
27
        let result = calculate_layout_for_subtree_fragment(
27
            &mut ctx,
27
            tree,
27
            text_cache,
            0, // the root layout node
27
            LogicalPosition::zero(),
27
            viewport.size,
27
            &mut page_positions,
27
            &mut tmp_scrollbars,
27
            &mut tmp_floats,
27
            ComputeMode::PerformLayout,
27
            Some(space),
27
            Some(&mut outgoing),
        );
27
        result?;
        // The ROOT box itself sits at the fragmentainer origin (its
        // children got positions from Pass 2; the root's own position is
        // the caller's job on the normal path).
27
        crate::solver3::pos_set(&mut page_positions, 0, LogicalPosition::zero());
        // Generate THIS page's display list from the fragment positions.
27
        let display_list = {
27
            let tree_ref: &_ = tree;
27
            crate::solver3::display_list::generate_display_list(
27
                &mut ctx,
27
                tree_ref,
27
                &page_positions,
27
                &BTreeMap::new(),
27
                &cache.scroll_ids,
27
                None,
27
                renderer_resources,
27
                id_namespace,
27
                dom_id,
            )?
        };
27
        cache.cache_map = std::mem::take(&mut ctx.cache_map);
27
        let content = cache
27
            .tree
27
            .as_ref()
27
            .and_then(|t| t.get(LayoutNodeId::new(0)))
27
            .and_then(|n| n.used_size)
27
            .map_or(0.0, |sz| sz.height);
        // PROGRESS GUARD (the NG infinite-loop class): an outgoing token
        // identical to the incoming one means the page consumed nothing.
27
        let stalled = match (&incoming, &outgoing) {
6
            (Some(a), Some(b)) => {
6
                token_fingerprint(a) == token_fingerprint(b) && a == b
            }
21
            _ => false,
        };
27
        pages.push(TokenizedPage {
27
            content_block_size: content,
27
            outgoing: outgoing.clone(),
27
            display_list,
27
        });
27
        laid_out += 1;
27
        if stalled || outgoing.is_none() {
11
            break;
16
        }
        // K34 CONVERGENCE. Tokens are value types with reliable `Eq`: if the
        // token leaving this page equals the one that left the SAME page
        // last time, every later page receives an identical input and is
        // therefore identical. Splice the cached tail in and stop — this is
        // what turns "repaginate the document" into "repaginate two pages".
16
        if let Some((cached, base)) = converge_against {
            let abs_page = base + page_idx;
            if let Some(cached_outgoing) = cached.outgoing.get(abs_page) {
                let same = match (cached_outgoing, &outgoing) {
                    (Some(a), Some(b)) => {
                        token_fingerprint(a) == token_fingerprint(b) && a == b
                    }
                    (None, None) => true,
                    _ => false,
                };
                if same && abs_page + 1 < cached.pages.len() {
                    pages.extend(cached.pages[abs_page + 1..].iter().cloned());
                    converged_at = Some(abs_page);
                    break;
                }
            }
16
        }
16
        incoming = outgoing;
    }
11
    Ok(PageLoopOutcome { pages, laid_out, converged_at })
11
}
/// K34: re-paginate from `first_dirty_page`, stopping as soon as the run
/// re-synchronizes with `cache`.
///
/// The caller supplies the page the edit dirtied (`page_of_y` on the
/// chokepoint's dirty extent) and the previous run's [`TokenCache`]. Pages
/// before `first_dirty_page` are reused untouched — their incoming tokens
/// predate the edit and are therefore still valid — and the loop resumes
/// from that page's cached incoming token. After each freshly laid page the
/// outgoing token is compared against the cached one for the same index: on
/// equality every later page is spliced in verbatim and the run stops.
///
/// Falls back to a full run whenever the cache cannot be trusted (empty,
/// or `first_dirty_page` beyond it), so a caller can always call this.
///
/// # Errors
///
/// Propagates layout failures from the underlying page loop.
#[allow(clippy::too_many_arguments)]
pub fn layout_document_tokenized_incremental<T, F>(
    cache: &mut LayoutCache,
    text_cache: &mut TextLayoutCache,
    new_dom: &StyledDom,
    viewport: LogicalRect,
    font_manager: &mut crate::font_traits::FontManager<T>,
    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
    image_cache: &azul_core::resources::ImageCache,
    get_system_time_fn: azul_core::task::GetSystemTimeCallback,
    font_loader: F,
    renderer_resources: &RendererResources,
    id_namespace: azul_core::resources::IdNamespace,
    dom_id: DomId,
    page_height: f32,
    max_pages: usize,
    token_cache: &TokenCache,
    first_dirty_page: usize,
) -> Result<IncrementalPagination>
where
    T: ParsedFontTrait + Sync + 'static,
    F: Fn(
            std::sync::Arc<rust_fontconfig::FontBytes>,
            usize,
        ) -> std::result::Result<T, crate::text3::cache::LayoutError>
        + Copy,
{
    // A full run whenever the cache cannot help.
    let usable = !token_cache.pages.is_empty()
        && token_cache.outgoing.len() == token_cache.pages.len()
        && first_dirty_page < token_cache.pages.len();
    if !usable {
        let pages = layout_document_tokenized(
            cache, text_cache, new_dom, viewport, font_manager, debug_messages,
            image_cache, get_system_time_fn, font_loader, renderer_resources,
            id_namespace, dom_id, page_height, max_pages,
        )?;
        let laid_out = pages.len();
        return Ok(IncrementalPagination { pages, laid_out, converged_at: None });
    }
    // Pages before the dirty one are untouched by definition.
    let mut pages: Vec<TokenizedPage> = token_cache.pages[..first_dirty_page].to_vec();
    // Resume from the token that ENTERED the dirty page: the previous
    // page's outgoing, which predates the edit.
    let resume_token = if first_dirty_page == 0 {
        None
    } else {
        token_cache.outgoing[first_dirty_page - 1].clone()
    };
    let tail = layout_document_tokenized_from(
        cache, text_cache, new_dom, viewport, font_manager, debug_messages,
        image_cache, get_system_time_fn, font_loader, renderer_resources,
        id_namespace, dom_id, page_height,
        max_pages.saturating_sub(first_dirty_page),
        resume_token,
        Some((token_cache, first_dirty_page)),
    )?;
    let laid_out = tail.laid_out;
    let converged_at = tail.converged_at;
    pages.extend(tail.pages);
    Ok(IncrementalPagination { pages, laid_out, converged_at })
}
/// The per-page delta of a re-estimation, for the editor's lazy re-break
/// loop: pages whose breaks are bit-for-bit unchanged keep their DOM
/// subtrees untouched; patching starts at `first_changed_page`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BreaksDelta {
    /// Breaks (compared by exact `y` AND `kind`) identical to the previous
    /// estimate up to this index. On the first estimate this is 0.
    pub unchanged_prefix_len: usize,
    /// The first page whose boundary moved — equal to
    /// `unchanged_prefix_len` (page N ends at break N).
    pub first_changed_page: usize,
    /// Whether the total page count changed.
    pub page_count_changed: bool,
}
/// Owns the caches an incremental pagination loop needs
/// ([`crate::solver3::cache::LayoutCache`] + text cache + the previous
/// estimate) so an embedder holds ONE session object instead of wiring
/// solver internals (AZUL-STILL-TODO B7).
///
/// ```text
/// changeset -> app model -> session.re_estimate(new_dom, ...) -> BreaksDelta
///           -> patch own DOM only from first_changed_page on
///           -> session.dom_breaks(new_dom) for the structural positions
/// ```
// Holds the layout + text caches, neither of which is `Debug` (they are
// large, self-referential-ish caches whose contents are meaningless in a
// debug print). Deriving would force `Debug` onto both cache types.
#[allow(missing_debug_implementations)]
#[cfg(feature = "text_layout")]
pub struct PaginationSession {
    pub layout_cache: LayoutCache,
    pub text_cache: TextLayoutCache,
    pub previous: Option<crate::solver3::page_breaks::PaginationInfo>,
}
#[cfg(feature = "text_layout")]
impl Default for PaginationSession {
    fn default() -> Self {
        Self::new()
    }
}
#[cfg(feature = "text_layout")]
impl PaginationSession {
    #[must_use]
18
    pub fn new() -> Self {
18
        Self {
18
            layout_cache: LayoutCache::default(),
18
            text_cache: TextLayoutCache::new(),
18
            previous: None,
18
        }
18
    }
    /// Re-estimate pagination for (a new generation of) the document and
    /// report which pages kept their boundaries. Layout reuses this
    /// session's caches, so an unchanged prefix is cheap by construction.
    #[allow(clippy::too_many_arguments)] // mirrors compute_document_pagination's surface
4
    pub fn re_estimate<T, F>(
4
        &mut self,
4
        styled_dom: &StyledDom,
4
        viewport: LogicalRect,
4
        font_manager: &mut crate::font_traits::FontManager<T>,
4
        scroll_offsets: &BTreeMap<NodeId, ScrollPosition>,
4
        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
4
        gpu_value_cache: Option<&azul_core::gpu::GpuValueCache>,
4
        renderer_resources: &RendererResources,
4
        id_namespace: azul_core::resources::IdNamespace,
4
        dom_id: DomId,
4
        font_loader: F,
4
        page_config: FakePageConfig,
4
        image_cache: &azul_core::resources::ImageCache,
4
        get_system_time_fn: azul_core::task::GetSystemTimeCallback,
4
    ) -> Result<BreaksDelta>
4
    where
4
        T: ParsedFontTrait + Sync + 'static,
4
        F: Fn(
4
            std::sync::Arc<rust_fontconfig::FontBytes>,
4
            usize,
4
        ) -> std::result::Result<T, crate::text3::cache::LayoutError>,
    {
4
        let fragmentation_context = FragmentationContext::new_paged(viewport.size);
4
        let info = compute_document_pagination(
4
            &mut self.layout_cache,
4
            &mut self.text_cache,
4
            fragmentation_context,
4
            styled_dom,
4
            viewport,
4
            font_manager,
4
            scroll_offsets,
4
            debug_messages,
4
            gpu_value_cache,
4
            renderer_resources,
4
            id_namespace,
4
            dom_id,
4
            font_loader,
4
            page_config,
4
            image_cache,
4
            get_system_time_fn,
        )?;
4
        let unchanged_prefix_len = match &self.previous {
2
            None => 0,
2
            Some(prev) => prev
2
                .breaks
2
                .iter()
2
                .zip(info.breaks.iter())
                // The reuse contract: unchanged breaks are bit-for-bit equal
                // (recompute_page_breaks_from), so exact comparison is right —
                // an epsilon would hide genuinely moved boundaries.
4
                .take_while(|(a, b)| a.y.to_bits() == b.y.to_bits() && a.kind == b.kind)
2
                .count(),
        };
4
        let page_count_changed = self
4
            .previous
4
            .as_ref()
4
            .is_none_or(|prev| prev.page_count != info.page_count);
4
        self.previous = Some(info);
4
        Ok(BreaksDelta {
4
            unchanged_prefix_len,
4
            first_changed_page: unchanged_prefix_len,
4
            page_count_changed,
4
        })
4
    }
    /// The latest estimate (after at least one [`Self::re_estimate`]).
    #[must_use]
4
    pub const fn info(&self) -> Option<&crate::solver3::page_breaks::PaginationInfo> {
4
        self.previous.as_ref()
4
    }
    /// Structural DOM positions for the latest estimate — see
    /// [`pagination_to_dom_breaks`]. Call with the SAME document that was
    /// last re-estimated.
    #[must_use]
18
    pub fn dom_breaks(&self, styled_dom: &StyledDom) -> Option<Vec<StructuralBreak>> {
18
        let info = self.previous.as_ref()?;
18
        pagination_to_dom_breaks(&self.layout_cache, styled_dom, info)
18
    }
}