xfa-layout-engine 1.0.0-beta.5

Box-model and pagination layout engine for XFA forms. Experimental — part of the PDFluent XFA stack, under active development.
Documentation
//! M5.4 — Split/defer layout trace anchors: integration test.
//!
//! Drives the layout engine on a synthetic form where one node
//! overflows the current page, exercising both decision sites:
//!
//! - `(paginate, paginate_defer_to_next_page_min_h)` fires when a
//!   child node's required height exceeds the remaining vertical
//!   space.
//! - `(paginate, paginate_split)` fires when the engine splits a
//!   container or text leaf at the page boundary.
//!
//! Trace is off by default; a `with_sink` scope installs a
//! `RecordingSink` only for this test.

use std::rc::Rc;

use xfa_layout_engine::form::{FormNode, FormNodeId, FormNodeType, FormTree, Occur};
use xfa_layout_engine::layout::LayoutEngine;
use xfa_layout_engine::text::FontMetrics;
use xfa_layout_engine::trace::{with_sink, RecordingSink};
use xfa_layout_engine::types::{BoxModel, LayoutStrategy};

fn make_field(tree: &mut FormTree, name: &str, w: f64, h: f64) -> FormNodeId {
    tree.add_node(FormNode {
        name: name.to_string(),
        node_type: FormNodeType::Field {
            value: name.to_string(),
        },
        box_model: BoxModel {
            width: Some(w),
            height: Some(h),
            max_width: f64::MAX,
            max_height: f64::MAX,
            ..Default::default()
        },
        layout: LayoutStrategy::Positioned,
        children: vec![],
        occur: Occur::once(),
        font: FontMetrics::default(),
        calculate: None,
        validate: None,
        column_widths: vec![],
        col_span: 1,
    })
}

fn make_subform(
    tree: &mut FormTree,
    name: &str,
    strategy: LayoutStrategy,
    w: Option<f64>,
    h: Option<f64>,
    children: Vec<FormNodeId>,
) -> FormNodeId {
    tree.add_node(FormNode {
        name: name.to_string(),
        node_type: FormNodeType::Subform,
        box_model: BoxModel {
            width: w,
            height: h,
            max_width: f64::MAX,
            max_height: f64::MAX,
            ..Default::default()
        },
        layout: strategy,
        children,
        occur: Occur::once(),
        font: FontMetrics::default(),
        calculate: None,
        validate: None,
        column_widths: vec![],
        col_span: 1,
    })
}

fn make_root(
    tree: &mut FormTree,
    width: f64,
    height: f64,
    children: Vec<FormNodeId>,
) -> FormNodeId {
    tree.add_node(FormNode {
        name: "Root".to_string(),
        node_type: FormNodeType::Root,
        box_model: BoxModel {
            width: Some(width),
            height: Some(height),
            max_width: f64::MAX,
            max_height: f64::MAX,
            ..Default::default()
        },
        layout: LayoutStrategy::TopToBottom,
        children,
        occur: Occur::once(),
        font: FontMetrics::default(),
        calculate: None,
        validate: None,
        column_widths: vec![],
        col_span: 1,
    })
}

#[test]
fn paginate_defer_anchor_fires_on_overflow() {
    // Several short rows + one tall row that does not fit; layout
    // engine must trigger the overflow path on the tall row.
    let mut tree = FormTree::new();
    let row1 = make_field(&mut tree, "Row1", 200.0, 30.0);
    let row2 = make_field(&mut tree, "Row2", 200.0, 30.0);
    let tall = make_field(&mut tree, "Tall", 200.0, 80.0);
    let group = make_subform(
        &mut tree,
        "Group",
        LayoutStrategy::TopToBottom,
        Some(200.0),
        None,
        vec![row1, row2, tall],
    );
    // Root content area is 100pt; row1 (30) + row2 (30) leaves 40pt,
    // tall (80) overflows.
    let root = make_root(&mut tree, 200.0, 100.0, vec![group]);

    let engine = LayoutEngine::new(&tree);
    let sink: Rc<RecordingSink> = Rc::new(RecordingSink::new());
    let result =
        with_sink(sink.clone(), || engine.layout(root)).expect("layout should produce some result");

    let events = sink.events();
    let defer_fires = events
        .iter()
        .filter(|e| {
            e.phase.tag() == "paginate" && e.reason.tag() == "paginate_defer_to_next_page_min_h"
        })
        .count();
    assert!(
        defer_fires >= 1,
        "expected paginate_defer_to_next_page_min_h to fire on overflow; events={events:?}"
    );

    // Sanity: layout produced at least one page (behaviour preserved).
    assert!(
        !result.pages.is_empty(),
        "layout should still produce pages with trace anchors wired in"
    );
}

#[test]
fn paginate_trace_silent_when_no_sink_installed() {
    // Same shape, but without `with_sink`. The layout engine must run
    // without observable trace activity (no panic, no behaviour
    // change).
    let mut tree = FormTree::new();
    let row1 = make_field(&mut tree, "Row1", 200.0, 30.0);
    let tall = make_field(&mut tree, "Tall", 200.0, 80.0);
    let group = make_subform(
        &mut tree,
        "Group",
        LayoutStrategy::TopToBottom,
        Some(200.0),
        None,
        vec![row1, tall],
    );
    let root = make_root(&mut tree, 200.0, 100.0, vec![group]);

    let engine = LayoutEngine::new(&tree);
    let result = engine
        .layout(root)
        .expect("layout should run without a sink");
    assert!(!result.pages.is_empty());
}