Skip to main content

xfa_layout_engine/
layout.rs

1//! Layout engine — positions form nodes into layout rectangles.
2//!
3//! Implements XFA 3.3 §4 (Box Model) and §8 (Layout for Growable Objects).
4//! Supports positioned layout and flowed layout (tb, lr-tb, rl-tb).
5//!
6//! # XFA Spec 3.3 Chapter 8 — Layout for Growable Objects
7//!
8//! This module implements the core layout algorithm described in §8.6 (p288):
9//! a content-driven single traversal of the Form DOM, adding layout nodes
10//! to a Layout DOM as content is placed into containers. When a container
11//! fills up, the engine traverses to a new container (next contentArea,
12//! next pageArea, or a new page).
13//!
14//! ## Spec coverage status (reviewed 2026-04-19):
15//!
16//! - §8.1 Text Placement in Growable Containers: ✅ implemented (anchorType via Appendix A)
17//! - §8.2 Flowing Layout (TB, LR-TB, RL-TB):    ✅ implemented
18//! - §8.3 hAlign in various layouts:              ✅ hAlign on children in TB/LR-TB/RL-TB
19//! - §8.4 Growable + Flowed interaction:          ✅ resize then reflow
20//! - §8.5 Layout DOM structure:                   ✅ pages > nodes hierarchy
21//! - §8.6 Layout Algorithm:                       ✅ content-driven traversal
22//! - §8.7 Content Splitting:                      ✅ text-line splitting + container splitting
23//! - §8.8 Pagination Strategies:                  ✅ orderedOccurrence (sequential by default)
24//! - §8.9 Adhesion (keep):                        ✅ keep-chain look-ahead (keep.next/previous)
25//! - §8.10 Leaders/Trailers:                      ✅ per-page leader/trailer; ⚠️ overflow/bookend
26//! - §8.11 Tables:                                ✅ columnWidths, colSpan, row equalization
27//! - Appendix A: Coordinate algorithms:           ✅ anchorType (all 9 variants)
28//! - Appendix B: Layout Objects:                  ✅ area, exclGroup, subformSet
29
30use crate::error::Result;
31use crate::form::{
32    ContentArea, DrawContent, FieldKind, FormNode, FormNodeId, FormNodeMeta, FormNodeType, FormTree,
33};
34use crate::text::{self, FontFamily};
35use crate::trace::{sites as trace_sites, Reason as TraceReason};
36use crate::types::{LayoutStrategy, Rect, Size, TextAlign};
37use std::sync::{Mutex, OnceLock};
38
39/// Resolve the display value for a field.
40///
41/// - **Dropdown**: if the field has save-items and the current value matches
42///   one of them, return the corresponding display-item.
43/// - **NumericEdit**: strip unnecessary trailing zeros from float strings
44///   (e.g. "1.00000000" → "1", "3.50" → "3.5").
45/// - **DateTimePicker**: if the raw value starts with an ISO date prefix,
46///   collapse it to `YYYY-MM-DD`.
47/// - Otherwise return the value as-is.
48fn resolve_display_value<'a>(value: &'a str, meta: &'a FormNodeMeta) -> std::borrow::Cow<'a, str> {
49    if value.is_empty() {
50        return std::borrow::Cow::Borrowed(value);
51    }
52    // Dropdown: resolve save-item → display-item.
53    if meta.field_kind == FieldKind::Dropdown {
54        if !meta.save_items.is_empty() {
55            if let Some(idx) = meta.save_items.iter().position(|s| s == value) {
56                if let Some(display) = meta.display_items.get(idx) {
57                    return std::borrow::Cow::Borrowed(display.as_str());
58                }
59            }
60        }
61        return std::borrow::Cow::Borrowed(value);
62    }
63    // NumericEdit: format raw float values by stripping trailing zeros.
64    if meta.field_kind == FieldKind::NumericEdit {
65        if let Ok(num) = value.parse::<f64>() {
66            // Format with enough precision, then strip trailing zeros.
67            let formatted = format!("{}", num);
68            return std::borrow::Cow::Owned(formatted);
69        }
70    }
71    if meta.field_kind == FieldKind::DateTimePicker {
72        if let Some(date) = extract_iso_date_prefix(value) {
73            return std::borrow::Cow::Owned(date.to_string());
74        }
75    }
76    std::borrow::Cow::Borrowed(value)
77}
78
79fn extract_iso_date_prefix(value: &str) -> Option<&str> {
80    let prefix = value.get(0..10)?;
81    let bytes = prefix.as_bytes();
82    if bytes.len() != 10
83        || !bytes[0..4].iter().all(u8::is_ascii_digit)
84        || bytes[4] != b'-'
85        || !bytes[5..7].iter().all(u8::is_ascii_digit)
86        || bytes[7] != b'-'
87        || !bytes[8..10].iter().all(u8::is_ascii_digit)
88    {
89        return None;
90    }
91    Some(prefix)
92}
93
94/// A unique identifier for a layout node.
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub struct LayoutNodeId(pub usize);
97
98/// The output of the layout engine: positioned rectangles on pages.
99#[derive(Debug)]
100pub struct LayoutDom {
101    /// Laid out pages.
102    pub pages: Vec<LayoutPage>,
103}
104
105/// Per-page pagination diagnostics collected only on opt-in code paths.
106#[derive(Debug, Clone, Default)]
107pub struct LayoutProfile {
108    /// Per-page profile data.
109    pub pages: Vec<LayoutProfilePage>,
110}
111
112/// Minimal vertical-space profiling metadata for one laid out page.
113#[derive(Debug, Clone)]
114pub struct LayoutProfilePage {
115    /// Page height.
116    pub page_height: f64,
117    /// Used height.
118    pub used_height: f64,
119    /// Whether content overflowed to the next page.
120    pub overflow_to_next: bool,
121    /// First element that overflowed.
122    pub first_overflow_element: Option<String>,
123}
124
125impl LayoutDom {
126    /// Estimate the total heap bytes consumed by this layout tree.
127    ///
128    /// Walks every page and every node recursively, summing the heap
129    /// contributions of `Vec` fields (children, display_items, save_items)
130    /// and inline `String` fields (name, content strings).  The estimate is
131    /// a lower bound — it does not account for `Vec` capacity overheads or
132    /// internal allocator padding.
133    ///
134    /// Primary use: memory-usage regression tests and profiling dashboards.
135    pub fn estimated_heap_bytes(&self) -> usize {
136        fn node_bytes(n: &LayoutNode) -> usize {
137            // Name string heap allocation
138            let mut total = n.name.len();
139            // Children vec + each child
140            total += n.children.capacity() * std::mem::size_of::<LayoutNode>();
141            for child in &n.children {
142                total += node_bytes(child);
143            }
144            // display_items / save_items
145            for s in &n.display_items {
146                total += s.len();
147            }
148            for s in &n.save_items {
149                total += s.len();
150            }
151            // Content strings
152            total += match &n.content {
153                LayoutContent::None => 0,
154                LayoutContent::Text(t) => t.len(),
155                LayoutContent::Field { value, .. } => value.len(),
156                LayoutContent::WrappedText { lines, .. } => {
157                    lines.iter().map(|l| l.len()).sum::<usize>()
158                }
159                LayoutContent::Image { data, mime_type } => data.len() + mime_type.len(),
160                LayoutContent::Draw(_) => 0,
161            };
162            total
163        }
164
165        let mut total = self.pages.capacity() * std::mem::size_of::<LayoutPage>();
166        for page in &self.pages {
167            total += page.nodes.capacity() * std::mem::size_of::<LayoutNode>();
168            for node in &page.nodes {
169                total += node_bytes(node);
170            }
171        }
172        total
173    }
174}
175
176/// Absolute maximum number of pages to prevent pagination explosion.
177/// Used as a hard upper bound; the dynamic limit from
178/// `estimate_page_limit` is preferred. (#729, #764)
179///
180/// XFA Spec 3.3 §9.3 "Layout for Dynamic Forms" (p357): the layout
181/// processor repeats page templates as needed for overflow content.
182/// The spec sets no explicit limit; this is our safety cap.
183const MAX_PAGES: usize = 500;
184
185/// A single page in the layout output.
186#[derive(Debug)]
187pub struct LayoutPage {
188    /// Page width.
189    pub width: f64,
190    /// Page height.
191    pub height: f64,
192    /// Layout nodes on this page.
193    pub nodes: Vec<LayoutNode>,
194}
195
196/// A positioned element on a page.
197#[derive(Debug, Clone)]
198pub struct LayoutNode {
199    /// The form node this layout node represents.
200    pub form_node: FormNodeId,
201    /// Bounding rectangle in page coordinates (points).
202    pub rect: Rect,
203    /// The node's display name (for debugging).
204    pub name: String,
205    /// Content for leaf nodes.
206    pub content: LayoutContent,
207    /// Children laid out within this node.
208    pub children: Vec<LayoutNode>,
209    /// Per-node visual style (colors, borders) from the XFA template.
210    pub style: crate::form::FormNodeStyle,
211    /// Display items for choice list fields (XFA 3.3 §7.7).
212    pub display_items: Vec<String>,
213    /// Save items for choice list fields (XFA 3.3 §7.7).
214    pub save_items: Vec<String>,
215}
216
217/// Content type for layout leaf nodes.
218#[derive(Debug, Clone)]
219pub enum LayoutContent {
220    /// No content.
221    None,
222    /// Text content.
223    Text(String),
224    /// Field content.
225    Field {
226        /// Field value.
227        value: String,
228        /// Field kind.
229        field_kind: crate::form::FieldKind,
230        /// Font size.
231        font_size: f64,
232        /// Font family.
233        font_family: FontFamily,
234    },
235    /// Pre-wrapped text lines for rendering.
236    WrappedText {
237        /// Wrapped text lines.
238        lines: Vec<String>,
239        /// Per-line flag: `true` when the line is the first line of a paragraph.
240        first_line_of_para: Vec<bool>,
241        /// Font size.
242        font_size: f64,
243        /// Horizontal text alignment (from XFA `<para hAlign>`).
244        text_align: TextAlign,
245        /// Font family for selecting the correct PDF font resource.
246        font_family: FontFamily,
247        /// Additional space above the first line of text (from XFA `<para spaceAbove>`).
248        space_above_pt: Option<f64>,
249        /// Additional space below the last line of text (from XFA `<para spaceBelow>`).
250        space_below_pt: Option<f64>,
251        /// True when this text originates from a field value (not a draw element).
252        from_field: bool,
253    },
254    /// A static image.
255    Image {
256        /// Image data.
257        data: Vec<u8>,
258        /// Image MIME type.
259        mime_type: String,
260    },
261    /// A static draw element (line, rectangle, arc, text).
262    Draw(DrawContent),
263}
264
265/// A content node queued for pagination, carrying page-break flags.
266#[derive(Debug, Clone)]
267struct QueuedNode {
268    id: FormNodeId,
269    break_before: bool,
270    break_after: bool,
271    #[allow(dead_code)]
272    break_target: Option<String>,
273    /// Optional override for the children of this node (used for splitting subforms/tables).
274    children_override: Option<Vec<FormNodeId>>,
275    /// Remaining text lines for a text leaf split across pages (§8.7).
276    text_lines_override: Option<Vec<String>>,
277    /// Per-child overrides propagated from nested splits.  When this node is
278    /// processed, children whose ID appears here use the associated list as
279    /// their own `children_override`, preventing duplication of already-placed
280    /// content.
281    nested_child_overrides: Option<Vec<(FormNodeId, Vec<FormNodeId>)>>,
282}
283
284type FittingResult = (
285    LayoutPage,
286    Vec<QueuedNode>,
287    bool,
288    Option<String>,
289    Option<LayoutProfilePage>,
290);
291
292#[derive(Debug, Default)]
293struct GroundTruthTraceState {
294    last_break_before_read: Option<String>,
295}
296
297static GROUNDTRUTH_TRACE_STATE: OnceLock<Mutex<GroundTruthTraceState>> = OnceLock::new();
298
299fn groundtruth_trace_enabled() -> bool {
300    std::env::var("XFA_TRACE_DOC")
301        .map(|value| !value.is_empty())
302        .unwrap_or(false)
303}
304
305fn groundtruth_trace_state() -> &'static Mutex<GroundTruthTraceState> {
306    GROUNDTRUTH_TRACE_STATE.get_or_init(|| Mutex::new(GroundTruthTraceState::default()))
307}
308
309/// The layout engine.
310pub struct LayoutEngine<'a> {
311    form: &'a FormTree,
312}
313
314impl<'a> LayoutEngine<'a> {
315    /// Create a new layout engine.
316    pub fn new(form: &'a FormTree) -> Self {
317        Self { form }
318    }
319
320    fn trace_node_id(&self, id: FormNodeId) -> String {
321        let node = self.form.get(id);
322        self.form
323            .meta(id)
324            .xfa_id
325            .clone()
326            .filter(|value| !value.is_empty())
327            .or_else(|| (!node.name.is_empty()).then(|| node.name.clone()))
328            .unwrap_or_else(|| id.0.to_string())
329    }
330
331    fn trace_node_name(&self, id: FormNodeId) -> String {
332        let node = self.form.get(id);
333        if !node.name.is_empty() {
334            node.name.clone()
335        } else {
336            self.trace_node_id(id)
337        }
338    }
339
340    fn trace_parent_id(&self, child_id: FormNodeId) -> Option<FormNodeId> {
341        self.form
342            .nodes
343            .iter()
344            .enumerate()
345            .find_map(|(idx, node)| node.children.contains(&child_id).then_some(FormNodeId(idx)))
346    }
347
348    fn trace_path_segment(&self, id: FormNodeId) -> String {
349        let node = self.form.get(id);
350        format!(
351            "{}#{}",
352            Self::form_node_type_name(&node.node_type),
353            self.trace_node_id(id)
354        )
355    }
356
357    fn trace_node_path(&self, id: FormNodeId) -> String {
358        let mut chain = vec![id];
359        let mut cursor = id;
360        while let Some(parent_id) = self.trace_parent_id(cursor) {
361            chain.push(parent_id);
362            cursor = parent_id;
363        }
364        chain.reverse();
365        chain
366            .into_iter()
367            .map(|node_id| self.trace_path_segment(node_id))
368            .collect::<Vec<_>>()
369            .join("/")
370    }
371
372    #[track_caller]
373    fn trace_vertical_state(
374        &self,
375        function: &'static str,
376        current_y: Option<f64>,
377        remaining_space: Option<f64>,
378        node_id: Option<FormNodeId>,
379        message: impl AsRef<str>,
380    ) {
381        if !groundtruth_trace_enabled() {
382            return;
383        }
384
385        let location = std::panic::Location::caller();
386        let (triggering_id, node_type, node_name) = if let Some(node_id) = node_id {
387            let node = self.form.get(node_id);
388            (
389                self.trace_node_id(node_id),
390                Self::form_node_type_name(&node.node_type).to_string(),
391                self.trace_node_name(node_id),
392            )
393        } else {
394            ("-".to_string(), "-".to_string(), "-".to_string())
395        };
396
397        eprintln!(
398            "{}:{} fn={} current_y={} remaining_space={} triggering_element_id={} node_type={} node_name={} {}",
399            location.file(),
400            location.line(),
401            function,
402            current_y
403                .map(|value| format!("{value:.3}"))
404                .unwrap_or_else(|| "-".to_string()),
405            remaining_space
406                .map(|value| format!("{value:.3}"))
407                .unwrap_or_else(|| "-".to_string()),
408            triggering_id,
409            node_type,
410            node_name,
411            message.as_ref(),
412        );
413    }
414
415    #[track_caller]
416    fn trace_break_before_read(
417        &self,
418        current_y: f64,
419        remaining_space: f64,
420        node_id: FormNodeId,
421        break_before: bool,
422        placed_count: usize,
423        header_node_count: usize,
424    ) {
425        if !groundtruth_trace_enabled() {
426            return;
427        }
428
429        let location = std::panic::Location::caller();
430        let preceding = format!(
431            "{}:{} break_before={} placed_count={} header_node_count={}",
432            location.file(),
433            location.line(),
434            break_before,
435            placed_count,
436            header_node_count
437        );
438        if let Ok(mut state) = groundtruth_trace_state().lock() {
439            state.last_break_before_read = Some(preceding);
440        }
441        self.trace_vertical_state(
442            "layout_content_fitting",
443            Some(current_y),
444            Some(remaining_space),
445            Some(node_id),
446            format!(
447                "break_before check break_before={} placed_count={} header_node_count={} event_type=read",
448                break_before,
449                placed_count,
450                header_node_count
451            ),
452        );
453    }
454
455    #[track_caller]
456    fn trace_commit_page_boundary(
457        &self,
458        current_page: usize,
459        used_height: Option<f64>,
460        page_height: Option<f64>,
461        next_item: Option<&QueuedNode>,
462    ) {
463        if !groundtruth_trace_enabled() {
464            return;
465        }
466
467        let remaining_space = match (used_height, page_height) {
468            (Some(used), Some(total)) => Some(total - used),
469            _ => None,
470        };
471        let preceding_break_read = groundtruth_trace_state()
472            .lock()
473            .ok()
474            .and_then(|state| state.last_break_before_read.clone())
475            .unwrap_or_else(|| "none".to_string());
476        let break_before = next_item.map(|queued| queued.break_before).unwrap_or(false);
477        let subform_path = next_item
478            .map(|queued| self.trace_node_path(queued.id))
479            .unwrap_or_else(|| "none".to_string());
480
481        self.trace_vertical_state(
482            "layout_internal",
483            used_height,
484            remaining_space,
485            next_item.map(|queued| queued.id),
486            format!(
487                "event_type=page_commit COMMIT page_boundary page={} -> page={} break_before={} preceding_break_read=\"{}\" subform_path={}",
488                current_page,
489                current_page + 1,
490                break_before,
491                preceding_break_read,
492                subform_path
493            ),
494        );
495    }
496
497    /// Perform layout and collect pagination diagnostics for each emitted page.
498    pub fn layout_with_profile(&self, root: FormNodeId) -> Result<(LayoutDom, LayoutProfile)> {
499        let mut profile = LayoutProfile::default();
500        let dom = self.layout_internal(root, Some(&mut profile))?;
501        Ok((dom, profile))
502    }
503
504    /// Perform layout on the entire form tree starting from the root node.
505    ///
506    /// XFA Spec 3.3 §8.6 — The Layout Algorithm (p288): content-driven single
507    /// traversal of the Form DOM, placing nodes into the Layout DOM. When a
508    /// container fills, traverse to the next container (§8.7 splitting, §8.8
509    /// pagination).
510    ///
511    /// Supports multi-page pagination: when content overflows a page's content
512    /// area, remaining nodes are placed on subsequent pages. The last page
513    /// template is repeated as needed for overflow content.
514    ///
515    /// TODO §8.8: simplexPaginated/duplexPaginated, pagePosition/oddOrEven/
516    /// blankOrNotBlank qualifications, termination processing (last/only page).
517    pub fn layout(&self, root: FormNodeId) -> Result<LayoutDom> {
518        self.layout_internal(root, None)
519    }
520
521    fn layout_internal(
522        &self,
523        root: FormNodeId,
524        mut profile: Option<&mut LayoutProfile>,
525    ) -> Result<LayoutDom> {
526        self.trace_vertical_state(
527            "layout_internal",
528            Some(0.0),
529            None,
530            Some(root),
531            format!("enter collect_profile={}", profile.is_some()),
532        );
533        let root_node = self.form.get(root);
534        let collect_profile = profile.is_some();
535
536        let (page_areas, raw_content_nodes) = self.extract_page_structure(root_node)?;
537        self.trace_vertical_state(
538            "layout_internal",
539            Some(0.0),
540            None,
541            Some(root),
542            format!(
543                "page structure extracted page_areas={} raw_content_nodes={} event_type=runtime_allocation",
544                page_areas.len(),
545                raw_content_nodes.len()
546            ),
547        );
548        // Build queued nodes with break_before flags and occur expansion.
549        let content_queued = self.queue_content(&raw_content_nodes);
550
551        let mut pages = Vec::new();
552
553        if page_areas.is_empty() {
554            // No explicit page structure — use root's dimensions
555            let page_w = root_node.box_model.width.unwrap_or(612.0);
556            let page_h = root_node.box_model.height.unwrap_or(792.0);
557            let area = ContentArea {
558                name: String::new(),
559                x: 0.0,
560                y: 0.0,
561                width: page_w,
562                height: page_h,
563                leader: None,
564                trailer: None,
565            };
566
567            if root_node.layout == LayoutStrategy::TopToBottom {
568                // TB layout supports pagination: split content across pages
569                let mut remaining = content_queued;
570                let page_limit = self.estimate_page_limit(&remaining, page_h);
571                while !remaining.is_empty() {
572                    if pages.len() >= page_limit {
573                        eprintln!(
574                            "WARNING: Page limit ({}) reached, truncating layout for {}",
575                            page_limit, root_node.name
576                        );
577                        break;
578                    }
579                    let (page, rest, consumed_break_only, _, page_profile) = self
580                        .layout_content_fitting(
581                            &area,
582                            &remaining,
583                            page_w,
584                            page_h,
585                            collect_profile,
586                        )?;
587                    if page.nodes.is_empty() && !consumed_break_only {
588                        // Force place one item to prevent infinite loop
589                        let forced = self.layout_content_on_page(
590                            &area,
591                            page_w,
592                            page_h,
593                            &[remaining[0].id],
594                            root_node.layout,
595                        )?;
596                        let next_remaining = remaining[1..].to_vec();
597                        log::debug!(
598                            "XFA layout: processing page {}/{} (forced)",
599                            pages.len() + 1,
600                            page_limit
601                        );
602                        if let Some(profile) = profile.as_deref_mut() {
603                            profile.pages.push(
604                                self.profile_page_from_nodes(
605                                    &forced,
606                                    area.y,
607                                    area.height,
608                                    !next_remaining.is_empty(),
609                                    next_remaining
610                                        .first()
611                                        .map(|qn| self.describe_queued_node(qn)),
612                                ),
613                            );
614                        }
615                        pages.push(forced);
616                        remaining = next_remaining;
617                    } else if consumed_break_only {
618                        // Break-only page: skip the blank page, continue with rest
619                        remaining = rest;
620                    } else {
621                        log::debug!(
622                            "XFA layout: processing page {}/{}",
623                            pages.len() + 1,
624                            page_limit
625                        );
626                        if !rest.is_empty() {
627                            self.trace_commit_page_boundary(
628                                pages.len() + 1,
629                                page_profile.as_ref().map(|profile| profile.used_height),
630                                page_profile.as_ref().map(|profile| profile.page_height),
631                                rest.first(),
632                            );
633                        }
634                        if let (Some(profile), Some(page_profile)) =
635                            (profile.as_deref_mut(), page_profile)
636                        {
637                            profile.pages.push(page_profile);
638                        }
639                        pages.push(page);
640                        remaining = rest;
641                    }
642                }
643            } else {
644                // Non-TB layouts: place everything on one page (layout_children expands occur)
645                let page = self.layout_content_on_page(
646                    &area,
647                    page_w,
648                    page_h,
649                    &raw_content_nodes,
650                    root_node.layout,
651                )?;
652                if let Some(profile) = profile.as_deref_mut() {
653                    profile.pages.push(self.profile_page_from_nodes(
654                        &page,
655                        area.y,
656                        area.height,
657                        false,
658                        None,
659                    ));
660                }
661                pages.push(page);
662            }
663        } else {
664            // XFA §4.2 — Positioned content: when ALL content nodes use
665            // positioned layout (absolute x/y coordinates), they share a
666            // single page.  Flowing them top-to-bottom across pages is
667            // incorrect and causes over-pagination (e.g. 2-page output for
668            // a 1-page form whose template defines two positioned subforms
669            // overlaid on the same pageArea).
670            //
671            // Guard: overlay heuristic only applies when every positioned
672            // content subform is *small* relative to the page (height ≤ 50%
673            // of the content area).  Page-sized subforms represent separate
674            // pages and must flow through the normal pagination path — cramming
675            // them onto one page causes under-pagination (#GATE-22).
676            let multi_positioned = content_queued.len() > 1
677                && content_queued.iter().all(|qn| {
678                    let node = self.form.get(qn.id);
679                    node.layout == LayoutStrategy::Positioned
680                        && matches!(
681                            node.node_type,
682                            FormNodeType::Subform | FormNodeType::Area | FormNodeType::ExclGroup
683                        )
684                        && !qn.break_before
685                })
686                && {
687                    let pa = &page_areas[0];
688                    let ca = primary_content_area(pa);
689                    let half_page = ca.height * 0.5;
690                    content_queued
691                        .iter()
692                        .all(|qn| self.compute_extent(qn.id).height <= half_page)
693                };
694
695            // #794 — Single positioned subform delegation: when there is
696            // exactly 1 content node that is a Positioned subform whose
697            // children are ALL Positioned AND fit within the page, the
698            // parent TopToBottom flow would compute the child's full
699            // envelope height and overflow to extra pages.  Delegate to
700            // positioned layout instead.  If the content overflows the
701            // page, let it paginate normally (#736).
702            let single_positioned_delegate = content_queued.len() == 1 && {
703                let qn = &content_queued[0];
704                let node = self.form.get(qn.id);
705                if node.layout == LayoutStrategy::Positioned
706                    && matches!(
707                        node.node_type,
708                        FormNodeType::Subform | FormNodeType::Area | FormNodeType::ExclGroup
709                    )
710                    && !qn.break_before
711                    && !node.children.is_empty()
712                    && node.children.iter().all(|&cid| {
713                        let child = self.form.get(cid);
714                        child.layout == LayoutStrategy::Positioned
715                    })
716                {
717                    // Check that the positioned content fits on one page.
718                    let pa = &page_areas[0];
719                    let ca = primary_content_area(pa);
720                    let child_extent = self.compute_extent(qn.id);
721                    child_extent.height <= ca.height
722                } else {
723                    false
724                }
725            };
726
727            let all_content_positioned = multi_positioned || single_positioned_delegate;
728
729            if all_content_positioned {
730                let pa = &page_areas[0];
731                let ca = primary_content_area(pa);
732                let ids: Vec<FormNodeId> = content_queued.iter().map(|qn| qn.id).collect();
733                let mut page = self.layout_content_on_page(
734                    ca,
735                    pa.page_width,
736                    pa.page_height,
737                    &ids,
738                    LayoutStrategy::Positioned,
739                )?;
740                let page_profile = if collect_profile {
741                    Some(self.profile_page_from_nodes(&page, ca.y, ca.height, false, None))
742                } else {
743                    None
744                };
745                self.prepend_fixed_nodes(&pa.fixed_nodes, &mut page)?;
746                if Self::has_visible_content(&page.nodes) {
747                    if let (Some(profile), Some(page_profile)) =
748                        (profile.as_deref_mut(), page_profile)
749                    {
750                        profile.pages.push(page_profile);
751                    }
752                    pages.push(page);
753                }
754            }
755
756            // Layout content across page areas, then repeat last template for overflow.
757            let mut remaining = if all_content_positioned {
758                Vec::new()
759            } else {
760                content_queued
761            };
762            let data_driven_body_queue =
763                self.queued_nodes_have_data_backed_body_content(&remaining);
764            // XFA §8.6 — layout termination is content-driven.  A continuation
765            // pageArea after an emitted page requires body content, an explicit
766            // page anchor, or an accepted split remainder only when the form has
767            // data-backed content.  Purely static forms (no data binding in the
768            // entire content queue) always paginate — their page areas represent
769            // intentional form pages, not boilerplate repeaters.
770            let page_area_continuation_needs_body_content = data_driven_body_queue;
771            for pa in &page_areas {
772                if remaining.is_empty() {
773                    break;
774                }
775                // XFA 3.3 §8.6: layout termination is content-driven.  A
776                // queued pageArea continuation after an emitted page is used
777                // only when body content, an explicit page anchor, or an
778                // accepted split remainder still needs that next pageArea.
779                if !pages.is_empty()
780                    && !self.queued_nodes_can_populate_continuation_page_area(
781                        &remaining,
782                        page_area_continuation_needs_body_content,
783                    )
784                {
785                    remaining.clear();
786                    break;
787                }
788                let ca = primary_content_area(pa);
789                let (mut placed, rest, consumed_break_only, _, page_profile) = self
790                    .layout_content_fitting(
791                        ca,
792                        &remaining,
793                        pa.page_width,
794                        pa.page_height,
795                        collect_profile,
796                    )?;
797                let commit_used_height = page_profile.as_ref().map(|profile| profile.used_height);
798                let commit_page_height = page_profile.as_ref().map(|profile| profile.page_height);
799                let mut page_committed = false;
800                if consumed_break_only {
801                    remaining = rest;
802                } else if Self::has_visible_content(&placed.nodes) {
803                    self.prepend_fixed_nodes(&pa.fixed_nodes, &mut placed)?;
804                    if let (Some(profile), Some(page_profile)) =
805                        (profile.as_deref_mut(), page_profile)
806                    {
807                        profile.pages.push(page_profile);
808                    }
809                    pages.push(placed);
810                    page_committed = true;
811                    remaining = rest;
812                } else if !pa.fixed_nodes.is_empty() {
813                    // Content nodes are invisible but the page area has
814                    // fixed elements (headers, footers, decorations).
815                    // Create the page with just the fixed chrome — this
816                    // matches Adobe's behavior for explicit page areas
817                    // whose flowing content is blank/hidden.
818                    self.prepend_fixed_nodes(&pa.fixed_nodes, &mut placed)?;
819                    if Self::has_visible_content(&placed.nodes) {
820                        if let (Some(profile), Some(page_profile)) =
821                            (profile.as_deref_mut(), page_profile)
822                        {
823                            profile.pages.push(page_profile);
824                        }
825                        pages.push(placed);
826                        page_committed = true;
827                    }
828                    remaining = rest;
829                } else {
830                    // Content nodes are all hidden/invisible and the page
831                    // area has no fixed elements — suppress the blank page.
832                    remaining = rest;
833                }
834                if page_committed && !remaining.is_empty() {
835                    self.trace_commit_page_boundary(
836                        pages.len(),
837                        commit_used_height,
838                        commit_page_height,
839                        remaining.first(),
840                    );
841                }
842            }
843
844            // Overflow: repeat page templates until all content is placed.
845            if !remaining.is_empty() {
846                let last_idx = page_areas.len() - 1;
847                let overflow_ca = primary_content_area(&page_areas[last_idx]);
848                // Dynamic page limit: estimated pages for remaining content + already placed.
849                let page_limit =
850                    self.estimate_page_limit(&remaining, overflow_ca.height) + pages.len();
851                while !remaining.is_empty() {
852                    if pages.len() >= page_limit {
853                        eprintln!(
854                            "WARNING: Page limit ({}) reached, truncating layout overflow for {}",
855                            page_limit, root_node.name
856                        );
857                        break;
858                    }
859                    let pa_idx = last_idx;
860                    let pa = &page_areas[pa_idx];
861                    let ca = primary_content_area(pa);
862
863                    let (mut page, rest, consumed_break_only, _, page_profile) = self
864                        .layout_content_fitting(
865                            ca,
866                            &remaining,
867                            pa.page_width,
868                            pa.page_height,
869                            collect_profile,
870                        )?;
871                    if page.nodes.is_empty() && !consumed_break_only {
872                        let forced = self.layout_content_on_page(
873                            ca,
874                            pa.page_width,
875                            pa.page_height,
876                            &[remaining[0].id],
877                            LayoutStrategy::TopToBottom,
878                        )?;
879                        let next_remaining = remaining[1..].to_vec();
880                        if Self::has_visible_content(&forced.nodes) {
881                            let mut forced = forced;
882                            let forced_profile = if collect_profile {
883                                Some(
884                                    self.profile_page_from_nodes(
885                                        &forced,
886                                        ca.y,
887                                        ca.height,
888                                        !next_remaining.is_empty(),
889                                        next_remaining
890                                            .first()
891                                            .map(|qn| self.describe_queued_node(qn)),
892                                    ),
893                                )
894                            } else {
895                                None
896                            };
897                            self.prepend_fixed_nodes(&pa.fixed_nodes, &mut forced)?;
898                            if let (Some(profile), Some(page_profile)) =
899                                (profile.as_deref_mut(), forced_profile)
900                            {
901                                profile.pages.push(page_profile);
902                            }
903                            pages.push(forced);
904                        }
905                        remaining = next_remaining;
906                    } else if consumed_break_only {
907                        remaining = rest;
908                    } else {
909                        if Self::has_visible_content(&page.nodes) {
910                            self.prepend_fixed_nodes(&pa.fixed_nodes, &mut page)?;
911                            if !rest.is_empty() {
912                                self.trace_commit_page_boundary(
913                                    pages.len() + 1,
914                                    page_profile.as_ref().map(|profile| profile.used_height),
915                                    page_profile.as_ref().map(|profile| profile.page_height),
916                                    rest.first(),
917                                );
918                            }
919                            if let (Some(profile), Some(page_profile)) =
920                                (profile.as_deref_mut(), page_profile)
921                            {
922                                profile.pages.push(page_profile);
923                            }
924                            pages.push(page);
925                        }
926                        remaining = rest;
927                    }
928                }
929            }
930        }
931
932        Ok(LayoutDom { pages })
933    }
934
935    // -------------------------------------------------------------------
936    // Helper methods for queued/hidden-aware pagination
937    // -------------------------------------------------------------------
938
939    fn queued_nodes_can_populate_continuation_page_area(
940        &self,
941        nodes: &[QueuedNode],
942        needs_body_content: bool,
943    ) -> bool {
944        if !needs_body_content {
945            return true;
946        }
947
948        nodes
949            .iter()
950            .any(|node| self.queued_node_can_populate_continuation_page_area(node))
951    }
952
953    fn queued_node_can_populate_continuation_page_area(&self, node: &QueuedNode) -> bool {
954        self.queued_node_has_explicit_page_anchor(node)
955            || Self::queued_node_is_split_remainder(node)
956            || self.queued_node_has_data_backed_body_content(node)
957    }
958
959    fn queued_node_has_explicit_page_anchor(&self, node: &QueuedNode) -> bool {
960        node.break_before
961            || node.break_target.is_some()
962            || self.form.meta(node.id).page_break_before
963    }
964
965    fn queued_node_is_split_remainder(node: &QueuedNode) -> bool {
966        node.text_lines_override.is_some()
967            || node.children_override.is_some()
968            || node
969                .nested_child_overrides
970                .as_ref()
971                .is_some_and(|overrides| !overrides.is_empty())
972    }
973
974    fn queued_nodes_have_data_backed_body_content(&self, nodes: &[QueuedNode]) -> bool {
975        nodes
976            .iter()
977            .any(|node| self.queued_node_has_data_backed_body_content(node))
978    }
979
980    fn queued_node_has_data_backed_body_content(&self, node: &QueuedNode) -> bool {
981        self.subtree_has_data_backed_body_content(
982            node.id,
983            node.children_override.as_deref(),
984            node.nested_child_overrides.as_deref(),
985            false,
986        )
987    }
988
989    /// XFA 3.3 §8.6 — layout termination is driven by remaining content.
990    /// Static template chrome may render on an already-required page, but it
991    /// does not by itself force an extra pageArea continuation after the
992    /// data-backed body content has ended.
993    fn subtree_has_data_backed_body_content(
994        &self,
995        id: FormNodeId,
996        children_override: Option<&[FormNodeId]>,
997        nested_overrides: Option<&[(FormNodeId, Vec<FormNodeId>)]>,
998        inherited_data_context: bool,
999    ) -> bool {
1000        if self.is_layout_hidden(id) {
1001            return false;
1002        }
1003
1004        let node = self.form.get(id);
1005        let meta = self.form.meta(id);
1006        let own_data_context = !meta.data_bind_none && meta.data_bind_ref.is_some();
1007        let data_context = inherited_data_context || own_data_context;
1008
1009        match &node.node_type {
1010            FormNodeType::Field { value } => data_context && !value.trim().is_empty(),
1011            FormNodeType::Root
1012            | FormNodeType::Subform
1013            | FormNodeType::Area
1014            | FormNodeType::ExclGroup
1015            | FormNodeType::SubformSet => {
1016                let children = children_override.unwrap_or(&node.children);
1017                let expanded = if children_override.is_some() {
1018                    children.to_vec()
1019                } else {
1020                    self.expand_occur(children)
1021                };
1022                expanded.iter().any(|&child_id| {
1023                    let child_override = nested_overrides
1024                        .and_then(|overrides| {
1025                            overrides
1026                                .iter()
1027                                .find(|(override_id, _)| *override_id == child_id)
1028                        })
1029                        .map(|(_, child_override)| child_override.as_slice());
1030                    self.subtree_has_data_backed_body_content(
1031                        child_id,
1032                        child_override,
1033                        nested_overrides,
1034                        data_context,
1035                    )
1036                })
1037            }
1038            FormNodeType::PageSet
1039            | FormNodeType::PageArea { .. }
1040            | FormNodeType::Draw(_)
1041            | FormNodeType::Image { .. } => false,
1042        }
1043    }
1044
1045    /// Estimate a dynamic page limit based on total content height vs page
1046    /// content area height.  Returns `min(estimated * 2, MAX_PAGES)` with a
1047    /// floor of 10 so tiny forms still have room for splitting overhead. (#764)
1048    fn estimate_page_limit(&self, content: &[QueuedNode], page_height: f64) -> usize {
1049        if page_height <= 0.0 {
1050            return MAX_PAGES;
1051        }
1052        let total_height: f64 = content
1053            .iter()
1054            .map(|qn| {
1055                self.compute_extent_with_available_and_override(
1056                    qn.id,
1057                    None,
1058                    qn.children_override.as_deref(),
1059                )
1060                .height
1061            })
1062            .sum();
1063        let estimated = (total_height / page_height).ceil() as usize;
1064        (estimated * 2).clamp(10, MAX_PAGES)
1065    }
1066
1067    /// Returns true if the layout nodes contain at least one visually rendered
1068    /// element (non-`None` content or a child with rendered content).
1069    ///
1070    /// Used to suppress blank pages whose content nodes are all hidden/invisible
1071    /// (e.g. conditional contact-form pages that are collapsed in the data).
1072    fn has_visible_content(nodes: &[LayoutNode]) -> bool {
1073        nodes.iter().any(|n| {
1074            !matches!(n.content, LayoutContent::None) || Self::has_visible_content(&n.children)
1075        })
1076    }
1077
1078    /// Returns true if the node should be completely skipped during layout
1079    /// (no layout space consumed).
1080    ///
1081    /// Adobe empirical behavior (not strictly per spec):
1082    /// - `hidden` / `invisible` / `inactive` -- no layout space, not rendered.
1083    /// - `visible` -- normal.
1084    ///
1085    /// See `Presence::is_layout_hidden()` for rationale (fixes #806).
1086    fn is_layout_hidden(&self, id: FormNodeId) -> bool {
1087        let meta = self.form.meta(id);
1088        if meta.presence.is_layout_hidden() {
1089            return true;
1090        }
1091        // Content-area-targeted nodes (breakBefore targetType="contentArea")
1092        // are excluded from the primary flow ONLY when they are small
1093        // decorative elements (< 50pt tall).  Large content subforms that
1094        // target the primary contentArea should still be laid out.
1095        if meta.content_area_break {
1096            let node = self.form.get(id);
1097            let h = node.box_model.height.unwrap_or(f64::MAX);
1098            return h < 50.0;
1099        }
1100        false
1101    }
1102
1103    /// Build a queue of `QueuedNode`s from a list of child IDs, skipping
1104    /// nodes that are layout-hidden and expanding occur rules.
1105    fn queue_content(&self, children: &[FormNodeId]) -> Vec<QueuedNode> {
1106        let expanded = self.expand_occur(children);
1107        expanded
1108            .into_iter()
1109            .filter(|&id| !self.is_layout_hidden(id))
1110            .map(|id| {
1111                let meta = self.form.meta(id);
1112                self.trace_vertical_state(
1113                    "queue_content",
1114                    None,
1115                    None,
1116                    Some(id),
1117                    format!(
1118                        "copy FormNodeMeta -> QueuedNode break_before={} break_after={} break_target={} event_type=copy_merge",
1119                        meta.page_break_before,
1120                        meta.page_break_after,
1121                        meta.break_target.clone().unwrap_or_default()
1122                    ),
1123                );
1124                QueuedNode {
1125                    id,
1126                    break_before: meta.page_break_before,
1127                    break_after: meta.page_break_after,
1128                    break_target: meta.break_target.clone(),
1129                    children_override: None,
1130                    text_lines_override: None,
1131                    nested_child_overrides: None,
1132                }
1133            })
1134            .collect()
1135    }
1136
1137    fn subtree_is_blank(&self, id: FormNodeId) -> bool {
1138        let node = self.form.get(id);
1139        match &node.node_type {
1140            FormNodeType::Field { value } => value.is_empty(),
1141            FormNodeType::Draw(ref content) => {
1142                if let DrawContent::Text(text) = content {
1143                    text.is_empty()
1144                } else {
1145                    false
1146                }
1147            }
1148            FormNodeType::Image { data, .. } => data.is_empty(),
1149            FormNodeType::Root | FormNodeType::PageSet | FormNodeType::PageArea { .. } => true,
1150            // Subform, Area, ExclGroup, SubformSet: blank when all children blank.
1151            FormNodeType::Subform
1152            | FormNodeType::Area
1153            | FormNodeType::ExclGroup
1154            | FormNodeType::SubformSet => node.children.iter().all(|&c| self.subtree_is_blank(c)),
1155        }
1156    }
1157
1158    /// XFA Spec 3.3 §8.9 — Adhesion (p311-314): whether `current_id` has a
1159    /// keep-with-next constraint relative to `next_id`, or `next_id` has
1160    /// keep-with-previous. Per spec, two adjacent objects adhere if the first
1161    /// declares next=contentArea OR the second declares previous=contentArea.
1162    /// Adhesion is restricted to siblings in the Form DOM (§8.9 p314).
1163    ///
1164    /// TODO §8.9: keep.pageArea level (must be on same page, not just same CA).
1165    #[allow(dead_code)]
1166    fn keep_links_content(&self, current_id: FormNodeId, next_id: FormNodeId) -> bool {
1167        let cur_meta = self.form.meta(current_id);
1168        let nxt_meta = self.form.meta(next_id);
1169        cur_meta.keep_next_content_area
1170            || nxt_meta.keep_previous_content_area
1171            || self.is_spacer_keep_with_next(current_id)
1172    }
1173
1174    /// Compute the cumulative height of a keep-chain starting at
1175    /// `start_idx` within `children`.
1176    #[allow(dead_code)]
1177    fn keep_chain_height(&self, children: &[FormNodeId], start_idx: usize, available: Size) -> f64 {
1178        let mut total = 0.0;
1179        for i in start_idx..children.len() {
1180            let sz = self.compute_extent_with_available(children[i], Some(available));
1181            total += sz.height;
1182            if i + 1 < children.len() && !self.keep_links_content(children[i], children[i + 1]) {
1183                break;
1184            }
1185        }
1186        total
1187    }
1188
1189    /// Compute the cumulative height of a keep-chain starting at
1190    /// `start_idx` within a queued-node slice.
1191    #[allow(dead_code)]
1192    fn queued_keep_chain_height(
1193        &self,
1194        children: &[QueuedNode],
1195        start_idx: usize,
1196        available: Size,
1197    ) -> f64 {
1198        let mut total = 0.0;
1199        for i in start_idx..children.len() {
1200            let sz = self.compute_extent_with_available(children[i].id, Some(available));
1201            total += sz.height;
1202            if i + 1 < children.len()
1203                && !self.keep_links_content(children[i].id, children[i + 1].id)
1204            {
1205                break;
1206            }
1207        }
1208        total
1209    }
1210
1211    /// Returns true when the node is a blank spacer with a keep-intact
1212    /// constraint (used as a "glue" between siblings).
1213    fn is_spacer_keep_with_next(&self, id: FormNodeId) -> bool {
1214        let meta = self.form.meta(id);
1215        meta.keep_intact_content_area && self.subtree_is_blank(id)
1216    }
1217
1218    /// Compute the cumulative height of a keep-chain starting at index
1219    /// `start_idx` within a filtered visible-ids list. The chain extends
1220    /// as long as consecutive nodes have `keep_next_content_area` or the
1221    /// next node has `keep_previous_content_area`.
1222    /// Returns (chain_height, chain_length).  A chain of length 1 means no
1223    /// keep properties link the node to its neighbour — it should NOT trigger
1224    /// a keep-chain page break and instead go through normal overflow/split.
1225    fn visible_keep_chain_height(
1226        &self,
1227        visible_ids: &[(usize, FormNodeId)],
1228        start_idx: usize,
1229        available: Size,
1230    ) -> (f64, usize) {
1231        self.trace_vertical_state(
1232            "visible_keep_chain_height",
1233            Some(0.0),
1234            Some(available.height),
1235            visible_ids.get(start_idx).map(|(_, id)| *id),
1236            format!(
1237                "enter start_idx={} visible_ids={}",
1238                start_idx,
1239                visible_ids.len()
1240            ),
1241        );
1242        let mut total = 0.0;
1243        let mut count = 0usize;
1244        for i in start_idx..visible_ids.len() {
1245            let (_, id) = visible_ids[i];
1246            let sz = self.compute_extent_with_available(id, Some(available));
1247            total += sz.height;
1248            count += 1;
1249            self.trace_vertical_state(
1250                "visible_keep_chain_height",
1251                Some(total),
1252                Some(available.height - total),
1253                Some(id),
1254                format!(
1255                    "accumulate keep chain index={} node_height={:.3} running_total={:.3} count={} event_type=read",
1256                    i,
1257                    sz.height,
1258                    total,
1259                    count
1260                ),
1261            );
1262            // Check if the chain continues to the next node.
1263            if i + 1 < visible_ids.len() {
1264                let (_, next_id) = visible_ids[i + 1];
1265                let cur_meta = self.form.meta(id);
1266                let nxt_meta = self.form.meta(next_id);
1267                let keep = cur_meta.keep_next_content_area
1268                    || nxt_meta.keep_previous_content_area
1269                    || (cur_meta.keep_intact_content_area && self.subtree_is_blank(id));
1270                self.trace_vertical_state(
1271                    "visible_keep_chain_height",
1272                    Some(total),
1273                    Some(available.height - total),
1274                    Some(id),
1275                    format!(
1276                        "keep-chain continuation check next={} cur.keep_next={} next.keep_previous={} cur.keep_intact_blank={} result={} event_type=read",
1277                        self.trace_node_id(next_id),
1278                        cur_meta.keep_next_content_area,
1279                        nxt_meta.keep_previous_content_area,
1280                        cur_meta.keep_intact_content_area && self.subtree_is_blank(id),
1281                        keep
1282                    ),
1283                );
1284                if !keep {
1285                    break;
1286                }
1287            }
1288        }
1289        self.trace_vertical_state(
1290            "visible_keep_chain_height",
1291            Some(total),
1292            Some(available.height - total),
1293            visible_ids.get(start_idx).map(|(_, id)| *id),
1294            format!(
1295                "return chain_height={:.3} chain_len={} event_type=read",
1296                total, count
1297            ),
1298        );
1299        (total, count)
1300    }
1301
1302    fn extract_page_structure(
1303        &self,
1304        root: &FormNode,
1305    ) -> Result<(Vec<PageAreaInfo>, Vec<FormNodeId>)> {
1306        let mut page_areas = Vec::new();
1307        let mut content_nodes = Vec::new();
1308
1309        for &child_id in &root.children {
1310            let child = self.form.get(child_id);
1311            match &child.node_type {
1312                FormNodeType::PageSet => {
1313                    for &pa_id in &child.children {
1314                        let pa_node = self.form.get(pa_id);
1315                        if let FormNodeType::PageArea { content_areas } = &pa_node.node_type {
1316                            let pa_meta = self.form.meta(pa_id);
1317                            let fixed: Vec<FormNodeId> = pa_node
1318                                .children
1319                                .iter()
1320                                .copied()
1321                                .filter(|&cid| {
1322                                    matches!(
1323                                        self.form.get(cid).node_type,
1324                                        FormNodeType::Draw(..)
1325                                            | FormNodeType::Subform
1326                                            | FormNodeType::Area
1327                                            | FormNodeType::ExclGroup
1328                                    )
1329                                })
1330                                .collect();
1331                            page_areas.push(PageAreaInfo {
1332                                name: pa_node.name.clone(),
1333                                xfa_id: pa_meta.xfa_id.clone(),
1334                                content_areas: content_areas.clone(),
1335                                page_width: pa_node.box_model.width.unwrap_or(612.0),
1336                                page_height: pa_node.box_model.height.unwrap_or(792.0),
1337                                fixed_nodes: fixed,
1338                            });
1339                        }
1340                    }
1341                }
1342                FormNodeType::PageArea { content_areas } => {
1343                    let pa_meta = self.form.meta(child_id);
1344                    let fixed: Vec<FormNodeId> = child
1345                        .children
1346                        .iter()
1347                        .copied()
1348                        .filter(|&cid| {
1349                            matches!(
1350                                self.form.get(cid).node_type,
1351                                FormNodeType::Draw(..)
1352                                    | FormNodeType::Subform
1353                                    | FormNodeType::Area
1354                                    | FormNodeType::ExclGroup
1355                            )
1356                        })
1357                        .collect();
1358                    page_areas.push(PageAreaInfo {
1359                        name: child.name.clone(),
1360                        xfa_id: pa_meta.xfa_id.clone(),
1361                        content_areas: content_areas.clone(),
1362                        page_width: child.box_model.width.unwrap_or(612.0),
1363                        page_height: child.box_model.height.unwrap_or(792.0),
1364                        fixed_nodes: fixed,
1365                    });
1366                }
1367                // XFA's canonical nesting: <subform layout="paginate"> wraps
1368                // <pageSet> and content siblings.  Look one level deeper so the
1369                // inner pageSet defines the page geometry and the remaining
1370                // children become the content nodes.  If no pageSet is found
1371                // inside, fall through and treat the subform as content.
1372                // (Fixes xl_02_row_layout.pdf and xl_09_field_types.pdf blank
1373                // render: the pageSet's 792pt height was consuming the full page
1374                // and pushing form content to page 2.)
1375                FormNodeType::Subform
1376                // Area is a positioned container — same page-structure logic as Subform.
1377                | FormNodeType::Area
1378                // ExclGroup is a radio-button group — treat as a TB subform container.
1379                | FormNodeType::ExclGroup => {
1380                    // Only recurse into subforms that directly contain a
1381                    // PageSet child.  Blind recursion into content subforms
1382                    // (especially Positioned ones) would shatter their
1383                    // internal structure and lose breakBefore/overflow
1384                    // semantics.
1385                    let has_pageset = child
1386                        .children
1387                        .iter()
1388                        .any(|&cid| matches!(self.form.get(cid).node_type, FormNodeType::PageSet));
1389                    if has_pageset {
1390                        let (inner_areas, inner_content) = self.extract_page_structure(child)?;
1391                        page_areas.extend(inner_areas);
1392                        content_nodes.extend(inner_content);
1393                    } else if child.layout == LayoutStrategy::TopToBottom {
1394                        // TB subform without inner PageSet — still recurse in
1395                        // case an inner TB child wraps a PageSet.
1396                        let (inner_areas, inner_content) = self.extract_page_structure(child)?;
1397                        if !inner_areas.is_empty() {
1398                            page_areas.extend(inner_areas);
1399                            content_nodes.extend(inner_content);
1400                        } else {
1401                            content_nodes.push(child_id);
1402                        }
1403                    } else {
1404                        content_nodes.push(child_id);
1405                    }
1406                }
1407                // SubformSet is transparent: process its children as direct content.
1408                // XFA 3.3 §7.1 — a subformSet is a conditional grouping with no
1409                // layout contribution of its own.
1410                FormNodeType::SubformSet => {
1411                    let (inner_areas, inner_content) = self.extract_page_structure(child)?;
1412                    page_areas.extend(inner_areas);
1413                    content_nodes.extend(inner_content);
1414                }
1415                _ => {
1416                    content_nodes.push(child_id);
1417                }
1418            }
1419        }
1420
1421        Ok((page_areas, content_nodes))
1422    }
1423
1424    fn layout_content_on_page(
1425        &self,
1426        content_area: &ContentArea,
1427        page_width: f64,
1428        page_height: f64,
1429        content_ids: &[FormNodeId],
1430        strategy: LayoutStrategy,
1431    ) -> Result<LayoutPage> {
1432        let mut page = LayoutPage {
1433            width: page_width,
1434            height: page_height,
1435            nodes: Vec::new(),
1436        };
1437
1438        let available = Size {
1439            width: content_area.width,
1440            height: content_area.height,
1441        };
1442
1443        let nodes = self.layout_children(content_ids, available, strategy)?;
1444
1445        // Offset nodes to content area position
1446        for mut node in nodes {
1447            node.rect.x += content_area.x;
1448            node.rect.y += content_area.y;
1449            page.nodes.push(node);
1450        }
1451
1452        Ok(page)
1453    }
1454
1455    fn profile_page_from_nodes(
1456        &self,
1457        page: &LayoutPage,
1458        content_y: f64,
1459        usable_height: f64,
1460        overflow_to_next: bool,
1461        first_overflow_element: Option<String>,
1462    ) -> LayoutProfilePage {
1463        let used_height = page
1464            .nodes
1465            .iter()
1466            .map(|node| (node.rect.y + node.rect.height) - content_y)
1467            .fold(0.0_f64, f64::max)
1468            .clamp(0.0, usable_height.max(0.0));
1469
1470        LayoutProfilePage {
1471            page_height: usable_height.max(0.0),
1472            used_height,
1473            overflow_to_next,
1474            first_overflow_element,
1475        }
1476    }
1477
1478    /// Lay out page-area fixed nodes (headers, footers, lines) at their
1479    /// absolute positions and prepend them to the page's node list so they
1480    /// render behind flowing content.
1481    fn prepend_fixed_nodes(&self, fixed_ids: &[FormNodeId], page: &mut LayoutPage) -> Result<()> {
1482        if fixed_ids.is_empty() {
1483            return Ok(());
1484        }
1485        let fixed_laid = self.layout_positioned(fixed_ids)?;
1486        let mut merged = fixed_laid;
1487        merged.append(&mut page.nodes);
1488        page.nodes = merged;
1489        Ok(())
1490    }
1491
1492    // TODO #1364 / #1376 audit-followup: refactor the 5-tuple return into a
1493    // dedicated `LayoutFittingResult` struct for readability. Suppressing
1494    // clippy::type_complexity here as an interim step so downstream crates
1495    // (notably pdf-xfa lib tests + clippy) build clean for security/policy
1496    // work — the M8 audit found this baseline blocked validation across
1497    // multiple PRs. Revisit when the layout-engine return shapes get a
1498    // proper type-design pass.
1499    #[allow(clippy::type_complexity)]
1500    fn layout_content_fitting(
1501        &self,
1502        content_area: &ContentArea,
1503        content_ids: &[QueuedNode],
1504        page_width: f64,
1505        page_height: f64,
1506        profile_enabled: bool,
1507    ) -> Result<FittingResult> {
1508        let mut page = LayoutPage {
1509            width: page_width,
1510            height: page_height,
1511            nodes: Vec::new(),
1512        };
1513
1514        // XFA Spec 3.3 §8.10 — Leaders and Trailers (p314-326).
1515        //
1516        // Current implementation: ✅ per-contentArea leader (placed at top) and
1517        // trailer (placed at bottom) on every page that uses the content area,
1518        // including overflow pages.
1519        //
1520        // Not yet implemented:
1521        //   - ⚠️ break leaders/trailers (appear only on page-break pages)
1522        //   - ⚠️ bookend leaders/trailers (appear on first/last occurrence pages)
1523        //   - ⚠️ overflow leaders/trailers with occurrence limits and inheritance
1524        //   - ⚠️ the `<overflow leader="..." trailer="...">` SOM-reference form
1525        //     (currently leaders/trailers must be set on ContentArea directly)
1526        let mut leader_height = 0.0;
1527        let mut trailer_height = 0.0;
1528
1529        if let Some(leader_id) = content_area.leader {
1530            let leader_size = self.compute_extent(leader_id);
1531            leader_height = leader_size.height;
1532            let leader_node = self.form.get(leader_id);
1533            let node = self.layout_single_node(leader_id, leader_node, 0.0, 0.0, None)?;
1534            let mut offset = node;
1535            offset.rect.x += content_area.x;
1536            offset.rect.y += content_area.y;
1537            page.nodes.push(offset);
1538        }
1539
1540        if let Some(trailer_id) = content_area.trailer {
1541            let trailer_size = self.compute_extent(trailer_id);
1542            trailer_height = trailer_size.height;
1543            // Trailer is placed at the bottom of the content area
1544            let trailer_y = content_area.height - trailer_height;
1545            let trailer_node = self.form.get(trailer_id);
1546            let node = self.layout_single_node(trailer_id, trailer_node, 0.0, trailer_y, None)?;
1547            let mut offset = node;
1548            offset.rect.x += content_area.x;
1549            offset.rect.y += content_area.y;
1550            page.nodes.push(offset);
1551        }
1552
1553        // Available height for content = total - leader - trailer.
1554        // Content area height: only extend beyond the declared height if
1555        // the content area reaches close to the page bottom. When there is
1556        // significant space between the content area bottom edge and the
1557        // page bottom (typically occupied by page-area fixed elements like
1558        // footers), respect the declared height so content overflows to the
1559        // next page rather than overlapping fixed chrome.
1560        let effective_ca_height = {
1561            let ca_bottom = content_area.y + content_area.height;
1562            let gap_below_ca = page_height - ca_bottom;
1563            // Allow extension only if the gap below is less than 36pt (~0.5in).
1564            // Larger gaps indicate page-area fixed elements (footers, disclaimers)
1565            // that content should not overlap.
1566            if gap_below_ca < 36.0 {
1567                let remaining_page = page_height - content_area.y;
1568                content_area.height.max(remaining_page)
1569            } else {
1570                content_area.height
1571            }
1572        };
1573        let content_height = effective_ca_height - leader_height - trailer_height;
1574        let available = Size {
1575            width: content_area.width,
1576            height: content_height,
1577        };
1578
1579        let mut y_cursor = leader_height;
1580        let mut placed_count = 0;
1581        let mut split_remaining: Vec<QueuedNode> = Vec::new();
1582        let content_bottom = leader_height + content_height;
1583        let mut consumed_break_only = false;
1584        let break_target = None;
1585        let mut max_used_bottom = 0.0_f64;
1586
1587        // Count leader/trailer nodes placed so far (for force-place detection).
1588        let header_node_count = (if content_area.leader.is_some() { 1 } else { 0 })
1589            + (if content_area.trailer.is_some() { 1 } else { 0 });
1590
1591        // Pre-compute a visible-node list for keep-chain look-ahead.
1592        // Each entry is (index-into-content_ids, FormNodeId).
1593        let visible_ids: Vec<(usize, FormNodeId)> = content_ids
1594            .iter()
1595            .enumerate()
1596            .filter(|(_, qn)| !self.is_layout_hidden(qn.id))
1597            .map(|(i, qn)| (i, qn.id))
1598            .collect();
1599        let mut vis_pos = 0; // current position in visible_ids
1600
1601        self.trace_vertical_state(
1602            "layout_content_fitting",
1603            Some(y_cursor),
1604            Some(content_height),
1605            content_ids.first().map(|queued| queued.id),
1606            format!(
1607                "enter page_width={:.3} page_height={:.3} content_area={} content_nodes={} leader_height={:.3} trailer_height={:.3} effective_ca_height={:.3}",
1608                page_width,
1609                page_height,
1610                content_area.name,
1611                content_ids.len(),
1612                leader_height,
1613                trailer_height,
1614                effective_ca_height
1615            ),
1616        );
1617        self.trace_vertical_state(
1618            "layout_content_fitting",
1619            Some(y_cursor),
1620            Some(content_height),
1621            content_ids.first().map(|queued| queued.id),
1622            format!(
1623                "visible_ids prepared count={} event_type=runtime_allocation",
1624                visible_ids.len()
1625            ),
1626        );
1627
1628        for (idx, qn) in content_ids.iter().enumerate() {
1629            let child_id = qn.id;
1630
1631            // Skip layout-hidden nodes — they consume no space.
1632            if self.is_layout_hidden(child_id) {
1633                placed_count += 1;
1634                continue;
1635            }
1636
1637            self.trace_vertical_state(
1638                "layout_content_fitting",
1639                Some(y_cursor),
1640                Some(content_bottom - y_cursor),
1641                Some(child_id),
1642                format!(
1643                    "loop entry idx={} placed_count={} vis_pos={} break_before={} break_after={}",
1644                    idx, placed_count, vis_pos, qn.break_before, qn.break_after
1645                ),
1646            );
1647
1648            // Handle break_before: if this node requests a page break and
1649            // we already placed content on this page, stop here so the
1650            // caller starts a new page with this node.
1651            self.trace_break_before_read(
1652                y_cursor,
1653                content_bottom - y_cursor,
1654                child_id,
1655                qn.break_before,
1656                placed_count,
1657                header_node_count,
1658            );
1659            if qn.break_before && placed_count > 0 {
1660                // Check if all placed content is blank spacers — if so,
1661                // fold the blank page: mark as consumed_break_only so the
1662                // caller skips the empty page.
1663                let only_blanks = page.nodes.len() <= header_node_count;
1664                if only_blanks {
1665                    consumed_break_only = true;
1666                }
1667                break;
1668            }
1669
1670            let child = self.form.get(child_id);
1671            let child_size = if let Some(ref override_lines) = qn.text_lines_override {
1672                let style_lh = self.form.meta(child_id).style.line_height_pt;
1673                let lh = style_lh.unwrap_or_else(|| child.font.line_height_pt());
1674                let w = self.compute_extent(child_id).width;
1675                Size {
1676                    width: w,
1677                    height: override_lines.len() as f64 * lh,
1678                }
1679            } else {
1680                self.compute_extent_with_available_and_override(
1681                    child_id,
1682                    Some(available),
1683                    qn.children_override.as_deref(),
1684                )
1685            };
1686
1687            // Keep-chain look-ahead: if this node starts a keep chain and
1688            // the chain doesn't fit in remaining space (but WOULD fit on a
1689            // fresh page), break now so the chain starts on the next page.
1690            // Only applies to actual multi-node chains (chain_len > 1), OR
1691            // single non-splittable nodes.  A splittable single node should go
1692            // through normal overflow/split logic instead of being pushed to a
1693            // fresh page, which wastes space and causes over-pagination (#866).
1694            if placed_count > 0 && vis_pos < visible_ids.len() {
1695                let (chain_height, chain_len) =
1696                    self.visible_keep_chain_height(&visible_ids, vis_pos, available);
1697                let remaining_on_page = content_bottom - y_cursor;
1698                let is_single_splittable = chain_len == 1 && self.can_split(child_id);
1699                self.trace_vertical_state(
1700                    "layout_content_fitting",
1701                    Some(y_cursor),
1702                    Some(remaining_on_page),
1703                    Some(child_id),
1704                    format!(
1705                        "keep look-ahead chain_height={:.3} chain_len={} remaining_on_page={:.3} content_height={:.3} is_single_splittable={} event_type=read",
1706                        chain_height,
1707                        chain_len,
1708                        remaining_on_page,
1709                        content_height,
1710                        is_single_splittable
1711                    ),
1712                );
1713                if !is_single_splittable
1714                    && chain_height > remaining_on_page
1715                    && chain_height <= content_height
1716                {
1717                    // Chain (or non-splittable single node) fits on a fresh page — break now.
1718                    self.trace_vertical_state(
1719                        "layout_content_fitting",
1720                        Some(y_cursor),
1721                        Some(remaining_on_page),
1722                        Some(child_id),
1723                        "keep look-ahead fired: current chain does not fit remaining space but fits on a fresh page event_type=rule_decision",
1724                    );
1725                    break;
1726                }
1727                // Unsatisfiable keep chain (exceeds page height):
1728                // fall through and use child's own height for placement.
1729            }
1730
1731            if y_cursor + child_size.height > content_bottom {
1732                let remaining_height = content_bottom - y_cursor;
1733
1734                // M5.4: trace anchor — child overflows current page.
1735                // `available_h` reports the remaining space; `needed_h`
1736                // reports the child's required height. The reason is
1737                // `paginate_defer_to_next_page_min_h` because the
1738                // overflow is triggered by the child not fitting in
1739                // the remaining vertical space (XFA §8.5 minH /
1740                // overflow semantics). Trace is off by default.
1741                let trace_som = self.trace_path_segment(child_id);
1742                trace_sites::paginate(
1743                    &trace_som,
1744                    TraceReason::PaginateDeferToNextPageMinH,
1745                    remaining_height,
1746                    child_size.height,
1747                );
1748
1749                // §8.7 Text leaf splitting at page boundary.
1750                if remaining_height > 0.0
1751                    && (qn.text_lines_override.is_some() || self.is_splittable_text_leaf(child_id))
1752                {
1753                    let lines = if let Some(ref ol) = qn.text_lines_override {
1754                        ol.clone()
1755                    } else {
1756                        let txt = match &child.node_type {
1757                            FormNodeType::Draw(DrawContent::Text(t)) => t.as_str(),
1758                            FormNodeType::Field { value } => value.as_str(),
1759                            _ => "",
1760                        };
1761                        let child_style = &self.form.meta(child_id).style;
1762                        let para_margins = child_style
1763                            .margin_left_pt
1764                            .unwrap_or(crate::types::DEFAULT_TEXT_PADDING)
1765                            + child_style
1766                                .margin_right_pt
1767                                .unwrap_or(crate::types::DEFAULT_TEXT_PADDING);
1768                        let child_border_w = child_style
1769                            .border_width_pt
1770                            .unwrap_or(child.box_model.border_width);
1771                        let insets_w = child.box_model.margins.horizontal()
1772                            + child_border_w * 2.0
1773                            + para_margins;
1774                        let max_w = (child_size.width - insets_w).max(1.0);
1775                        text::wrap_text(
1776                            txt,
1777                            max_w,
1778                            &child.font,
1779                            child_style.text_indent_pt.unwrap_or(0.0),
1780                            child_style.line_height_pt,
1781                        )
1782                        .lines
1783                    };
1784                    let (partial, rest_nodes) =
1785                        self.split_text_node(child_id, y_cursor, remaining_height, &lines)?;
1786                    if partial.rect.height > 0.0 && partial.rect.height <= remaining_height + 1.0 {
1787                        if profile_enabled {
1788                            max_used_bottom = max_used_bottom
1789                                .max((y_cursor + partial.rect.height - leader_height).max(0.0));
1790                        }
1791                        let mut offset_node = partial;
1792                        offset_node.rect.x += content_area.x;
1793                        offset_node.rect.y += content_area.y;
1794                        // M5.4: trace anchor — text leaf successfully
1795                        // split at the page boundary. `available_h`
1796                        // reports the placed partial's height;
1797                        // `needed_h` reports the original child height.
1798                        trace_sites::paginate(
1799                            &trace_som,
1800                            TraceReason::PaginateSplit,
1801                            offset_node.rect.height,
1802                            child_size.height,
1803                        );
1804                        page.nodes.push(offset_node);
1805                        placed_count += 1;
1806                        split_remaining = rest_nodes;
1807                    } else if placed_count > 0 {
1808                        break;
1809                    }
1810                // Try to split this node if it's a splittable container.
1811                } else if remaining_height > 0.0 && self.can_split(child_id) {
1812                    let (partial, rest_nodes) = self.split_tb_node(
1813                        child_id,
1814                        y_cursor,
1815                        remaining_height,
1816                        available,
1817                        qn.children_override.as_deref(),
1818                        qn.nested_child_overrides.as_deref(),
1819                    )?;
1820
1821                    let partial_fits = partial.rect.height <= remaining_height + 1.0;
1822                    let split_productive = !partial.children.is_empty()
1823                        && (partial_fits || partial.children.len() > 1);
1824                    if !partial.children.is_empty() && (partial_fits || split_productive) {
1825                        if profile_enabled {
1826                            max_used_bottom = max_used_bottom
1827                                .max((y_cursor + partial.rect.height - leader_height).max(0.0));
1828                        }
1829                        let mut offset_node = partial;
1830                        offset_node.rect.x += content_area.x;
1831                        offset_node.rect.y += content_area.y;
1832                        // M5.4: trace anchor — container split accepted
1833                        // (top-to-bottom node split at the page
1834                        // boundary).
1835                        trace_sites::paginate(
1836                            &trace_som,
1837                            TraceReason::PaginateSplit,
1838                            offset_node.rect.height,
1839                            child_size.height,
1840                        );
1841                        page.nodes.push(offset_node);
1842                        placed_count += 1;
1843                        split_remaining = rest_nodes;
1844                    } else if placed_count > 0 {
1845                        // Single oversized child doesn't fit and page has
1846                        // content — defer to a fresh page where re-split
1847                        // with full height can do better.
1848                        break;
1849                    }
1850                } else if idx == 0 || page.nodes.len() <= header_node_count {
1851                    // First content item too large and can't split — force place it
1852                    let x = self.child_h_align_offset(child_id, child_size.width, available.width);
1853                    if profile_enabled {
1854                        max_used_bottom = max_used_bottom
1855                            .max((y_cursor + child_size.height - leader_height).max(0.0));
1856                    }
1857                    let node = self.layout_single_node_with_extent(
1858                        child_id,
1859                        child,
1860                        x,
1861                        y_cursor,
1862                        child_size,
1863                        qn.children_override.as_deref(),
1864                    )?;
1865                    let mut offset_node = node;
1866                    offset_node.rect.x += content_area.x;
1867                    offset_node.rect.y += content_area.y;
1868                    page.nodes.push(offset_node);
1869                    placed_count += 1;
1870                }
1871                break;
1872            }
1873
1874            // Proactive split: if the child fits on the page but contains
1875            // inner page-break-before children, split it using the FULL
1876            // content height so the inner break is detected.
1877            if self.has_inner_break(child_id) && self.can_split(child_id) {
1878                let (partial, rest_nodes) = self.split_tb_node(
1879                    child_id,
1880                    y_cursor,
1881                    content_height,
1882                    available,
1883                    qn.children_override.as_deref(),
1884                    qn.nested_child_overrides.as_deref(),
1885                )?;
1886                let remaining_on_page = content_bottom - y_cursor;
1887                if !partial.children.is_empty() && partial.rect.height <= remaining_on_page {
1888                    if profile_enabled {
1889                        max_used_bottom = max_used_bottom
1890                            .max((y_cursor + partial.rect.height - leader_height).max(0.0));
1891                    }
1892                    let mut offset_node = partial;
1893                    offset_node.rect.x += content_area.x;
1894                    offset_node.rect.y += content_area.y;
1895                    page.nodes.push(offset_node);
1896                    placed_count += 1;
1897                    split_remaining = rest_nodes;
1898                } else if placed_count > 0 {
1899                    break;
1900                } else {
1901                    if !partial.children.is_empty() {
1902                        if profile_enabled {
1903                            max_used_bottom = max_used_bottom
1904                                .max((y_cursor + partial.rect.height - leader_height).max(0.0));
1905                        }
1906                        let mut offset_node = partial;
1907                        offset_node.rect.x += content_area.x;
1908                        offset_node.rect.y += content_area.y;
1909                        page.nodes.push(offset_node);
1910                    }
1911                    placed_count += 1;
1912                    split_remaining = rest_nodes;
1913                }
1914                break;
1915            }
1916
1917            // XFA Spec 3.3 §8.3 — hAlign positions child within content area
1918            let x = self.child_h_align_offset(child_id, child_size.width, available.width);
1919            let node = if let Some(ref override_lines) = qn.text_lines_override {
1920                let child_style = &self.form.meta(child_id).style;
1921                LayoutNode {
1922                    form_node: child_id,
1923                    rect: Rect::new(x, y_cursor, child_size.width, child_size.height),
1924                    name: child.name.clone(),
1925                    content: LayoutContent::WrappedText {
1926                        lines: override_lines.clone(),
1927                        first_line_of_para: vec![false; override_lines.len()],
1928                        font_size: child.font.size,
1929                        text_align: child.font.text_align,
1930                        font_family: child.font.typeface,
1931                        space_above_pt: child_style.space_above_pt,
1932                        space_below_pt: child_style.space_below_pt,
1933                        from_field: matches!(child.node_type, FormNodeType::Field { .. }),
1934                    },
1935                    children: Vec::new(),
1936                    style: self.form.meta(child_id).style.clone(),
1937                    display_items: self.form.meta(child_id).display_items.clone(),
1938                    save_items: self.form.meta(child_id).save_items.clone(),
1939                }
1940            } else {
1941                self.layout_single_node_with_extent(
1942                    child_id,
1943                    child,
1944                    x,
1945                    y_cursor,
1946                    child_size,
1947                    qn.children_override.as_deref(),
1948                )?
1949            };
1950            if profile_enabled {
1951                max_used_bottom =
1952                    max_used_bottom.max((y_cursor + child_size.height - leader_height).max(0.0));
1953            }
1954            let mut offset_node = node;
1955            offset_node.rect.x += content_area.x;
1956            offset_node.rect.y += content_area.y;
1957            page.nodes.push(offset_node);
1958
1959            y_cursor += child_size.height;
1960            placed_count += 1;
1961            vis_pos += 1;
1962
1963            self.trace_vertical_state(
1964                "layout_content_fitting",
1965                Some(y_cursor),
1966                Some(content_bottom - y_cursor),
1967                Some(child_id),
1968                format!(
1969                    "placed node child_height={:.3} new_y_cursor={:.3} placed_count={} vis_pos={}",
1970                    child_size.height, y_cursor, placed_count, vis_pos
1971                ),
1972            );
1973
1974            if qn.break_after {
1975                self.trace_vertical_state(
1976                    "layout_content_fitting",
1977                    Some(y_cursor),
1978                    Some(content_bottom - y_cursor),
1979                    Some(child_id),
1980                    format!("break_after check break_after={}", qn.break_after),
1981                );
1982                break;
1983            }
1984        }
1985
1986        let mut remaining = split_remaining;
1987        remaining.extend(content_ids[placed_count..].iter().cloned());
1988        self.trace_vertical_state(
1989            "layout_content_fitting",
1990            Some(y_cursor),
1991            Some(content_bottom - y_cursor),
1992            remaining.first().map(|queued| queued.id),
1993            format!(
1994                "return remaining_nodes={} consumed_break_only={} first_remaining={}",
1995                remaining.len(),
1996                consumed_break_only,
1997                remaining
1998                    .first()
1999                    .map(|queued| self.describe_queued_node(queued))
2000                    .unwrap_or_else(|| "none".to_string())
2001            ),
2002        );
2003        let page_profile = if profile_enabled {
2004            Some(LayoutProfilePage {
2005                page_height: content_height.max(0.0),
2006                used_height: max_used_bottom.clamp(0.0, content_height.max(0.0)),
2007                overflow_to_next: !remaining.is_empty() && !consumed_break_only,
2008                first_overflow_element: if !remaining.is_empty() && !consumed_break_only {
2009                    Some(self.describe_queued_node(&remaining[0]))
2010                } else {
2011                    None
2012                },
2013            })
2014        } else {
2015            None
2016        };
2017        Ok((
2018            page,
2019            remaining,
2020            consumed_break_only,
2021            break_target,
2022            page_profile,
2023        ))
2024    }
2025
2026    fn describe_queued_node(&self, queued: &QueuedNode) -> String {
2027        let node = self.form.get(queued.id);
2028        let identifier = self
2029            .form
2030            .meta(queued.id)
2031            .xfa_id
2032            .clone()
2033            .filter(|id| !id.is_empty())
2034            .or_else(|| (!node.name.is_empty()).then(|| node.name.clone()))
2035            .unwrap_or_else(|| queued.id.0.to_string());
2036        let height = self
2037            .compute_extent_with_available_and_override(
2038                queued.id,
2039                None,
2040                queued.children_override.as_deref(),
2041            )
2042            .height;
2043        format!(
2044            "{}#{} (h={height:.1})",
2045            Self::form_node_type_name(&node.node_type),
2046            identifier
2047        )
2048    }
2049
2050    fn form_node_type_name(node_type: &FormNodeType) -> &'static str {
2051        match node_type {
2052            FormNodeType::Root => "root",
2053            FormNodeType::PageSet => "pageSet",
2054            FormNodeType::PageArea { .. } => "pageArea",
2055            FormNodeType::Subform => "subform",
2056            FormNodeType::Area => "area",
2057            FormNodeType::ExclGroup => "exclGroup",
2058            FormNodeType::SubformSet => "subformSet",
2059            FormNodeType::Field { .. } => "field",
2060            FormNodeType::Draw(_) => "draw",
2061            FormNodeType::Image { .. } => "image",
2062        }
2063    }
2064
2065    /// XFA Spec 3.3 §8.7 — Content Splitting (p290): determines whether a node
2066    /// can be split across pages. Per Appendix B (p1520), subforms are splittable
2067    /// "in margins and where consensus exists among contained objects".
2068    ///
2069    /// Split restrictions (§8.7 p291): barcode, geometric figure, image = no split.
2070    /// Text = split between lines only. Widget = no split.
2071    /// keep.intact controls: none (free), contentArea (within CA), pageArea (within page).
2072    ///
2073    /// TODO §8.7: text-level splitting (between lines), orphan/widow controls,
2074    /// split consensus algorithm (p294), per-type default intact values.
2075    fn can_split(&self, id: FormNodeId) -> bool {
2076        let node = self.form.get(id);
2077        if node.children.is_empty() {
2078            return self.is_splittable_text_leaf(id);
2079        }
2080        if let Some(explicit_h) = node.box_model.height {
2081            // TB subforms with explicit height: allow splitting only when
2082            // content actually overflows the declared height (#768).
2083            if node.layout == LayoutStrategy::TopToBottom {
2084                let extent = self.compute_extent(id);
2085                return extent.height > explicit_h;
2086            }
2087            return false;
2088        }
2089        // (#764) Positioned subforms with a finite maxH are bounded — treat
2090        // them like explicit-height nodes and don't split.
2091        if node.layout == LayoutStrategy::Positioned && node.box_model.max_height < f64::MAX {
2092            return false;
2093        }
2094        matches!(
2095            node.layout,
2096            LayoutStrategy::TopToBottom
2097                | LayoutStrategy::LeftToRightTB
2098                | LayoutStrategy::RightToLeftTB
2099                | LayoutStrategy::Table
2100                | LayoutStrategy::Positioned
2101        )
2102    }
2103
2104    /// Check if a childless node is a text leaf that can be split between lines.
2105    ///
2106    /// XFA Spec 3.3 §8.7 (p291): text may be split between lines.
2107    /// Images, barcodes, geometric figures, and widgets may NOT be split.
2108    fn is_splittable_text_leaf(&self, id: FormNodeId) -> bool {
2109        let node = self.form.get(id);
2110        if !node.children.is_empty() {
2111            return false;
2112        }
2113        if self.form.meta(id).keep_intact_content_area {
2114            return false;
2115        }
2116        let style = &self.form.meta(id).style;
2117        let para_margins = style
2118            .margin_left_pt
2119            .unwrap_or(crate::types::DEFAULT_TEXT_PADDING)
2120            + style
2121                .margin_right_pt
2122                .unwrap_or(crate::types::DEFAULT_TEXT_PADDING);
2123        match &node.node_type {
2124            FormNodeType::Draw(DrawContent::Text(t)) => {
2125                let line_count = text::wrap_text(
2126                    t,
2127                    (node.box_model.content_width() - para_margins).max(1.0),
2128                    &node.font,
2129                    style.text_indent_pt.unwrap_or(0.0),
2130                    style.line_height_pt,
2131                )
2132                .lines
2133                .len();
2134                line_count > 1
2135            }
2136            FormNodeType::Field { value } if !value.is_empty() => {
2137                let line_count = text::wrap_text(
2138                    value,
2139                    (node.box_model.content_width() - para_margins).max(1.0),
2140                    &node.font,
2141                    style.text_indent_pt.unwrap_or(0.0),
2142                    style.line_height_pt,
2143                )
2144                .lines
2145                .len();
2146                line_count > 1
2147            }
2148            _ => false,
2149        }
2150    }
2151
2152    /// Split a text leaf node at a line boundary so the first portion fits
2153    /// within `remaining_height`.
2154    ///
2155    /// XFA Spec 3.3 §8.7 (p291): text may be split between lines only.
2156    fn split_text_node(
2157        &self,
2158        id: FormNodeId,
2159        y_offset: f64,
2160        remaining_height: f64,
2161        lines: &[String],
2162    ) -> Result<(LayoutNode, Vec<QueuedNode>)> {
2163        let node = self.form.get(id);
2164        let style_lh = self.form.meta(id).style.line_height_pt;
2165        let lh = style_lh.unwrap_or_else(|| node.font.line_height_pt());
2166        let split_points = text::text_split_points(lines.len(), lh);
2167
2168        let mut split_at = 0;
2169        for &sp in &split_points {
2170            if sp <= remaining_height + 0.5 {
2171                split_at += 1;
2172            } else {
2173                break;
2174            }
2175        }
2176
2177        if split_at == 0 {
2178            let full_height = lines.len() as f64 * lh;
2179            let split_style = &self.form.meta(id).style;
2180            let full_node = LayoutNode {
2181                form_node: id,
2182                rect: Rect::new(0.0, y_offset, self.compute_extent(id).width, full_height),
2183                name: node.name.clone(),
2184                content: LayoutContent::WrappedText {
2185                    lines: lines.to_vec(),
2186                    first_line_of_para: vec![false; lines.len()],
2187                    font_size: node.font.size,
2188                    text_align: node.font.text_align,
2189                    font_family: node.font.typeface,
2190                    space_above_pt: split_style.space_above_pt,
2191                    space_below_pt: split_style.space_below_pt,
2192                    from_field: matches!(node.node_type, FormNodeType::Field { .. }),
2193                },
2194                children: Vec::new(),
2195                style: self.form.meta(id).style.clone(),
2196                display_items: self.form.meta(id).display_items.clone(),
2197                save_items: self.form.meta(id).save_items.clone(),
2198            };
2199            return Ok((full_node, Vec::new()));
2200        }
2201
2202        let top_lines: Vec<String> = lines[..split_at].to_vec();
2203        let bottom_lines: Vec<String> = lines[split_at..].to_vec();
2204        let partial_height = split_at as f64 * lh;
2205        let node_width = self.compute_extent(id).width;
2206        let split_style = &self.form.meta(id).style;
2207
2208        let partial_node = LayoutNode {
2209            form_node: id,
2210            rect: Rect::new(0.0, y_offset, node_width, partial_height),
2211            name: node.name.clone(),
2212            content: LayoutContent::WrappedText {
2213                lines: top_lines.clone(),
2214                first_line_of_para: vec![false; top_lines.len()],
2215                font_size: node.font.size,
2216                text_align: node.font.text_align,
2217                font_family: node.font.typeface,
2218                space_above_pt: split_style.space_above_pt,
2219                space_below_pt: split_style.space_below_pt,
2220                from_field: matches!(node.node_type, FormNodeType::Field { .. }),
2221            },
2222            children: Vec::new(),
2223            style: self.form.meta(id).style.clone(),
2224            display_items: self.form.meta(id).display_items.clone(),
2225            save_items: self.form.meta(id).save_items.clone(),
2226        };
2227
2228        let rest = if bottom_lines.is_empty() {
2229            Vec::new()
2230        } else {
2231            vec![QueuedNode {
2232                id,
2233                break_before: false,
2234                break_after: self.form.meta(id).page_break_after,
2235                break_target: None,
2236                children_override: None,
2237                text_lines_override: Some(bottom_lines),
2238                nested_child_overrides: None,
2239            }]
2240        };
2241
2242        Ok((partial_node, rest))
2243    }
2244
2245    /// Check if any direct (expanded) child of a tb-layout subform has
2246    /// `page_break_before` set, meaning the node must be split at that
2247    /// point even if it fits in the remaining space.
2248    fn has_inner_break(&self, id: FormNodeId) -> bool {
2249        let node = self.form.get(id);
2250        if !matches!(
2251            node.layout,
2252            LayoutStrategy::TopToBottom
2253                | LayoutStrategy::LeftToRightTB
2254                | LayoutStrategy::RightToLeftTB
2255        ) {
2256            return false;
2257        }
2258        let expanded = self.expand_occur(&node.children);
2259        expanded
2260            .iter()
2261            .any(|&cid| self.form.meta(cid).page_break_before)
2262    }
2263
2264    /// XFA Spec 3.3 §8.7 — Content Splitting (p290-294): split a tb-layout node
2265    /// by placing children that fit in `remaining_height`, returning the rest.
2266    ///
2267    /// Per §8.7 p294 "split consensus": the split location should be the lowest
2268    /// point acceptable to all contained objects. Our implementation splits at
2269    /// child boundaries (not within text lines).
2270    ///
2271    /// Respects keep constraints (§8.9 Adhesion): if a child has
2272    /// `keep_next_content_area`, the split will not occur between that child
2273    /// and its successor. Also respects `page_break_before` as mandatory splits.
2274    fn split_tb_node(
2275        &self,
2276        id: FormNodeId,
2277        y_offset: f64,
2278        remaining_height: f64,
2279        _available: Size,
2280        children_override: Option<&[FormNodeId]>,
2281        nested_overrides: Option<&[(FormNodeId, Vec<FormNodeId>)]>,
2282    ) -> Result<(LayoutNode, Vec<QueuedNode>)> {
2283        let node = self.form.get(id);
2284
2285        // Delegate to positioned splitter for positioned subforms (#736).
2286        if node.layout == LayoutStrategy::Positioned {
2287            return self.split_positioned_node(id, y_offset, remaining_height, children_override);
2288        }
2289
2290        let node_children = children_override.unwrap_or(&node.children);
2291        // (#764) When children_override is set, the list already contains
2292        // occur-expanded IDs from a previous split.  Re-expanding would
2293        // duplicate children on every page, causing infinite pagination.
2294        let expanded_children = if children_override.is_some() {
2295            node_children.to_vec()
2296        } else {
2297            self.expand_occur(node_children)
2298        };
2299
2300        let mut placed_children = Vec::new();
2301        let mut child_y = 0.0;
2302        let mut split_idx = 0;
2303        // Track the last valid split point (respecting keep constraints).
2304        let mut last_valid_split = 0;
2305        let mut last_valid_y = 0.0_f64;
2306        let mut split_rest_override: Option<Vec<QueuedNode>> = None;
2307
2308        for (i, &child_id) in expanded_children.iter().enumerate() {
2309            let child = self.form.get(child_id);
2310            // Look up per-child override from a previous nested split so
2311            // that partially-split children use their reduced children list.
2312            let child_co = nested_overrides
2313                .and_then(|no| no.iter().find(|(nid, _)| *nid == child_id))
2314                .map(|(_, co)| co.as_slice());
2315            let child_size = self.compute_extent_with_override(child_id, child_co);
2316            let child_meta = self.form.meta(child_id);
2317
2318            // If this child has keep_intact and doesn't fit, split BEFORE it
2319            // so it moves to the next page entirely.
2320            if child_meta.keep_intact_content_area
2321                && child_y + child_size.height > remaining_height
2322                && !placed_children.is_empty()
2323            {
2324                split_idx = i;
2325                break;
2326            }
2327
2328            // §8.7 Text leaf splitting: if the overflowing child is a text
2329            // leaf, split at a line boundary instead of a child boundary.
2330            if child_y + child_size.height > remaining_height
2331                && !child_meta.keep_intact_content_area
2332                && self.is_splittable_text_leaf(child_id)
2333            {
2334                let child_remaining = (remaining_height - child_y).max(0.0);
2335                if child_remaining > 0.0 {
2336                    let cnode = self.form.get(child_id);
2337                    let txt = match &cnode.node_type {
2338                        FormNodeType::Draw(DrawContent::Text(t)) => t.as_str(),
2339                        FormNodeType::Field { value } => value.as_str(),
2340                        _ => "",
2341                    };
2342                    let cstyle = &self.form.meta(child_id).style;
2343                    let cpara = cstyle
2344                        .margin_left_pt
2345                        .unwrap_or(crate::types::DEFAULT_TEXT_PADDING)
2346                        + cstyle
2347                            .margin_right_pt
2348                            .unwrap_or(crate::types::DEFAULT_TEXT_PADDING);
2349                    let cborder_w = cstyle
2350                        .border_width_pt
2351                        .unwrap_or(cnode.box_model.border_width);
2352                    let insets_w = cnode.box_model.margins.horizontal() + cborder_w * 2.0 + cpara;
2353                    let max_w = (self.compute_extent(child_id).width - insets_w).max(1.0);
2354                    let wrapped = text::wrap_text(
2355                        txt,
2356                        max_w,
2357                        &cnode.font,
2358                        cstyle.text_indent_pt.unwrap_or(0.0),
2359                        cstyle.line_height_pt,
2360                    );
2361                    let (partial_child, child_rest) =
2362                        self.split_text_node(child_id, child_y, child_remaining, &wrapped.lines)?;
2363
2364                    if partial_child.rect.height > 0.0
2365                        && partial_child.rect.height <= child_remaining + 0.5
2366                    {
2367                        placed_children.push(partial_child);
2368                        child_y += placed_children.last().unwrap().rect.height;
2369                        split_idx = i + 1;
2370
2371                        let mut rest: Vec<QueuedNode> = child_rest;
2372                        rest.extend(expanded_children[i + 1..].iter().map(|&cid| QueuedNode {
2373                            id: cid,
2374                            break_before: self.form.meta(cid).page_break_before,
2375                            break_after: self.form.meta(cid).page_break_after,
2376                            break_target: None,
2377                            children_override: None,
2378                            text_lines_override: None,
2379                            nested_child_overrides: None,
2380                        }));
2381                        split_rest_override = Some(rest);
2382                        break;
2383                    }
2384                }
2385            }
2386
2387            // If the next child itself is a splittable container and it is
2388            // the first overflowing child, split it recursively instead of
2389            // forcing the entire container onto a single page.
2390            if child_y + child_size.height > remaining_height
2391                && !child_meta.keep_intact_content_area
2392                && self.can_split(child_id)
2393            {
2394                let child_remaining = (remaining_height - child_y).max(0.0);
2395                if child_remaining > 0.0 {
2396                    let child_available = Size {
2397                        width: child.box_model.content_width().min(child_size.width),
2398                        height: child.box_model.content_height().min(child_size.height),
2399                    };
2400                    let (partial_child, child_rest) = self.split_tb_node(
2401                        child_id,
2402                        child_y,
2403                        child_remaining,
2404                        child_available,
2405                        child_co,
2406                        nested_overrides,
2407                    )?;
2408
2409                    let partial_fits = partial_child.rect.height <= child_remaining + 1.0;
2410                    let split_productive = !partial_child.children.is_empty()
2411                        && (partial_fits || partial_child.children.len() > 1);
2412
2413                    if split_productive {
2414                        placed_children.push(partial_child);
2415                        child_y += placed_children.last().unwrap().rect.height;
2416                        split_idx = i + 1;
2417
2418                        // When the recursive split returns a single QueuedNode
2419                        // that wraps the same parent (e.g. a positioned subform
2420                        // split via split_positioned_node), preserve its
2421                        // children_override so the next page only processes the
2422                        // remaining children.  Without this, the override is
2423                        // discarded and the full subform is re-split every page,
2424                        // causing an infinite pagination loop (#737).
2425                        //
2426                        // For other cases (multiple QueuedNodes, or a single
2427                        // node for a different child), collect IDs as before
2428                        // but also preserve any children_override from each
2429                        // node as nested_child_overrides.  This prevents
2430                        // duplication when a deeply nested child was partially
2431                        // split — without this, its override is lost and the
2432                        // child re-renders all content on the next page.
2433                        let (child_override, child_nested) = if child_rest.len() == 1
2434                            && child_rest[0].id == child_id
2435                            && child_rest[0].children_override.is_some()
2436                        {
2437                            let qn = child_rest.into_iter().next().unwrap();
2438                            (qn.children_override, qn.nested_child_overrides)
2439                        } else {
2440                            let mut ids = Vec::new();
2441                            let mut nested = Vec::new();
2442                            for qn in child_rest {
2443                                ids.push(qn.id);
2444                                if let Some(co) = qn.children_override {
2445                                    nested.push((qn.id, co));
2446                                }
2447                                if let Some(nco) = qn.nested_child_overrides {
2448                                    nested.extend(nco);
2449                                }
2450                            }
2451                            (
2452                                Some(ids),
2453                                if nested.is_empty() {
2454                                    None
2455                                } else {
2456                                    Some(nested)
2457                                },
2458                            )
2459                        };
2460
2461                        let mut rest = vec![QueuedNode {
2462                            id: child_id,
2463                            break_before: false,
2464                            break_after: self.form.meta(child_id).page_break_after,
2465                            break_target: None,
2466                            children_override: child_override,
2467                            text_lines_override: None,
2468                            nested_child_overrides: child_nested,
2469                        }];
2470                        rest.extend(expanded_children[i + 1..].iter().map(|&cid| QueuedNode {
2471                            id: cid,
2472                            break_before: self.form.meta(cid).page_break_before,
2473                            break_after: self.form.meta(cid).page_break_after,
2474                            break_target: None,
2475                            children_override: None,
2476                            text_lines_override: None,
2477                            nested_child_overrides: None,
2478                        }));
2479                        split_rest_override = Some(rest);
2480                        break;
2481                    }
2482                }
2483            }
2484
2485            // Overflow detection: child doesn't fit in remaining space.
2486            // 0.5pt tolerance for sub-point rounding (#971).
2487            if child_y + child_size.height > remaining_height + 0.5 && !placed_children.is_empty() {
2488                // Overflow: split at the last valid split point.
2489                if last_valid_split > 0 && last_valid_split < placed_children.len() {
2490                    // Trim placed_children to the last valid split point.
2491                    placed_children.truncate(last_valid_split);
2492                    child_y = last_valid_y;
2493                    split_idx = last_valid_split;
2494                } else {
2495                    split_idx = i;
2496                }
2497                break;
2498            }
2499
2500            let child_node = self.layout_single_node(child_id, child, 0.0, child_y, child_co)?;
2501            placed_children.push(child_node);
2502            child_y += child_size.height;
2503            split_idx = i + 1;
2504
2505            // Check if this is a valid split point (no keep constraint).
2506            let has_keep = child_meta.keep_next_content_area;
2507            let next_has_keep_prev = if i + 1 < expanded_children.len() {
2508                self.form
2509                    .meta(expanded_children[i + 1])
2510                    .keep_previous_content_area
2511            } else {
2512                false
2513            };
2514            if !has_keep && !next_has_keep_prev {
2515                last_valid_split = split_idx;
2516                last_valid_y = child_y;
2517            }
2518        }
2519
2520        let content = match &node.node_type {
2521            FormNodeType::Field { value } => {
2522                let meta = self.form.meta(id);
2523                LayoutContent::Field {
2524                    value: resolve_display_value(value, meta).to_string(),
2525                    field_kind: meta.field_kind,
2526                    font_size: node.font.size,
2527                    font_family: node.font.typeface,
2528                }
2529            }
2530            FormNodeType::Draw(DrawContent::Text(content)) => LayoutContent::Text(content.clone()),
2531            FormNodeType::Draw(dc) => LayoutContent::Draw(dc.clone()),
2532            FormNodeType::Image { data, mime_type } => LayoutContent::Image {
2533                data: data.clone(),
2534                mime_type: mime_type.clone(),
2535            },
2536            _ => LayoutContent::None,
2537        };
2538
2539        // Compute partial extent: full width, height = content that fit
2540        let partial_width = self
2541            .compute_extent_with_override(id, children_override)
2542            .width;
2543
2544        let partial_node = LayoutNode {
2545            form_node: id,
2546            rect: Rect::new(0.0, y_offset, partial_width, child_y),
2547            name: node.name.clone(),
2548            content,
2549            children: placed_children,
2550            style: self.form.meta(id).style.clone(),
2551            display_items: self.form.meta(id).display_items.clone(),
2552            save_items: self.form.meta(id).save_items.clone(),
2553        };
2554
2555        let rest = split_rest_override.unwrap_or_else(|| {
2556            expanded_children[split_idx..]
2557                .iter()
2558                .map(|&cid| QueuedNode {
2559                    id: cid,
2560                    break_before: self.form.meta(cid).page_break_before,
2561                    break_after: self.form.meta(cid).page_break_after,
2562                    break_target: None,
2563                    children_override: None,
2564                    text_lines_override: None,
2565                    nested_child_overrides: None,
2566                })
2567                .collect()
2568        });
2569        Ok((partial_node, rest))
2570    }
2571
2572    /// Split a positioned-layout subform across pages (#736).
2573    ///
2574    /// Children are sorted by their y-coordinate.  Those whose bottom edge
2575    /// (y + height, shifted by y_base) fits within `remaining_height` are
2576    /// placed on the current page.  Remaining children are returned for
2577    /// layout on subsequent pages.
2578    ///
2579    /// When called for overflow children (via `children_override`), a y_base
2580    /// shift is computed from the minimum y-position so they render starting
2581    /// near the top of the new page.  The same shift is applied by
2582    /// `compute_extent_with_available_and_override` so the reported height
2583    /// is consistent.
2584    fn split_positioned_node(
2585        &self,
2586        id: FormNodeId,
2587        y_offset: f64,
2588        remaining_height: f64,
2589        children_override: Option<&[FormNodeId]>,
2590    ) -> Result<(LayoutNode, Vec<QueuedNode>)> {
2591        let node = self.form.get(id);
2592        let node_children = children_override.unwrap_or(&node.children);
2593        // (#764) Skip occur re-expansion when override is set — it already
2594        // contains expanded IDs from the previous split.
2595        let expanded_children = if children_override.is_some() {
2596            node_children.to_vec()
2597        } else {
2598            self.expand_occur(node_children)
2599        };
2600
2601        // Sort children by their y-position for deterministic splitting.
2602        let mut sorted: Vec<FormNodeId> = expanded_children.clone();
2603        sorted.sort_by(|&a, &b| {
2604            let ay = self.form.get(a).box_model.y;
2605            let by = self.form.get(b).box_model.y;
2606            ay.partial_cmp(&by).unwrap_or(std::cmp::Ordering::Equal)
2607        });
2608
2609        // When processing overflow children (children_override is set),
2610        // shift all y-positions so the topmost child starts at y=0.
2611        // This mirrors the y_base logic in compute_extent.
2612        let y_base = if children_override.is_some() {
2613            sorted
2614                .first()
2615                .map(|&cid| self.form.get(cid).box_model.y)
2616                .unwrap_or(0.0)
2617        } else {
2618            0.0
2619        };
2620        let parent_margin_x = node.box_model.margins.left;
2621        let parent_margin_y = node.box_model.margins.top;
2622
2623        let mut placed_children = Vec::new();
2624        let mut rest_children = Vec::new();
2625        let mut max_placed_bottom = 0.0_f64;
2626
2627        for &child_id in &sorted {
2628            let child = self.form.get(child_id);
2629            let child_size = self.compute_extent(child_id);
2630            let shifted_y = child.box_model.y - y_base + parent_margin_y;
2631            let child_bottom = shifted_y + child_size.height;
2632
2633            if child_bottom <= remaining_height + 1.0 {
2634                // Child fits on this page -- place at shifted position.
2635                let child_node = self.layout_single_node(
2636                    child_id,
2637                    child,
2638                    child.box_model.x + parent_margin_x,
2639                    shifted_y,
2640                    None,
2641                )?;
2642                max_placed_bottom = max_placed_bottom.max(child_bottom);
2643                placed_children.push(child_node);
2644            } else {
2645                // Child overflows -- defer to next page.
2646                rest_children.push(child_id);
2647            }
2648        }
2649
2650        // (#764) Ensure progress: if nothing was placed, force-place the
2651        // first child so the split always advances.  Without this guard,
2652        // positioned subforms whose first child exceeds the remaining
2653        // height return an empty partial on every page, causing the
2654        // caller to loop indefinitely.
2655        if placed_children.is_empty() && !sorted.is_empty() {
2656            let first_id = sorted[0];
2657            let first = self.form.get(first_id);
2658            let first_size = self.compute_extent(first_id);
2659            let shifted_y = first.box_model.y - y_base + parent_margin_y;
2660            let child_node = self.layout_single_node(
2661                first_id,
2662                first,
2663                first.box_model.x + parent_margin_x,
2664                shifted_y,
2665                None,
2666            )?;
2667            max_placed_bottom = shifted_y + first_size.height;
2668            placed_children.push(child_node);
2669            // Remove the force-placed child from rest.
2670            rest_children.retain(|&cid| cid != first_id);
2671        }
2672
2673        let content = match &node.node_type {
2674            FormNodeType::Field { value } => {
2675                let meta = self.form.meta(id);
2676                LayoutContent::Field {
2677                    value: resolve_display_value(value, meta).to_string(),
2678                    field_kind: meta.field_kind,
2679                    font_size: node.font.size,
2680                    font_family: node.font.typeface,
2681                }
2682            }
2683            FormNodeType::Draw(DrawContent::Text(content)) => LayoutContent::Text(content.clone()),
2684            FormNodeType::Draw(dc) => LayoutContent::Draw(dc.clone()),
2685            FormNodeType::Image { data, mime_type } => LayoutContent::Image {
2686                data: data.clone(),
2687                mime_type: mime_type.clone(),
2688            },
2689            _ => LayoutContent::None,
2690        };
2691
2692        let partial_width = self
2693            .compute_extent_with_override(id, children_override)
2694            .width;
2695
2696        let partial_node = LayoutNode {
2697            form_node: id,
2698            rect: Rect::new(0.0, y_offset, partial_width, max_placed_bottom),
2699            name: node.name.clone(),
2700            content,
2701            children: placed_children,
2702            style: self.form.meta(id).style.clone(),
2703            display_items: self.form.meta(id).display_items.clone(),
2704            save_items: self.form.meta(id).save_items.clone(),
2705        };
2706
2707        // Remaining children are wrapped in a QueuedNode for the same
2708        // parent with children_override.  On the next page, compute_extent
2709        // will apply y_base shifting to produce a correct relative height,
2710        // and split_positioned_node will shift y-positions so children
2711        // render near the top of the new page.
2712        let rest = if rest_children.is_empty() {
2713            Vec::new()
2714        } else {
2715            vec![QueuedNode {
2716                id,
2717                break_before: false,
2718                break_after: self.form.meta(id).page_break_after,
2719                break_target: None,
2720                children_override: Some(rest_children),
2721                text_lines_override: None,
2722                nested_child_overrides: None,
2723            }]
2724        };
2725
2726        Ok((partial_node, rest))
2727    }
2728
2729    /// Layout children within available space using the given strategy.
2730    ///
2731    /// Children with `occur.count() > 1` are expanded into multiple instances.
2732    /// Primary layout hot path.
2733    ///
2734    /// Performance: this function is called recursively for every container in
2735    /// the form tree (n = total nodes) and for every page during pagination
2736    /// (m = pages).  The effective complexity is O(n * m) in the worst case
2737    /// (e.g. 100-occurrence repeating subforms with overflow across 50 pages).
2738    ///
2739    /// Known hotspot: `expand_occur` allocates a new Vec on every call.
2740    /// Memoizing subform heights for repeated occurrences of the same
2741    /// `FormNodeId` would eliminate redundant `compute_extent` calls.
2742    fn layout_children(
2743        &self,
2744        children: &[FormNodeId],
2745        available: Size,
2746        strategy: LayoutStrategy,
2747    ) -> Result<Vec<LayoutNode>> {
2748        let expanded = self.expand_occur(children);
2749        match strategy {
2750            LayoutStrategy::Positioned => self.layout_positioned(&expanded),
2751            LayoutStrategy::TopToBottom => self.layout_tb(&expanded, available),
2752            LayoutStrategy::LeftToRightTB => self.layout_lr_tb(&expanded, available),
2753            LayoutStrategy::RightToLeftTB => self.layout_rl_tb(&expanded, available),
2754            LayoutStrategy::Table => self.layout_table(&expanded, available),
2755            LayoutStrategy::Row => self.layout_row(&expanded, available),
2756        }
2757    }
2758
2759    /// Expand children based on occur rules.
2760    ///
2761    /// A child with `occur.count() == 3` produces three entries in the output.
2762    /// Each entry refers to the same FormNodeId (the template), which the layout
2763    /// engine treats as separate instances at different positions.
2764    ///
2765    /// Nodes with non-visible presence (hidden/invisible/inactive) are skipped
2766    /// entirely — they consume no layout space (Adobe empirical, fixes #806).
2767    // XFA Spec 3.3 §7.4 / §9.2 — Repeating Elements using Occurrence Limits:
2768    // At layout time, the occur.count() (= initial for empty merge, or
2769    // data-driven count) determines how many copies appear.  Blank repeating
2770    // subforms are capped at occur.min to avoid empty rows (#701).
2771    fn expand_occur(&self, children: &[FormNodeId]) -> Vec<FormNodeId> {
2772        let mut expanded = Vec::new();
2773        for &child_id in children {
2774            if self.is_layout_hidden(child_id) {
2775                continue;
2776            }
2777            let child = self.form.get(child_id);
2778            // #701: limit blank repeating subforms to occur.min
2779            let count = if child.occur.is_repeating()
2780                && child.occur.count() > child.occur.min
2781                && self.has_field_descendants(child_id)
2782                && self.subtree_is_blank(child_id)
2783            {
2784                child.occur.min
2785            } else {
2786                child.occur.count()
2787            };
2788            // #865: Script-controlled pages have occur min=0/max=0/initial=0.
2789            // Without a script engine, these would be invisible.  Show them
2790            // once so the static content is rendered.
2791            let count = if count == 0
2792                && matches!(
2793                    child.node_type,
2794                    FormNodeType::Subform | FormNodeType::Area | FormNodeType::ExclGroup
2795                )
2796                && self.has_field_descendants(child_id)
2797            {
2798                1
2799            } else {
2800                count
2801            };
2802            for _ in 0..count {
2803                expanded.push(child_id);
2804            }
2805        }
2806        expanded
2807    }
2808
2809    /// Returns true if the subtree contains at least one Field/Draw/Image node.
2810    fn has_field_descendants(&self, id: FormNodeId) -> bool {
2811        let node = self.form.get(id);
2812        match &node.node_type {
2813            FormNodeType::Field { .. } | FormNodeType::Draw(..) | FormNodeType::Image { .. } => {
2814                true
2815            }
2816            // Area, ExclGroup, SubformSet: recurse like Subform.
2817            FormNodeType::Subform
2818            | FormNodeType::Area
2819            | FormNodeType::ExclGroup
2820            | FormNodeType::SubformSet => {
2821                node.children.iter().any(|&c| self.has_field_descendants(c))
2822            }
2823            _ => false,
2824        }
2825    }
2826
2827    /// XFA Spec 3.3 §8.2 — Positioned Layout: each child uses its own x,y
2828    /// coordinates. pageArea and contentArea always use positioned layout.
2829    /// Subforms default to positioned when no `layout` attribute is present.
2830    ///
2831    /// §2.6 + Appendix A (p1510): the `anchorType` attribute determines which
2832    /// point of the element's nominal extent is placed at (x,y).  The default
2833    /// is `topLeft` (no adjustment).
2834    fn layout_positioned(&self, children: &[FormNodeId]) -> Result<Vec<LayoutNode>> {
2835        use crate::form::AnchorType;
2836
2837        let mut nodes = Vec::new();
2838        for &child_id in children {
2839            let child = self.form.get(child_id);
2840            let anchor = self.form.meta(child_id).anchor_type;
2841
2842            // Compute nominal extent so we can offset for the anchor point.
2843            let extent = self.compute_extent(child_id);
2844            let w = extent.width;
2845            let h = extent.height;
2846
2847            let (dx, dy) = match anchor {
2848                AnchorType::TopLeft => (0.0, 0.0),
2849                AnchorType::TopCenter => (-w / 2.0, 0.0),
2850                AnchorType::TopRight => (-w, 0.0),
2851                AnchorType::MiddleLeft => (0.0, -h / 2.0),
2852                AnchorType::MiddleCenter => (-w / 2.0, -h / 2.0),
2853                AnchorType::MiddleRight => (-w, -h / 2.0),
2854                AnchorType::BottomLeft => (0.0, -h),
2855                AnchorType::BottomCenter => (-w / 2.0, -h),
2856                AnchorType::BottomRight => (-w, -h),
2857            };
2858
2859            let node = self.layout_single_node_with_extent(
2860                child_id,
2861                child,
2862                child.box_model.x + dx,
2863                child.box_model.y + dy,
2864                extent,
2865                None,
2866            )?;
2867            nodes.push(node);
2868        }
2869        Ok(nodes)
2870    }
2871
2872    /// XFA Spec 3.3 §8.3 (p282-284) — compute x offset for a child based on
2873    /// its `<para hAlign>` within the parent container width.
2874    fn child_h_align_offset(&self, child_id: FormNodeId, child_w: f64, parent_w: f64) -> f64 {
2875        match self.form.meta(child_id).style.h_align {
2876            Some(TextAlign::Center) => ((parent_w - child_w) / 2.0).max(0.0),
2877            Some(TextAlign::Right) => (parent_w - child_w).max(0.0),
2878            _ => 0.0,
2879        }
2880    }
2881
2882    /// Shift all nodes in a completed LR-TB row by the hAlign-derived offset.
2883    /// Uses the first node's hAlign to determine the row alignment
2884    /// (XFA Spec 3.3 §8.3 Example 8.12, p284).
2885    fn shift_row_h_align(&self, row: &mut [LayoutNode], row_width: f64, parent_width: f64) {
2886        let align = row
2887            .first()
2888            .and_then(|n| self.form.meta(n.form_node).style.h_align);
2889        let offset = match align {
2890            Some(TextAlign::Center) => ((parent_width - row_width) / 2.0).max(0.0),
2891            Some(TextAlign::Right) => (parent_width - row_width).max(0.0),
2892            _ => return,
2893        };
2894        for node in row {
2895            node.rect.x += offset;
2896        }
2897    }
2898
2899    /// XFA Spec 3.3 §8.2 — Top-to-Bottom Layout (p280).
2900    /// §8.3 (p282-284): child hAlign offsets x within parent width.
2901    fn layout_tb(&self, children: &[FormNodeId], available: Size) -> Result<Vec<LayoutNode>> {
2902        let mut nodes = Vec::new();
2903        let mut y_cursor = 0.0;
2904
2905        for &child_id in children {
2906            let child = self.form.get(child_id);
2907            let child_size = self.compute_extent_with_available(child_id, Some(available));
2908            let child_bottom = y_cursor + child_size.height;
2909
2910            // 0.5pt tolerance prevents marginal overflows from triggering
2911            // pagination — sub-point rounding differences should not create
2912            // extra pages (#971).
2913            if child_bottom > available.height + 0.5 {
2914                let remaining_height = (available.height - y_cursor).max(0.0);
2915
2916                // Use the same overflow semantics as layout_content_fitting():
2917                // split text leaves at line boundaries, split splittable TB
2918                // containers at child boundaries, then stop placing further
2919                // children in this container pass.
2920                if remaining_height > 0.0 && self.is_splittable_text_leaf(child_id) {
2921                    let txt = match &child.node_type {
2922                        FormNodeType::Draw(DrawContent::Text(t)) => t.as_str(),
2923                        FormNodeType::Field { value } => value.as_str(),
2924                        _ => "",
2925                    };
2926                    let child_style = &self.form.meta(child_id).style;
2927                    let para_margins = child_style
2928                        .margin_left_pt
2929                        .unwrap_or(crate::types::DEFAULT_TEXT_PADDING)
2930                        + child_style
2931                            .margin_right_pt
2932                            .unwrap_or(crate::types::DEFAULT_TEXT_PADDING);
2933                    let child_border_w = child_style
2934                        .border_width_pt
2935                        .unwrap_or(child.box_model.border_width);
2936                    let insets_w =
2937                        child.box_model.margins.horizontal() + child_border_w * 2.0 + para_margins;
2938                    let max_w = (child_size.width - insets_w).max(1.0);
2939                    let wrapped = text::wrap_text(
2940                        txt,
2941                        max_w,
2942                        &child.font,
2943                        child_style.text_indent_pt.unwrap_or(0.0),
2944                        child_style.line_height_pt,
2945                    );
2946                    let (partial, _) =
2947                        self.split_text_node(child_id, y_cursor, remaining_height, &wrapped.lines)?;
2948
2949                    if partial.rect.height > 0.0 && partial.rect.height <= remaining_height + 1.0 {
2950                        let x = self.child_h_align_offset(
2951                            child_id,
2952                            partial.rect.width,
2953                            available.width,
2954                        );
2955                        let mut partial = partial;
2956                        partial.rect.x = x;
2957                        nodes.push(partial);
2958                    } else if nodes.is_empty() {
2959                        // Keep progress when the first child is oversized.
2960                        let x =
2961                            self.child_h_align_offset(child_id, child_size.width, available.width);
2962                        let node = self.layout_single_node_with_extent(
2963                            child_id, child, x, y_cursor, child_size, None,
2964                        )?;
2965                        nodes.push(node);
2966                    }
2967                } else if remaining_height > 0.0 && self.can_split(child_id) {
2968                    let (partial, _) = self.split_tb_node(
2969                        child_id,
2970                        y_cursor,
2971                        remaining_height,
2972                        available,
2973                        None,
2974                        None,
2975                    )?;
2976                    if partial.rect.height > 0.0 && partial.rect.height <= remaining_height + 1.0 {
2977                        let x = self.child_h_align_offset(
2978                            child_id,
2979                            partial.rect.width,
2980                            available.width,
2981                        );
2982                        let mut partial = partial;
2983                        partial.rect.x = x;
2984                        nodes.push(partial);
2985                    } else if nodes.is_empty() {
2986                        // Keep progress when the first child is oversized.
2987                        let x =
2988                            self.child_h_align_offset(child_id, child_size.width, available.width);
2989                        let node = self.layout_single_node_with_extent(
2990                            child_id, child, x, y_cursor, child_size, None,
2991                        )?;
2992                        nodes.push(node);
2993                    }
2994                } else if nodes.is_empty() {
2995                    // Keep progress when the first child is oversized.
2996                    let x = self.child_h_align_offset(child_id, child_size.width, available.width);
2997                    let node = self.layout_single_node_with_extent(
2998                        child_id, child, x, y_cursor, child_size, None,
2999                    )?;
3000                    nodes.push(node);
3001                }
3002
3003                break;
3004            }
3005
3006            let x = self.child_h_align_offset(child_id, child_size.width, available.width);
3007
3008            let node = self
3009                .layout_single_node_with_extent(child_id, child, x, y_cursor, child_size, None)?;
3010            nodes.push(node);
3011
3012            y_cursor = child_bottom;
3013        }
3014        Ok(nodes)
3015    }
3016
3017    /// XFA Spec 3.3 §8.2 — Left-to-Right Top-to-Bottom Tiled Layout (p281).
3018    /// §8.3 Example 8.12 (p284): when children specify hAlign, the entire row
3019    /// is shifted within the parent width (first child's hAlign determines row).
3020    fn layout_lr_tb(&self, children: &[FormNodeId], available: Size) -> Result<Vec<LayoutNode>> {
3021        let mut nodes = Vec::new();
3022        let mut x_cursor = 0.0;
3023        let mut y_cursor = 0.0;
3024        let mut row_height = 0.0_f64;
3025        let mut row_start = 0_usize;
3026
3027        for &child_id in children {
3028            let child = self.form.get(child_id);
3029            let child_size = self.compute_extent(child_id);
3030
3031            // Wrap to next row if doesn't fit horizontally
3032            if x_cursor + child_size.width > available.width && x_cursor > 0.0 {
3033                self.shift_row_h_align(&mut nodes[row_start..], x_cursor, available.width);
3034                row_start = nodes.len();
3035                y_cursor += row_height;
3036                x_cursor = 0.0;
3037                row_height = 0.0;
3038            }
3039
3040            let node = self.layout_single_node(child_id, child, x_cursor, y_cursor, None)?;
3041            nodes.push(node);
3042
3043            x_cursor += child_size.width;
3044            row_height = row_height.max(child_size.height);
3045        }
3046        self.shift_row_h_align(&mut nodes[row_start..], x_cursor, available.width);
3047        Ok(nodes)
3048    }
3049
3050    /// XFA Spec 3.3 §8.2 — Right-to-Left Top-to-Bottom Tiled Layout (p282).
3051    /// §8.3 (p282): default hAlign is "right" for RTL. Per-child hAlign
3052    /// overrides flow position (Example 8.11, p283).
3053    fn layout_rl_tb(&self, children: &[FormNodeId], available: Size) -> Result<Vec<LayoutNode>> {
3054        let mut nodes = Vec::new();
3055        let mut x_cursor = available.width;
3056        let mut y_cursor = 0.0;
3057        let mut row_height = 0.0_f64;
3058
3059        for &child_id in children {
3060            let child = self.form.get(child_id);
3061            let child_size = self.compute_extent(child_id);
3062
3063            // Wrap to next row if doesn't fit
3064            if x_cursor - child_size.width < 0.0 && x_cursor < available.width {
3065                y_cursor += row_height;
3066                x_cursor = available.width;
3067                row_height = 0.0;
3068            }
3069
3070            // §8.3: explicit hAlign overrides default RTL flow position
3071            let h_align = self.form.meta(child_id).style.h_align;
3072            let x = match h_align {
3073                Some(TextAlign::Left) => {
3074                    x_cursor -= child_size.width;
3075                    0.0
3076                }
3077                Some(TextAlign::Center) => {
3078                    x_cursor -= child_size.width;
3079                    ((available.width - child_size.width) / 2.0).max(0.0)
3080                }
3081                _ => {
3082                    x_cursor -= child_size.width;
3083                    x_cursor
3084                }
3085            };
3086            let node = self.layout_single_node(child_id, child, x, y_cursor, None)?;
3087            nodes.push(node);
3088
3089            row_height = row_height.max(child_size.height);
3090        }
3091        Ok(nodes)
3092    }
3093
3094    /// Table layout: resolve column widths from the table node, then delegate
3095    /// to `layout_table_rows`. This stub is still called from `layout_children`
3096    /// but the real table path is intercepted in `layout_single_node_with_extent`
3097    /// where we have access to the parent FormNode's `column_widths`.
3098    fn layout_table(&self, children: &[FormNodeId], available: Size) -> Result<Vec<LayoutNode>> {
3099        // Fallback: equal-width columns based on max cell count
3100        let max_cells = children
3101            .iter()
3102            .map(|&row_id| self.form.get(row_id).children.len())
3103            .max()
3104            .unwrap_or(0);
3105        if max_cells == 0 {
3106            return Ok(Vec::new());
3107        }
3108        let col_width = available.width / max_cells as f64;
3109        let col_widths: Vec<f64> = vec![col_width; max_cells];
3110        self.layout_table_rows(children, available, &col_widths)
3111    }
3112
3113    /// XFA Spec 3.3 §8.11 — Tables (p327-332): stack rows vertically,
3114    /// distributing cells across resolved column widths. Per spec: first lay
3115    /// out cells with natural sizes, then expand cells to column width, then
3116    /// expand cells vertically to row height (tallest cell).
3117    ///
3118    /// TODO §8.11: rl-row (right-to-left row), non-row direct children in table.
3119    fn layout_table_rows(
3120        &self,
3121        children: &[FormNodeId],
3122        available: Size,
3123        col_widths: &[f64],
3124    ) -> Result<Vec<LayoutNode>> {
3125        let expanded = self.expand_occur(children);
3126        let mut nodes = Vec::new();
3127        let mut y_cursor = 0.0;
3128
3129        for &row_id in &expanded {
3130            let row_node = self.form.get(row_id);
3131            let row_children = self.expand_occur(&row_node.children);
3132
3133            // Layout cells within this row using column widths
3134            let mut cells = Vec::new();
3135            let mut x_cursor = 0.0;
3136            let mut col_idx = 0usize;
3137            let mut max_cell_height = 0.0_f64;
3138
3139            for &cell_id in &row_children {
3140                if col_idx >= col_widths.len() {
3141                    break;
3142                }
3143                let cell = self.form.get(cell_id);
3144                let span = cell.col_span;
3145
3146                // Calculate cell width from column widths
3147                let cell_width = if span == -1 {
3148                    // Span remaining columns
3149                    col_widths[col_idx..].iter().sum::<f64>()
3150                } else {
3151                    let span_count =
3152                        (span.max(1) as usize).min(col_widths.len().saturating_sub(col_idx));
3153                    col_widths[col_idx..col_idx + span_count]
3154                        .iter()
3155                        .sum::<f64>()
3156                };
3157
3158                // Layout cell with forced width
3159                let cell_available = Size {
3160                    width: cell_width,
3161                    height: available.height - y_cursor,
3162                };
3163                let cell_height = self
3164                    .compute_extent_with_available(cell_id, Some(cell_available))
3165                    .height;
3166                let cell_extent = Size {
3167                    width: cell_width,
3168                    height: cell_height,
3169                };
3170
3171                let cell_node = self.layout_single_node_with_extent(
3172                    cell_id,
3173                    cell,
3174                    x_cursor,
3175                    0.0,
3176                    cell_extent,
3177                    None,
3178                )?;
3179                max_cell_height = max_cell_height.max(cell_extent.height);
3180                cells.push(cell_node);
3181
3182                x_cursor += cell_width;
3183                if span == -1 {
3184                    col_idx = col_widths.len();
3185                } else {
3186                    col_idx += span.max(1) as usize;
3187                }
3188            }
3189
3190            // Equalize row height: all cells expand to tallest
3191            for cell in &mut cells {
3192                cell.rect.height = max_cell_height;
3193            }
3194
3195            // Create row layout node
3196            let row_layout = LayoutNode {
3197                form_node: row_id,
3198                rect: Rect::new(0.0, y_cursor, available.width, max_cell_height),
3199                name: row_node.name.clone(),
3200                content: LayoutContent::None,
3201                children: cells,
3202                style: self.form.meta(row_id).style.clone(),
3203                display_items: self.form.meta(row_id).display_items.clone(),
3204                save_items: self.form.meta(row_id).save_items.clone(),
3205            };
3206            nodes.push(row_layout);
3207
3208            y_cursor += max_cell_height;
3209        }
3210        Ok(nodes)
3211    }
3212
3213    fn resolve_column_widths_with_override(
3214        &self,
3215        table_node: &FormNode,
3216        available_width: f64,
3217        children_override: Option<&[FormNodeId]>,
3218    ) -> Vec<f64> {
3219        let specified = &table_node.column_widths;
3220        let node_children = children_override.unwrap_or(&table_node.children);
3221
3222        // Determine number of columns
3223        let max_cols_from_rows = node_children
3224            .iter()
3225            .map(|&row_id| {
3226                let row = self.form.get(row_id);
3227                row.children
3228                    .iter()
3229                    .map(|&cell_id| {
3230                        let cell = self.form.get(cell_id);
3231                        cell.col_span.max(1) as usize
3232                    })
3233                    .sum::<usize>()
3234            })
3235            .max()
3236            .unwrap_or(0);
3237
3238        let num_cols = specified.len().max(max_cols_from_rows);
3239        if num_cols == 0 {
3240            return vec![];
3241        }
3242
3243        let mut widths = Vec::with_capacity(num_cols);
3244        for i in 0..num_cols {
3245            let spec_value = specified.get(i).copied().unwrap_or(-1.0);
3246            if spec_value >= 0.0 {
3247                widths.push(spec_value);
3248            } else {
3249                // Auto-size: find widest single-span cell in this column
3250                let mut max_w = 0.0_f64;
3251                for &row_id in node_children {
3252                    let row = self.form.get(row_id);
3253                    let mut col_idx = 0usize;
3254                    for &cell_id in &row.children {
3255                        let cell = self.form.get(cell_id);
3256                        let span = cell.col_span;
3257                        if col_idx == i && span == 1 {
3258                            let cell_extent = self.compute_extent(cell_id);
3259                            max_w = max_w.max(cell_extent.width);
3260                        }
3261                        col_idx += span.max(1) as usize;
3262                    }
3263                }
3264                widths.push(max_w);
3265            }
3266        }
3267
3268        // Scale down proportionally if total exceeds available width
3269        let total: f64 = widths.iter().sum();
3270        if total > available_width && total > 0.0 {
3271            let scale = available_width / total;
3272            for w in &mut widths {
3273                *w *= scale;
3274            }
3275        }
3276
3277        widths
3278    }
3279
3280    /// Row layout: children fill horizontally within the row.
3281    /// Used for standalone Row-layout subforms (not inside a table context).
3282    fn layout_row(&self, children: &[FormNodeId], available: Size) -> Result<Vec<LayoutNode>> {
3283        let mut nodes = Vec::new();
3284        let mut x_cursor = 0.0;
3285
3286        for &child_id in children {
3287            let child = self.form.get(child_id);
3288            let child_size = self.compute_extent(child_id);
3289
3290            let node = self.layout_single_node(child_id, child, x_cursor, 0.0, None)?;
3291            nodes.push(node);
3292
3293            x_cursor += child_size.width;
3294
3295            if x_cursor > available.width {
3296                break;
3297            }
3298        }
3299        Ok(nodes)
3300    }
3301
3302    /// Layout a single node: compute its rect and recursively layout children.
3303    fn layout_single_node(
3304        &self,
3305        id: FormNodeId,
3306        node: &FormNode,
3307        x: f64,
3308        y: f64,
3309        children_override: Option<&[FormNodeId]>,
3310    ) -> Result<LayoutNode> {
3311        let extent = self.compute_extent_with_override(id, children_override);
3312        self.layout_single_node_with_extent(id, node, x, y, extent, children_override)
3313    }
3314
3315    /// Layout a single node with a pre-computed extent.
3316    fn layout_single_node_with_extent(
3317        &self,
3318        id: FormNodeId,
3319        node: &FormNode,
3320        x: f64,
3321        y: f64,
3322        extent: Size,
3323        children_override: Option<&[FormNodeId]>,
3324    ) -> Result<LayoutNode> {
3325        // All non-visible presence values (hidden/invisible/inactive) produce
3326        // no visual content. In practice these nodes are already filtered in
3327        // queue_content/expand_occur, but this guard handles children_override.
3328        if self.form.meta(id).presence.is_layout_hidden() {
3329            let hidden_meta = self.form.meta(id);
3330            return Ok(LayoutNode {
3331                form_node: id,
3332                rect: Rect::new(x, y, extent.width, extent.height),
3333                name: node.name.clone(),
3334                content: LayoutContent::None,
3335                children: Vec::new(),
3336                style: Default::default(),
3337                display_items: hidden_meta.display_items.clone(),
3338                save_items: hidden_meta.save_items.clone(),
3339            });
3340        }
3341
3342        let node_style = &self.form.meta(id).style;
3343        let para_margins = node_style
3344            .margin_left_pt
3345            .unwrap_or(crate::types::DEFAULT_TEXT_PADDING)
3346            + node_style
3347                .margin_right_pt
3348                .unwrap_or(crate::types::DEFAULT_TEXT_PADDING);
3349
3350        let content = match &node.node_type {
3351            FormNodeType::Field { value } => {
3352                let meta = self.form.meta(id);
3353                let display_val = resolve_display_value(value, meta);
3354                if !display_val.is_empty() && node.children.is_empty() {
3355                    let border_w = node_style
3356                        .border_width_pt
3357                        .unwrap_or(node.box_model.border_width);
3358                    let insets_w =
3359                        node.box_model.margins.horizontal() + border_w * 2.0 + para_margins;
3360                    let max_w = (extent.width - insets_w).max(0.0);
3361                    let wrapped = text::wrap_text(
3362                        &display_val,
3363                        max_w,
3364                        &node.font,
3365                        node_style.text_indent_pt.unwrap_or(0.0),
3366                        node_style.line_height_pt,
3367                    );
3368                    LayoutContent::WrappedText {
3369                        lines: wrapped.lines,
3370                        first_line_of_para: wrapped.first_line_of_para,
3371                        font_size: node.font.size,
3372                        text_align: node.font.text_align,
3373                        font_family: node.font.typeface,
3374                        space_above_pt: node_style.space_above_pt,
3375                        space_below_pt: node_style.space_below_pt,
3376                        from_field: true,
3377                    }
3378                } else {
3379                    LayoutContent::Field {
3380                        value: display_val.to_string(),
3381                        field_kind: meta.field_kind,
3382                        font_size: node.font.size,
3383                        font_family: node.font.typeface,
3384                    }
3385                }
3386            }
3387            FormNodeType::Draw(DrawContent::Text(content)) => {
3388                if !content.is_empty() && node.children.is_empty() {
3389                    let border_w = node_style
3390                        .border_width_pt
3391                        .unwrap_or(node.box_model.border_width);
3392                    let insets_w =
3393                        node.box_model.margins.horizontal() + border_w * 2.0 + para_margins;
3394                    let max_w = (extent.width - insets_w).max(0.0);
3395                    let wrapped = text::wrap_text(
3396                        content,
3397                        max_w,
3398                        &node.font,
3399                        node_style.text_indent_pt.unwrap_or(0.0),
3400                        node_style.line_height_pt,
3401                    );
3402                    LayoutContent::WrappedText {
3403                        lines: wrapped.lines,
3404                        first_line_of_para: wrapped.first_line_of_para,
3405                        font_size: node.font.size,
3406                        text_align: node.font.text_align,
3407                        font_family: node.font.typeface,
3408                        space_above_pt: node_style.space_above_pt,
3409                        space_below_pt: node_style.space_below_pt,
3410                        from_field: false,
3411                    }
3412                } else {
3413                    LayoutContent::Text(content.clone())
3414                }
3415            }
3416            FormNodeType::Draw(dc) => LayoutContent::Draw(dc.clone()),
3417            FormNodeType::Image { data, mime_type } => LayoutContent::Image {
3418                data: data.clone(),
3419                mime_type: mime_type.clone(),
3420            },
3421            _ => LayoutContent::None,
3422        };
3423
3424        let child_available = Size {
3425            width: node.box_model.content_width().min(extent.width),
3426            height: node.box_model.content_height().min(extent.height),
3427        };
3428
3429        let node_children = children_override.unwrap_or(&node.children);
3430
3431        let children = if node_children.is_empty() {
3432            Vec::new()
3433        } else if node.layout == LayoutStrategy::Table {
3434            // Table layout: resolve column widths from the parent node,
3435            // then distribute cells across rows.
3436            let col_widths = self.resolve_column_widths_with_override(
3437                node,
3438                child_available.width,
3439                children_override,
3440            );
3441            self.layout_table_rows(node_children, child_available, &col_widths)?
3442        } else {
3443            self.layout_children(node_children, child_available, node.layout)?
3444        };
3445        let mut children = children;
3446
3447        if node.layout == LayoutStrategy::Positioned {
3448            let dx = node.box_model.margins.left;
3449            let mut dy = node.box_model.margins.top;
3450            if let Some(override_children) = children_override {
3451                if let Some(y_base) = override_children
3452                    .iter()
3453                    .map(|&cid| self.form.get(cid).box_model.y)
3454                    .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
3455                {
3456                    dy -= y_base;
3457                }
3458            }
3459            if dx != 0.0 || dy != 0.0 {
3460                for child in &mut children {
3461                    child.rect.x += dx;
3462                    child.rect.y += dy;
3463                }
3464            }
3465        }
3466
3467        Ok(LayoutNode {
3468            form_node: id,
3469            rect: Rect::new(x, y, extent.width, extent.height),
3470            name: node.name.clone(),
3471            content,
3472            children,
3473            style: self.form.meta(id).style.clone(),
3474            display_items: self.form.meta(id).display_items.clone(),
3475            save_items: self.form.meta(id).save_items.clone(),
3476        })
3477    }
3478
3479    /// Compute the outer extent (total bounding box) of a form node.
3480    ///
3481    /// When `available` is provided, growable dimensions may expand to fill
3482    /// the available space (XFA §8: growable objects fill the parent container).
3483    pub fn compute_extent(&self, id: FormNodeId) -> Size {
3484        self.compute_extent_with_available(id, None)
3485    }
3486
3487    /// Compute extent with optional children override.
3488    pub fn compute_extent_with_override(
3489        &self,
3490        id: FormNodeId,
3491        children_override: Option<&[FormNodeId]>,
3492    ) -> Size {
3493        self.compute_extent_with_available_and_override(id, None, children_override)
3494    }
3495
3496    /// Compute extent with optional available-space constraint.
3497    ///
3498    /// For growable dimensions (width/height = None), the element sizes to fit
3499    /// its content. When `available` is given, a growable dimension expands to
3500    /// at least the available space (but content can make it larger, subject to
3501    /// max constraints).
3502    fn compute_extent_with_available(&self, id: FormNodeId, available: Option<Size>) -> Size {
3503        self.compute_extent_with_available_and_override(id, available, None)
3504    }
3505
3506    fn compute_extent_with_available_and_override(
3507        &self,
3508        id: FormNodeId,
3509        available: Option<Size>,
3510        children_override: Option<&[FormNodeId]>,
3511    ) -> Size {
3512        let node = self.form.get(id);
3513        let bm = &node.box_model;
3514        let ext_style = &self.form.meta(id).style;
3515
3516        // If explicit size is set, use it — except for TB subforms with
3517        // children, where we must compute the actual content height so that
3518        // pagination can detect overflow beyond the explicit height (#768).
3519        let is_tb_with_children = node.layout == LayoutStrategy::TopToBottom
3520            && !children_override.unwrap_or(&node.children).is_empty();
3521        if let (Some(w), Some(h)) = (bm.width, bm.height) {
3522            if !is_tb_with_children {
3523                return Size {
3524                    width: w,
3525                    height: h,
3526                };
3527            }
3528        }
3529
3530        // For growable dimensions, compute from children or text content
3531        let mut content_size = Size::default();
3532
3533        let node_children = children_override.unwrap_or(&node.children);
3534
3535        if !node_children.is_empty() {
3536            // (#764) When children_override is set the list is already
3537            // occur-expanded from a prior split — re-expanding inflates the
3538            // extent and contributes to over-pagination.
3539            let expanded = if children_override.is_some() {
3540                node_children.to_vec()
3541            } else {
3542                self.expand_occur(node_children)
3543            };
3544            match node.layout {
3545                LayoutStrategy::TopToBottom => {
3546                    // #687: pass available so text wrapping is considered
3547                    for &child_id in &expanded {
3548                        let cs = self.compute_extent_with_available(child_id, available);
3549                        content_size.width = content_size.width.max(cs.width);
3550                        content_size.height += cs.height;
3551                    }
3552                }
3553                LayoutStrategy::LeftToRightTB | LayoutStrategy::Row => {
3554                    for &child_id in &expanded {
3555                        let cs = self.compute_extent_with_available(child_id, available);
3556                        content_size.width += cs.width;
3557                        content_size.height = content_size.height.max(cs.height);
3558                    }
3559                }
3560                LayoutStrategy::Table => {
3561                    let avail_w = available.map(|a| a.width).unwrap_or(f64::MAX);
3562                    let col_widths =
3563                        self.resolve_column_widths_with_override(node, avail_w, children_override);
3564                    let table_width: f64 = col_widths.iter().sum();
3565                    content_size.width = content_size.width.max(table_width);
3566                    // Table height = sum of row heights
3567                    for &row_id in &expanded {
3568                        let row_extent = self.compute_extent_with_available(row_id, available);
3569                        content_size.height += row_extent.height;
3570                    }
3571                }
3572                _ => {
3573                    // Positioned: envelope all children (occur doesn't stack in positioned).
3574                    // When a children_override is active (from a positioned split),
3575                    // compute relative height from the minimum y so that remaining
3576                    // children after a page break produce a sensible extent instead
3577                    // of the full original height (#736).
3578                    let y_base = if children_override.is_some() {
3579                        node_children
3580                            .iter()
3581                            .map(|&cid| self.form.get(cid).box_model.y)
3582                            .fold(f64::MAX, f64::min)
3583                    } else {
3584                        0.0
3585                    };
3586                    for &child_id in node_children {
3587                        let child = self.form.get(child_id);
3588                        let cs = self.compute_extent(child_id);
3589                        content_size.width = content_size.width.max(child.box_model.x + cs.width);
3590                        content_size.height = content_size
3591                            .height
3592                            .max(child.box_model.y - y_base + cs.height);
3593                    }
3594                }
3595            }
3596        } else {
3597            // Leaf node: measure text content for Draw/Field nodes
3598            let text_content = match &node.node_type {
3599                FormNodeType::Draw(DrawContent::Text(content)) => Some(content.as_str()),
3600                FormNodeType::Field { value } => Some(value.as_str()),
3601                _ => None,
3602            };
3603
3604            if let Some(txt) = text_content {
3605                if !txt.is_empty() {
3606                    let ext_style = &self.form.meta(id).style;
3607                    let ext_para = ext_style
3608                        .margin_left_pt
3609                        .unwrap_or(crate::types::DEFAULT_TEXT_PADDING)
3610                        + ext_style
3611                            .margin_right_pt
3612                            .unwrap_or(crate::types::DEFAULT_TEXT_PADDING);
3613                    let border_w = ext_style.border_width_pt.unwrap_or(bm.border_width);
3614                    let insets_w = bm.margins.horizontal() + border_w * 2.0 + ext_para;
3615                    let space_above = ext_style.space_above_pt.unwrap_or(0.0);
3616                    let space_below = ext_style.space_below_pt.unwrap_or(0.0);
3617                    // If width is fixed, wrap text within that width minus insets
3618                    // If width is growable, measure without wrapping
3619                    let text_size = if let Some(w) = bm.width {
3620                        let max_text_width = (w - insets_w).max(0.0);
3621                        text::wrap_text(
3622                            txt,
3623                            max_text_width,
3624                            &node.font,
3625                            ext_style.text_indent_pt.unwrap_or(0.0),
3626                            ext_style.line_height_pt,
3627                        )
3628                        .size
3629                    } else if let Some(avail) = available {
3630                        let max_text_width = (avail.width - insets_w).max(0.0);
3631                        text::wrap_text(
3632                            txt,
3633                            max_text_width,
3634                            &node.font,
3635                            ext_style.text_indent_pt.unwrap_or(0.0),
3636                            ext_style.line_height_pt,
3637                        )
3638                        .size
3639                    } else {
3640                        text::measure_text(txt, &node.font)
3641                    };
3642                    content_size.width = content_size.width.max(text_size.width);
3643                    content_size.height = content_size
3644                        .height
3645                        .max(text_size.height + space_above + space_below);
3646                }
3647            }
3648        }
3649
3650        // When available space is given, growable dims expand to fill it
3651        if let Some(avail) = available {
3652            if bm.width.is_none() {
3653                let border_w = ext_style.border_width_pt.unwrap_or(bm.border_width);
3654                let insets_w = bm.margins.horizontal() + border_w * 2.0;
3655                content_size.width = content_size.width.max(avail.width - insets_w);
3656            }
3657        }
3658
3659        let mut result = bm.outer_size(content_size);
3660
3661        // For TB subforms with children AND an explicit height, ensure the
3662        // reported height reflects actual content height (not clamped to the
3663        // fixed h) so layout_content_fitting() detects overflow and triggers
3664        // pagination.  max_height constraints on growable subforms are NOT
3665        // overridden — only explicit height declarations (#768).
3666        if is_tb_with_children && bm.height.is_some() {
3667            let border_w = ext_style.border_width_pt.unwrap_or(bm.border_width);
3668            let mut unclamped_h = content_size.height + bm.margins.vertical() + border_w * 2.0;
3669            if let Some(ref cap) = bm.caption {
3670                if matches!(
3671                    cap.placement,
3672                    crate::types::CaptionPlacement::Top | crate::types::CaptionPlacement::Bottom
3673                ) {
3674                    unclamped_h += cap.reserve.unwrap_or(0.0);
3675                }
3676            }
3677            result.height = result.height.max(unclamped_h);
3678        }
3679
3680        result
3681    }
3682}
3683
3684/// Find a page area by break target string. The target can match the page
3685/// area's `name`, its `xfa_id`, or a `pageArea[N]` index reference.
3686#[allow(dead_code)]
3687fn find_page_area_by_target(page_areas: &[PageAreaInfo], target: &str) -> Option<usize> {
3688    // Try matching by name first (e.g. "MP3").
3689    if let Some(idx) = page_areas.iter().position(|pa| pa.name == target) {
3690        return Some(idx);
3691    }
3692    // Try matching by xfa_id (e.g. "Page4_ID").
3693    if let Some(idx) = page_areas
3694        .iter()
3695        .position(|pa| pa.xfa_id.as_deref() == Some(target))
3696    {
3697        return Some(idx);
3698    }
3699    // Try matching "pageArea[N]" index reference.
3700    if let Some(rest) = target.strip_prefix("pageArea") {
3701        if let Some(idx_str) = rest.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
3702            if let Ok(idx) = idx_str.parse::<usize>() {
3703                if idx < page_areas.len() {
3704                    return Some(idx);
3705                }
3706            }
3707        }
3708        // "pageArea" without index → first.
3709        if rest.is_empty() && !page_areas.is_empty() {
3710            return Some(0);
3711        }
3712    }
3713    None
3714}
3715
3716/// Get the primary (largest) content area from a page area.
3717fn primary_content_area(pa: &PageAreaInfo) -> &ContentArea {
3718    let max_area = pa
3719        .content_areas
3720        .iter()
3721        .map(|ca| ca.width * ca.height)
3722        .fold(0.0_f64, f64::max);
3723    pa.content_areas
3724        .iter()
3725        .find(|ca| {
3726            let a = ca.width * ca.height;
3727            a >= max_area * 0.90 || pa.content_areas.len() == 1
3728        })
3729        .unwrap_or(&pa.content_areas[0])
3730}
3731
3732struct PageAreaInfo {
3733    /// Name of the page area (e.g. "MP1", "MP3") for break targeting.
3734    name: String,
3735    /// XFA id attribute (e.g. "Page1", "Page4_ID") for break targeting.
3736    xfa_id: Option<String>,
3737    content_areas: Vec<ContentArea>,
3738    page_width: f64,
3739    page_height: f64,
3740    /// Fixed-position nodes (e.g., page-level headers/footers) placed on every
3741    /// page that uses this page area.
3742    fixed_nodes: Vec<FormNodeId>,
3743}
3744
3745#[cfg(test)]
3746mod tests {
3747    use super::*;
3748    use crate::form::Occur;
3749    use crate::text::FontMetrics;
3750    use crate::types::BoxModel;
3751
3752    fn make_field(tree: &mut FormTree, name: &str, w: f64, h: f64) -> FormNodeId {
3753        tree.add_node(FormNode {
3754            name: name.to_string(),
3755            node_type: FormNodeType::Field {
3756                value: name.to_string(),
3757            },
3758            box_model: BoxModel {
3759                width: Some(w),
3760                height: Some(h),
3761                max_width: f64::MAX,
3762                max_height: f64::MAX,
3763                ..Default::default()
3764            },
3765            layout: LayoutStrategy::Positioned,
3766            children: vec![],
3767            occur: Occur::once(),
3768            font: FontMetrics::default(),
3769            calculate: None,
3770            validate: None,
3771            column_widths: vec![],
3772            col_span: 1,
3773        })
3774    }
3775
3776    fn make_subform(
3777        tree: &mut FormTree,
3778        name: &str,
3779        strategy: LayoutStrategy,
3780        w: Option<f64>,
3781        h: Option<f64>,
3782        children: Vec<FormNodeId>,
3783    ) -> FormNodeId {
3784        tree.add_node(FormNode {
3785            name: name.to_string(),
3786            node_type: FormNodeType::Subform,
3787            box_model: BoxModel {
3788                width: w,
3789                height: h,
3790                max_width: f64::MAX,
3791                max_height: f64::MAX,
3792                ..Default::default()
3793            },
3794            layout: strategy,
3795            children,
3796            occur: Occur::once(),
3797            font: FontMetrics::default(),
3798            calculate: None,
3799            validate: None,
3800            column_widths: vec![],
3801            col_span: 1,
3802        })
3803    }
3804
3805    /// Count leaf layout nodes (fields/draws) recursively across a page.
3806    fn count_leaf_nodes(page: &LayoutPage) -> usize {
3807        fn count(nodes: &[LayoutNode]) -> usize {
3808            nodes
3809                .iter()
3810                .map(|n| {
3811                    if n.children.is_empty() {
3812                        1
3813                    } else {
3814                        count(&n.children)
3815                    }
3816                })
3817                .sum()
3818        }
3819        count(&page.nodes)
3820    }
3821
3822    fn make_field_value(
3823        tree: &mut FormTree,
3824        name: &str,
3825        value: &str,
3826        w: f64,
3827        h: f64,
3828    ) -> FormNodeId {
3829        tree.add_node(FormNode {
3830            name: name.to_string(),
3831            node_type: FormNodeType::Field {
3832                value: value.to_string(),
3833            },
3834            box_model: BoxModel {
3835                width: Some(w),
3836                height: Some(h),
3837                max_width: f64::MAX,
3838                max_height: f64::MAX,
3839                ..Default::default()
3840            },
3841            layout: LayoutStrategy::Positioned,
3842            children: vec![],
3843            occur: Occur::once(),
3844            font: FontMetrics::default(),
3845            calculate: None,
3846            validate: None,
3847            column_widths: vec![],
3848            col_span: 1,
3849        })
3850    }
3851
3852    fn make_draw_text(tree: &mut FormTree, name: &str, text: &str, w: f64, h: f64) -> FormNodeId {
3853        tree.add_node(FormNode {
3854            name: name.to_string(),
3855            node_type: FormNodeType::Draw(DrawContent::Text(text.to_string())),
3856            box_model: BoxModel {
3857                width: Some(w),
3858                height: Some(h),
3859                max_width: f64::MAX,
3860                max_height: f64::MAX,
3861                ..Default::default()
3862            },
3863            layout: LayoutStrategy::Positioned,
3864            children: vec![],
3865            occur: Occur::once(),
3866            font: FontMetrics::default(),
3867            calculate: None,
3868            validate: None,
3869            column_widths: vec![],
3870            col_span: 1,
3871        })
3872    }
3873
3874    fn make_page_area(tree: &mut FormTree, name: &str, width: f64, height: f64) -> FormNodeId {
3875        tree.add_node(FormNode {
3876            name: name.to_string(),
3877            node_type: FormNodeType::PageArea {
3878                content_areas: vec![ContentArea {
3879                    name: "Body".to_string(),
3880                    x: 0.0,
3881                    y: 0.0,
3882                    width,
3883                    height,
3884                    leader: None,
3885                    trailer: None,
3886                }],
3887            },
3888            box_model: BoxModel {
3889                width: Some(width),
3890                height: Some(height),
3891                max_width: f64::MAX,
3892                max_height: f64::MAX,
3893                ..Default::default()
3894            },
3895            layout: LayoutStrategy::Positioned,
3896            children: vec![],
3897            occur: Occur::once(),
3898            font: FontMetrics::default(),
3899            calculate: None,
3900            validate: None,
3901            column_widths: vec![],
3902            col_span: 1,
3903        })
3904    }
3905
3906    fn make_root(tree: &mut FormTree, children: Vec<FormNodeId>) -> FormNodeId {
3907        tree.add_node(FormNode {
3908            name: "Root".to_string(),
3909            node_type: FormNodeType::Root,
3910            box_model: BoxModel {
3911                max_width: f64::MAX,
3912                max_height: f64::MAX,
3913                ..Default::default()
3914            },
3915            layout: LayoutStrategy::TopToBottom,
3916            children,
3917            occur: Occur::once(),
3918            font: FontMetrics::default(),
3919            calculate: None,
3920            validate: None,
3921            column_widths: vec![],
3922            col_span: 1,
3923        })
3924    }
3925
3926    fn mark_data_bound(tree: &mut FormTree, id: FormNodeId) {
3927        let name = tree.get(id).name.clone();
3928        tree.meta_mut(id).data_bind_ref = Some(format!("$.{name}"));
3929    }
3930
3931    #[test]
3932    fn positioned_layout() {
3933        let mut tree = FormTree::new();
3934        let f1 = tree.add_node(FormNode {
3935            name: "Field1".to_string(),
3936            node_type: FormNodeType::Field {
3937                value: "A".to_string(),
3938            },
3939            box_model: BoxModel {
3940                width: Some(100.0),
3941                height: Some(20.0),
3942                x: 10.0,
3943                y: 30.0,
3944                max_width: f64::MAX,
3945                max_height: f64::MAX,
3946                ..Default::default()
3947            },
3948            layout: LayoutStrategy::Positioned,
3949            children: vec![],
3950            occur: Occur::once(),
3951            font: FontMetrics::default(),
3952            calculate: None,
3953            validate: None,
3954            column_widths: vec![],
3955            col_span: 1,
3956        });
3957        let f2 = tree.add_node(FormNode {
3958            name: "Field2".to_string(),
3959            node_type: FormNodeType::Field {
3960                value: "B".to_string(),
3961            },
3962            box_model: BoxModel {
3963                width: Some(100.0),
3964                height: Some(20.0),
3965                x: 10.0,
3966                y: 60.0,
3967                max_width: f64::MAX,
3968                max_height: f64::MAX,
3969                ..Default::default()
3970            },
3971            layout: LayoutStrategy::Positioned,
3972            children: vec![],
3973            occur: Occur::once(),
3974            font: FontMetrics::default(),
3975            calculate: None,
3976            validate: None,
3977            column_widths: vec![],
3978            col_span: 1,
3979        });
3980        let root = tree.add_node(FormNode {
3981            name: "Root".to_string(),
3982            node_type: FormNodeType::Root,
3983            box_model: BoxModel {
3984                width: Some(612.0),
3985                height: Some(792.0),
3986                max_width: f64::MAX,
3987                max_height: f64::MAX,
3988                ..Default::default()
3989            },
3990            layout: LayoutStrategy::Positioned,
3991            children: vec![f1, f2],
3992            occur: Occur::once(),
3993            font: FontMetrics::default(),
3994            calculate: None,
3995            validate: None,
3996            column_widths: vec![],
3997            col_span: 1,
3998        });
3999
4000        let engine = LayoutEngine::new(&tree);
4001        let result = engine.layout(root).unwrap();
4002
4003        assert_eq!(result.pages.len(), 1);
4004        let page = &result.pages[0];
4005        assert_eq!(page.nodes.len(), 2);
4006        assert_eq!(page.nodes[0].rect.x, 10.0);
4007        assert_eq!(page.nodes[0].rect.y, 30.0);
4008        assert_eq!(page.nodes[1].rect.x, 10.0);
4009        assert_eq!(page.nodes[1].rect.y, 60.0);
4010    }
4011
4012    #[test]
4013    fn positioned_children_offset_by_parent_margins() {
4014        use crate::types::Insets;
4015
4016        let mut tree = FormTree::new();
4017        let child = tree.add_node(FormNode {
4018            name: "Child".to_string(),
4019            node_type: FormNodeType::Field {
4020                value: "Child".to_string(),
4021            },
4022            box_model: BoxModel {
4023                width: Some(80.0),
4024                height: Some(20.0),
4025                x: 0.0,
4026                y: 0.0,
4027                max_width: f64::MAX,
4028                max_height: f64::MAX,
4029                ..Default::default()
4030            },
4031            layout: LayoutStrategy::Positioned,
4032            children: vec![],
4033            occur: Occur::once(),
4034            font: FontMetrics::default(),
4035            calculate: None,
4036            validate: None,
4037            column_widths: vec![],
4038            col_span: 1,
4039        });
4040
4041        let parent = tree.add_node(FormNode {
4042            name: "Parent".to_string(),
4043            node_type: FormNodeType::Subform,
4044            box_model: BoxModel {
4045                width: Some(200.0),
4046                height: Some(100.0),
4047                x: 50.0,
4048                y: 40.0,
4049                margins: Insets {
4050                    top: 15.0,
4051                    right: 0.0,
4052                    bottom: 0.0,
4053                    left: 10.0,
4054                },
4055                max_width: f64::MAX,
4056                max_height: f64::MAX,
4057                ..Default::default()
4058            },
4059            layout: LayoutStrategy::Positioned,
4060            children: vec![child],
4061            occur: Occur::once(),
4062            font: FontMetrics::default(),
4063            calculate: None,
4064            validate: None,
4065            column_widths: vec![],
4066            col_span: 1,
4067        });
4068
4069        let root = tree.add_node(FormNode {
4070            name: "Root".to_string(),
4071            node_type: FormNodeType::Root,
4072            box_model: BoxModel {
4073                width: Some(612.0),
4074                height: Some(792.0),
4075                max_width: f64::MAX,
4076                max_height: f64::MAX,
4077                ..Default::default()
4078            },
4079            layout: LayoutStrategy::Positioned,
4080            children: vec![parent],
4081            occur: Occur::once(),
4082            font: FontMetrics::default(),
4083            calculate: None,
4084            validate: None,
4085            column_widths: vec![],
4086            col_span: 1,
4087        });
4088
4089        let engine = LayoutEngine::new(&tree);
4090        let result = engine.layout(root).unwrap();
4091
4092        let parent_node = &result.pages[0].nodes[0];
4093        assert_eq!(parent_node.rect.x, 50.0);
4094        assert_eq!(parent_node.rect.y, 40.0);
4095        assert_eq!(parent_node.children[0].rect.x, 10.0);
4096        assert_eq!(parent_node.children[0].rect.y, 15.0);
4097    }
4098
4099    #[test]
4100    fn tb_layout() {
4101        let mut tree = FormTree::new();
4102        let f1 = make_field(&mut tree, "Field1", 200.0, 30.0);
4103        let f2 = make_field(&mut tree, "Field2", 200.0, 30.0);
4104        let f3 = make_field(&mut tree, "Field3", 200.0, 30.0);
4105
4106        let root = tree.add_node(FormNode {
4107            name: "Root".to_string(),
4108            node_type: FormNodeType::Root,
4109            box_model: BoxModel {
4110                width: Some(612.0),
4111                height: Some(792.0),
4112                max_width: f64::MAX,
4113                max_height: f64::MAX,
4114                ..Default::default()
4115            },
4116            layout: LayoutStrategy::TopToBottom,
4117            children: vec![f1, f2, f3],
4118            occur: Occur::once(),
4119            font: FontMetrics::default(),
4120            calculate: None,
4121            validate: None,
4122            column_widths: vec![],
4123            col_span: 1,
4124        });
4125
4126        let engine = LayoutEngine::new(&tree);
4127        let result = engine.layout(root).unwrap();
4128
4129        assert_eq!(result.pages.len(), 1);
4130        let page = &result.pages[0];
4131        assert_eq!(page.nodes.len(), 3);
4132        assert_eq!(page.nodes[0].rect.y, 0.0);
4133        assert_eq!(page.nodes[1].rect.y, 30.0);
4134        assert_eq!(page.nodes[2].rect.y, 60.0);
4135    }
4136
4137    #[test]
4138    fn lr_tb_wrapping() {
4139        let mut tree = FormTree::new();
4140        // 3 fields of 250pt width in a 600pt container → 2 fit on first row, 1 wraps
4141        let f1 = make_field(&mut tree, "F1", 250.0, 30.0);
4142        let f2 = make_field(&mut tree, "F2", 250.0, 30.0);
4143        let f3 = make_field(&mut tree, "F3", 250.0, 30.0);
4144
4145        let root = tree.add_node(FormNode {
4146            name: "Root".to_string(),
4147            node_type: FormNodeType::Root,
4148            box_model: BoxModel {
4149                width: Some(600.0),
4150                height: Some(792.0),
4151                max_width: f64::MAX,
4152                max_height: f64::MAX,
4153                ..Default::default()
4154            },
4155            layout: LayoutStrategy::LeftToRightTB,
4156            children: vec![f1, f2, f3],
4157            occur: Occur::once(),
4158            font: FontMetrics::default(),
4159            calculate: None,
4160            validate: None,
4161            column_widths: vec![],
4162            col_span: 1,
4163        });
4164
4165        let engine = LayoutEngine::new(&tree);
4166        let result = engine.layout(root).unwrap();
4167
4168        let page = &result.pages[0];
4169        assert_eq!(page.nodes.len(), 3);
4170        // First two on row 1
4171        assert_eq!(page.nodes[0].rect.x, 0.0);
4172        assert_eq!(page.nodes[0].rect.y, 0.0);
4173        assert_eq!(page.nodes[1].rect.x, 250.0);
4174        assert_eq!(page.nodes[1].rect.y, 0.0);
4175        // Third wraps to row 2
4176        assert_eq!(page.nodes[2].rect.x, 0.0);
4177        assert_eq!(page.nodes[2].rect.y, 30.0);
4178    }
4179
4180    #[test]
4181    fn nested_subforms() {
4182        let mut tree = FormTree::new();
4183        let f1 = make_field(&mut tree, "Name", 200.0, 25.0);
4184        let f2 = make_field(&mut tree, "Email", 200.0, 25.0);
4185
4186        let sub = make_subform(
4187            &mut tree,
4188            "PersonalInfo",
4189            LayoutStrategy::TopToBottom,
4190            Some(300.0),
4191            Some(100.0),
4192            vec![f1, f2],
4193        );
4194
4195        let root = tree.add_node(FormNode {
4196            name: "Root".to_string(),
4197            node_type: FormNodeType::Root,
4198            box_model: BoxModel {
4199                width: Some(612.0),
4200                height: Some(792.0),
4201                max_width: f64::MAX,
4202                max_height: f64::MAX,
4203                ..Default::default()
4204            },
4205            layout: LayoutStrategy::TopToBottom,
4206            children: vec![sub],
4207            occur: Occur::once(),
4208            font: FontMetrics::default(),
4209            calculate: None,
4210            validate: None,
4211            column_widths: vec![],
4212            col_span: 1,
4213        });
4214
4215        let engine = LayoutEngine::new(&tree);
4216        let result = engine.layout(root).unwrap();
4217
4218        let page = &result.pages[0];
4219        assert_eq!(page.nodes.len(), 1);
4220        let subform = &page.nodes[0];
4221        assert_eq!(subform.name, "PersonalInfo");
4222        assert_eq!(subform.rect.width, 300.0);
4223        assert_eq!(subform.children.len(), 2);
4224        assert_eq!(subform.children[0].rect.y, 0.0);
4225        assert_eq!(subform.children[1].rect.y, 25.0);
4226    }
4227
4228    #[test]
4229    fn page_area_layout() {
4230        let mut tree = FormTree::new();
4231        let f1 = make_field(&mut tree, "Field1", 200.0, 30.0);
4232
4233        let page_area = tree.add_node(FormNode {
4234            name: "Page1".to_string(),
4235            node_type: FormNodeType::PageArea {
4236                content_areas: vec![ContentArea {
4237                    name: "Body".to_string(),
4238                    x: 36.0,
4239                    y: 36.0,
4240                    width: 540.0,
4241                    height: 720.0,
4242                    leader: None,
4243                    trailer: None,
4244                }],
4245            },
4246            box_model: BoxModel {
4247                width: Some(612.0),
4248                height: Some(792.0),
4249                max_width: f64::MAX,
4250                max_height: f64::MAX,
4251                ..Default::default()
4252            },
4253            layout: LayoutStrategy::Positioned,
4254            children: vec![],
4255            occur: Occur::once(),
4256            font: FontMetrics::default(),
4257            calculate: None,
4258            validate: None,
4259            column_widths: vec![],
4260            col_span: 1,
4261        });
4262
4263        let root = tree.add_node(FormNode {
4264            name: "Root".to_string(),
4265            node_type: FormNodeType::Root,
4266            box_model: BoxModel {
4267                width: Some(612.0),
4268                height: Some(792.0),
4269                max_width: f64::MAX,
4270                max_height: f64::MAX,
4271                ..Default::default()
4272            },
4273            layout: LayoutStrategy::TopToBottom,
4274            children: vec![page_area, f1],
4275            occur: Occur::once(),
4276            font: FontMetrics::default(),
4277            calculate: None,
4278            validate: None,
4279            column_widths: vec![],
4280            col_span: 1,
4281        });
4282
4283        let engine = LayoutEngine::new(&tree);
4284        let result = engine.layout(root).unwrap();
4285
4286        assert_eq!(result.pages.len(), 1);
4287        let page = &result.pages[0];
4288        // Field should be offset by content area position (36, 36)
4289        assert_eq!(page.nodes[0].rect.x, 36.0);
4290        assert_eq!(page.nodes[0].rect.y, 36.0);
4291    }
4292
4293    #[test]
4294    fn trailing_empty_pagearea_continuation_suppressed() {
4295        let mut tree = FormTree::new();
4296        let first_page = make_page_area(&mut tree, "Page1", 200.0, 80.0);
4297        let continuation_page = make_page_area(&mut tree, "Page2", 200.0, 100.0);
4298        let record = make_field_value(&mut tree, "record", "data", 180.0, 80.0);
4299        mark_data_bound(&mut tree, record);
4300        let template_only = make_draw_text(&mut tree, "template_only", "template", 180.0, 20.0);
4301        let root = make_root(
4302            &mut tree,
4303            vec![first_page, continuation_page, record, template_only],
4304        );
4305
4306        let engine = LayoutEngine::new(&tree);
4307        let result = engine.layout(root).unwrap();
4308
4309        assert_eq!(result.pages.len(), 1);
4310        assert_eq!(count_leaf_nodes(&result.pages[0]), 1);
4311    }
4312
4313    #[test]
4314    fn static_two_page_form_both_pages_emitted() {
4315        // Models DOTRW1040 (08ea2c58): two explicit pageAreas, two page-sized
4316        // static content subforms (no data binding).  The continuation guard
4317        // must not suppress the second page for purely static forms.
4318        let mut tree = FormTree::new();
4319        let pa1 = make_page_area(&mut tree, "Page1", 200.0, 100.0);
4320        let pa2 = make_page_area(&mut tree, "Page2", 200.0, 100.0);
4321        // page1_content fills most of page 1 (90/100 = 90%)
4322        let page1_content = make_draw_text(&mut tree, "page1_content", "page1", 180.0, 90.0);
4323        // page2_content fills most of page 2 (85/100 = 85%)
4324        let page2_content = make_draw_text(&mut tree, "page2_content", "page2", 180.0, 85.0);
4325        let root = make_root(&mut tree, vec![pa1, pa2, page1_content, page2_content]);
4326
4327        let engine = LayoutEngine::new(&tree);
4328        let result = engine.layout(root).unwrap();
4329
4330        assert_eq!(
4331            result.pages.len(),
4332            2,
4333            "static two-page form must emit 2 pages"
4334        );
4335        assert_eq!(count_leaf_nodes(&result.pages[0]), 1);
4336        assert_eq!(count_leaf_nodes(&result.pages[1]), 1);
4337    }
4338
4339    #[test]
4340    fn valid_multi_page_overflow_unchanged() {
4341        let mut tree = FormTree::new();
4342        let page_area = make_page_area(&mut tree, "Page1", 200.0, 100.0);
4343        let mut children = vec![page_area];
4344        for idx in 0..4 {
4345            let field = make_field_value(&mut tree, &format!("record{idx}"), "data", 180.0, 40.0);
4346            mark_data_bound(&mut tree, field);
4347            children.push(field);
4348        }
4349        let root = make_root(&mut tree, children);
4350
4351        let engine = LayoutEngine::new(&tree);
4352        let result = engine.layout(root).unwrap();
4353
4354        assert_eq!(result.pages.len(), 2);
4355        assert_eq!(result.pages.iter().map(count_leaf_nodes).sum::<usize>(), 4);
4356    }
4357
4358    #[test]
4359    fn template_only_page_kept_when_explicitly_required() {
4360        let mut tree = FormTree::new();
4361        let first_page = make_page_area(&mut tree, "Page1", 200.0, 80.0);
4362        let anchored_page = make_page_area(&mut tree, "Page2", 200.0, 100.0);
4363        let record = make_field_value(&mut tree, "record", "data", 180.0, 80.0);
4364        mark_data_bound(&mut tree, record);
4365        let anchored_draw = make_draw_text(&mut tree, "anchored_draw", "next page", 180.0, 20.0);
4366        tree.meta_mut(anchored_draw).page_break_before = true;
4367        let root = make_root(
4368            &mut tree,
4369            vec![first_page, anchored_page, record, anchored_draw],
4370        );
4371
4372        let engine = LayoutEngine::new(&tree);
4373        let result = engine.layout(root).unwrap();
4374
4375        assert_eq!(result.pages.len(), 2);
4376        assert_eq!(count_leaf_nodes(&result.pages[1]), 1);
4377    }
4378
4379    #[test]
4380    fn single_page_form_remains_single_page() {
4381        let mut tree = FormTree::new();
4382        let page_area = make_page_area(&mut tree, "Page1", 200.0, 100.0);
4383        let record = make_field_value(&mut tree, "record", "data", 180.0, 40.0);
4384        mark_data_bound(&mut tree, record);
4385        let root = make_root(&mut tree, vec![page_area, record]);
4386
4387        let engine = LayoutEngine::new(&tree);
4388        let result = engine.layout(root).unwrap();
4389
4390        assert_eq!(result.pages.len(), 1);
4391        assert_eq!(count_leaf_nodes(&result.pages[0]), 1);
4392    }
4393
4394    #[test]
4395    fn continuation_with_remaining_body_still_emits() {
4396        let mut tree = FormTree::new();
4397        let first_page = make_page_area(&mut tree, "Page1", 200.0, 60.0);
4398        let continuation_page = make_page_area(&mut tree, "Page2", 200.0, 100.0);
4399        let first_record = make_field_value(&mut tree, "first_record", "data", 180.0, 60.0);
4400        let second_record = make_field_value(&mut tree, "second_record", "data", 180.0, 40.0);
4401        mark_data_bound(&mut tree, first_record);
4402        mark_data_bound(&mut tree, second_record);
4403        let root = make_root(
4404            &mut tree,
4405            vec![first_page, continuation_page, first_record, second_record],
4406        );
4407
4408        let engine = LayoutEngine::new(&tree);
4409        let result = engine.layout(root).unwrap();
4410
4411        assert_eq!(result.pages.len(), 2);
4412        assert_eq!(count_leaf_nodes(&result.pages[1]), 1);
4413    }
4414
4415    #[test]
4416    fn growable_extent() {
4417        let mut tree = FormTree::new();
4418        let f1 = make_field(&mut tree, "F1", 100.0, 20.0);
4419        let f2 = make_field(&mut tree, "F2", 150.0, 20.0);
4420
4421        // Subform with no explicit size — should grow to fit children
4422        let sub = make_subform(
4423            &mut tree,
4424            "Container",
4425            LayoutStrategy::TopToBottom,
4426            None,
4427            None,
4428            vec![f1, f2],
4429        );
4430
4431        let engine = LayoutEngine::new(&tree);
4432        let extent = engine.compute_extent(sub);
4433
4434        // Width = max child width = 150, Height = sum = 40
4435        assert_eq!(extent.width, 150.0);
4436        assert_eq!(extent.height, 40.0);
4437    }
4438
4439    #[test]
4440    fn rl_tb_layout() {
4441        let mut tree = FormTree::new();
4442        let f1 = make_field(&mut tree, "F1", 100.0, 30.0);
4443        let f2 = make_field(&mut tree, "F2", 100.0, 30.0);
4444
4445        let root = tree.add_node(FormNode {
4446            name: "Root".to_string(),
4447            node_type: FormNodeType::Root,
4448            box_model: BoxModel {
4449                width: Some(400.0),
4450                height: Some(400.0),
4451                max_width: f64::MAX,
4452                max_height: f64::MAX,
4453                ..Default::default()
4454            },
4455            layout: LayoutStrategy::RightToLeftTB,
4456            children: vec![f1, f2],
4457            occur: Occur::once(),
4458            font: FontMetrics::default(),
4459            calculate: None,
4460            validate: None,
4461            column_widths: vec![],
4462            col_span: 1,
4463        });
4464
4465        let engine = LayoutEngine::new(&tree);
4466        let result = engine.layout(root).unwrap();
4467        let page = &result.pages[0];
4468
4469        // RL: first field at right edge, second to its left
4470        assert_eq!(page.nodes[0].rect.x, 300.0); // 400 - 100
4471        assert_eq!(page.nodes[1].rect.x, 200.0); // 400 - 100 - 100
4472    }
4473
4474    // --- Dynamic sizing tests (Epic 3.5) ---
4475
4476    #[test]
4477    fn growable_clamped_by_min() {
4478        // A container with tiny content but min constraints
4479        let mut tree = FormTree::new();
4480        let f1 = make_field(&mut tree, "F1", 50.0, 10.0);
4481
4482        let sub = tree.add_node(FormNode {
4483            name: "Container".to_string(),
4484            node_type: FormNodeType::Subform,
4485            box_model: BoxModel {
4486                width: None,
4487                height: None,
4488                min_width: 200.0,
4489                min_height: 100.0,
4490                max_width: f64::MAX,
4491                max_height: f64::MAX,
4492                ..Default::default()
4493            },
4494            layout: LayoutStrategy::TopToBottom,
4495            children: vec![f1],
4496            occur: Occur::once(),
4497            font: FontMetrics::default(),
4498            calculate: None,
4499            validate: None,
4500            column_widths: vec![],
4501            col_span: 1,
4502        });
4503
4504        let engine = LayoutEngine::new(&tree);
4505        let extent = engine.compute_extent(sub);
4506
4507        // Content is 50x10 but min clamps to 200x100
4508        assert_eq!(extent.width, 200.0);
4509        assert_eq!(extent.height, 100.0);
4510    }
4511
4512    #[test]
4513    fn growable_clamped_by_max() {
4514        // A container with large content but max constraints
4515        let mut tree = FormTree::new();
4516        let f1 = make_field(&mut tree, "F1", 500.0, 300.0);
4517
4518        let sub = tree.add_node(FormNode {
4519            name: "Container".to_string(),
4520            node_type: FormNodeType::Subform,
4521            box_model: BoxModel {
4522                width: None,
4523                height: None,
4524                min_width: 0.0,
4525                min_height: 0.0,
4526                max_width: 200.0,
4527                max_height: 100.0,
4528                ..Default::default()
4529            },
4530            layout: LayoutStrategy::TopToBottom,
4531            children: vec![f1],
4532            occur: Occur::once(),
4533            font: FontMetrics::default(),
4534            calculate: None,
4535            validate: None,
4536            column_widths: vec![],
4537            col_span: 1,
4538        });
4539
4540        let engine = LayoutEngine::new(&tree);
4541        let extent = engine.compute_extent(sub);
4542
4543        // Content is 500x300 but max clamps to 200x100
4544        assert_eq!(extent.width, 200.0);
4545        assert_eq!(extent.height, 100.0);
4546    }
4547
4548    #[test]
4549    fn partially_growable_width_fixed() {
4550        // Width fixed, height growable
4551        let mut tree = FormTree::new();
4552        let f1 = make_field(&mut tree, "F1", 100.0, 25.0);
4553        let f2 = make_field(&mut tree, "F2", 100.0, 25.0);
4554
4555        let sub = tree.add_node(FormNode {
4556            name: "Container".to_string(),
4557            node_type: FormNodeType::Subform,
4558            box_model: BoxModel {
4559                width: Some(300.0),
4560                height: None,
4561                max_width: f64::MAX,
4562                max_height: f64::MAX,
4563                ..Default::default()
4564            },
4565            layout: LayoutStrategy::TopToBottom,
4566            children: vec![f1, f2],
4567            occur: Occur::once(),
4568            font: FontMetrics::default(),
4569            calculate: None,
4570            validate: None,
4571            column_widths: vec![],
4572            col_span: 1,
4573        });
4574
4575        let engine = LayoutEngine::new(&tree);
4576        let extent = engine.compute_extent(sub);
4577
4578        // Width fixed at 300, height grows to content (25+25=50)
4579        assert_eq!(extent.width, 300.0);
4580        assert_eq!(extent.height, 50.0);
4581    }
4582
4583    #[test]
4584    fn partially_growable_height_fixed() {
4585        // Height fixed, width growable
4586        let mut tree = FormTree::new();
4587        let f1 = make_field(&mut tree, "F1", 100.0, 25.0);
4588        let f2 = make_field(&mut tree, "F2", 150.0, 25.0);
4589
4590        let sub = tree.add_node(FormNode {
4591            name: "Container".to_string(),
4592            node_type: FormNodeType::Subform,
4593            box_model: BoxModel {
4594                width: None,
4595                height: Some(200.0),
4596                max_width: f64::MAX,
4597                max_height: f64::MAX,
4598                ..Default::default()
4599            },
4600            layout: LayoutStrategy::TopToBottom,
4601            children: vec![f1, f2],
4602            occur: Occur::once(),
4603            font: FontMetrics::default(),
4604            calculate: None,
4605            validate: None,
4606            column_widths: vec![],
4607            col_span: 1,
4608        });
4609
4610        let engine = LayoutEngine::new(&tree);
4611        let extent = engine.compute_extent(sub);
4612
4613        // Height fixed at 200, width grows to max child (150)
4614        assert_eq!(extent.width, 150.0);
4615        assert_eq!(extent.height, 200.0);
4616    }
4617
4618    #[test]
4619    fn growable_fills_available_width_in_tb() {
4620        // A growable subform inside a tb-layout parent should fill parent width
4621        let mut tree = FormTree::new();
4622        let f1 = make_field(&mut tree, "F1", 100.0, 25.0);
4623
4624        let growable_sub = tree.add_node(FormNode {
4625            name: "GrowableSub".to_string(),
4626            node_type: FormNodeType::Subform,
4627            box_model: BoxModel {
4628                width: None,
4629                height: None,
4630                max_width: f64::MAX,
4631                max_height: f64::MAX,
4632                ..Default::default()
4633            },
4634            layout: LayoutStrategy::TopToBottom,
4635            children: vec![f1],
4636            occur: Occur::once(),
4637            font: FontMetrics::default(),
4638            calculate: None,
4639            validate: None,
4640            column_widths: vec![],
4641            col_span: 1,
4642        });
4643
4644        let root = tree.add_node(FormNode {
4645            name: "Root".to_string(),
4646            node_type: FormNodeType::Root,
4647            box_model: BoxModel {
4648                width: Some(500.0),
4649                height: Some(400.0),
4650                max_width: f64::MAX,
4651                max_height: f64::MAX,
4652                ..Default::default()
4653            },
4654            layout: LayoutStrategy::TopToBottom,
4655            children: vec![growable_sub],
4656            occur: Occur::once(),
4657            font: FontMetrics::default(),
4658            calculate: None,
4659            validate: None,
4660            column_widths: vec![],
4661            col_span: 1,
4662        });
4663
4664        let engine = LayoutEngine::new(&tree);
4665        let result = engine.layout(root).unwrap();
4666
4667        let page = &result.pages[0];
4668        // Growable subform should fill the parent's available width (500)
4669        assert_eq!(page.nodes[0].rect.width, 500.0);
4670        // Height should be content-based (25)
4671        assert_eq!(page.nodes[0].rect.height, 25.0);
4672    }
4673
4674    #[test]
4675    fn growable_fill_capped_by_max() {
4676        // A growable subform filling parent, but capped by maxW
4677        let mut tree = FormTree::new();
4678        let f1 = make_field(&mut tree, "F1", 100.0, 25.0);
4679
4680        let growable_sub = tree.add_node(FormNode {
4681            name: "GrowableSub".to_string(),
4682            node_type: FormNodeType::Subform,
4683            box_model: BoxModel {
4684                width: None,
4685                height: None,
4686                max_width: 300.0,
4687                max_height: f64::MAX,
4688                ..Default::default()
4689            },
4690            layout: LayoutStrategy::TopToBottom,
4691            children: vec![f1],
4692            occur: Occur::once(),
4693            font: FontMetrics::default(),
4694            calculate: None,
4695            validate: None,
4696            column_widths: vec![],
4697            col_span: 1,
4698        });
4699
4700        let root = tree.add_node(FormNode {
4701            name: "Root".to_string(),
4702            node_type: FormNodeType::Root,
4703            box_model: BoxModel {
4704                width: Some(500.0),
4705                height: Some(400.0),
4706                max_width: f64::MAX,
4707                max_height: f64::MAX,
4708                ..Default::default()
4709            },
4710            layout: LayoutStrategy::TopToBottom,
4711            children: vec![growable_sub],
4712            occur: Occur::once(),
4713            font: FontMetrics::default(),
4714            calculate: None,
4715            validate: None,
4716            column_widths: vec![],
4717            col_span: 1,
4718        });
4719
4720        let engine = LayoutEngine::new(&tree);
4721        let result = engine.layout(root).unwrap();
4722
4723        let page = &result.pages[0];
4724        // Would fill 500, but maxW caps it to 300
4725        assert_eq!(page.nodes[0].rect.width, 300.0);
4726    }
4727
4728    #[test]
4729    fn growable_with_margins_in_tb() {
4730        // Growable container with margins should fill parent minus margins
4731        use crate::types::Insets;
4732        let mut tree = FormTree::new();
4733        let f1 = make_field(&mut tree, "F1", 50.0, 20.0);
4734
4735        let growable_sub = tree.add_node(FormNode {
4736            name: "GrowableSub".to_string(),
4737            node_type: FormNodeType::Subform,
4738            box_model: BoxModel {
4739                width: None,
4740                height: None,
4741                margins: Insets {
4742                    top: 5.0,
4743                    right: 10.0,
4744                    bottom: 5.0,
4745                    left: 10.0,
4746                },
4747                max_width: f64::MAX,
4748                max_height: f64::MAX,
4749                ..Default::default()
4750            },
4751            layout: LayoutStrategy::TopToBottom,
4752            children: vec![f1],
4753            occur: Occur::once(),
4754            font: FontMetrics::default(),
4755            calculate: None,
4756            validate: None,
4757            column_widths: vec![],
4758            col_span: 1,
4759        });
4760
4761        let root = tree.add_node(FormNode {
4762            name: "Root".to_string(),
4763            node_type: FormNodeType::Root,
4764            box_model: BoxModel {
4765                width: Some(400.0),
4766                height: Some(300.0),
4767                max_width: f64::MAX,
4768                max_height: f64::MAX,
4769                ..Default::default()
4770            },
4771            layout: LayoutStrategy::TopToBottom,
4772            children: vec![growable_sub],
4773            occur: Occur::once(),
4774            font: FontMetrics::default(),
4775            calculate: None,
4776            validate: None,
4777            column_widths: vec![],
4778            col_span: 1,
4779        });
4780
4781        let engine = LayoutEngine::new(&tree);
4782        let result = engine.layout(root).unwrap();
4783
4784        let page = &result.pages[0];
4785        // Width: content fills 400 - margins(20) = 380, outer = 380 + 20 = 400
4786        assert_eq!(page.nodes[0].rect.width, 400.0);
4787        // Height: content 20, outer = 20 + margins(10) = 30
4788        assert_eq!(page.nodes[0].rect.height, 30.0);
4789    }
4790
4791    #[test]
4792    fn nested_growable_containers() {
4793        // Nested growable containers should propagate constraints correctly
4794        let mut tree = FormTree::new();
4795        let f1 = make_field(&mut tree, "F1", 80.0, 20.0);
4796
4797        let inner = tree.add_node(FormNode {
4798            name: "Inner".to_string(),
4799            node_type: FormNodeType::Subform,
4800            box_model: BoxModel {
4801                width: None,
4802                height: None,
4803                min_width: 150.0,
4804                max_width: f64::MAX,
4805                max_height: f64::MAX,
4806                ..Default::default()
4807            },
4808            layout: LayoutStrategy::TopToBottom,
4809            children: vec![f1],
4810            occur: Occur::once(),
4811            font: FontMetrics::default(),
4812            calculate: None,
4813            validate: None,
4814            column_widths: vec![],
4815            col_span: 1,
4816        });
4817
4818        // Outer container with maxW constraint
4819        let outer = tree.add_node(FormNode {
4820            name: "Outer".to_string(),
4821            node_type: FormNodeType::Subform,
4822            box_model: BoxModel {
4823                width: None,
4824                height: None,
4825                max_width: 400.0,
4826                max_height: f64::MAX,
4827                ..Default::default()
4828            },
4829            layout: LayoutStrategy::TopToBottom,
4830            children: vec![inner],
4831            occur: Occur::once(),
4832            font: FontMetrics::default(),
4833            calculate: None,
4834            validate: None,
4835            column_widths: vec![],
4836            col_span: 1,
4837        });
4838
4839        let engine = LayoutEngine::new(&tree);
4840        let extent = engine.compute_extent(outer);
4841
4842        // Inner: content 80, minW clamps to 150 → 150x20
4843        // Outer: child is 150, maxW is 400 → 150x20
4844        assert_eq!(extent.width, 150.0);
4845        assert_eq!(extent.height, 20.0);
4846    }
4847
4848    #[test]
4849    fn min_max_in_lr_tb_layout() {
4850        // Min/max constraints on children in lr-tb layout
4851        let mut tree = FormTree::new();
4852
4853        let f1 = tree.add_node(FormNode {
4854            name: "F1".to_string(),
4855            node_type: FormNodeType::Field {
4856                value: "A".to_string(),
4857            },
4858            box_model: BoxModel {
4859                width: None,
4860                height: Some(30.0),
4861                min_width: 200.0,
4862                max_width: f64::MAX,
4863                max_height: f64::MAX,
4864                ..Default::default()
4865            },
4866            layout: LayoutStrategy::Positioned,
4867            children: vec![],
4868            occur: Occur::once(),
4869            font: FontMetrics::default(),
4870            calculate: None,
4871            validate: None,
4872            column_widths: vec![],
4873            col_span: 1,
4874        });
4875
4876        let f2 = make_field(&mut tree, "F2", 200.0, 30.0);
4877
4878        let root = tree.add_node(FormNode {
4879            name: "Root".to_string(),
4880            node_type: FormNodeType::Root,
4881            box_model: BoxModel {
4882                width: Some(500.0),
4883                height: Some(400.0),
4884                max_width: f64::MAX,
4885                max_height: f64::MAX,
4886                ..Default::default()
4887            },
4888            layout: LayoutStrategy::LeftToRightTB,
4889            children: vec![f1, f2],
4890            occur: Occur::once(),
4891            font: FontMetrics::default(),
4892            calculate: None,
4893            validate: None,
4894            column_widths: vec![],
4895            col_span: 1,
4896        });
4897
4898        let engine = LayoutEngine::new(&tree);
4899        let result = engine.layout(root).unwrap();
4900
4901        let page = &result.pages[0];
4902        // F1 has minW=200, no content so uses 200
4903        assert_eq!(page.nodes[0].rect.width, 200.0);
4904        // F2 at x=200
4905        assert_eq!(page.nodes[1].rect.x, 200.0);
4906    }
4907
4908    // --- Occur rules tests (Epic 3.6) ---
4909
4910    #[test]
4911    fn occur_default_once() {
4912        // Default occur = 1, should produce exactly one instance
4913        let mut tree = FormTree::new();
4914        let f1 = make_field(&mut tree, "F1", 100.0, 30.0);
4915
4916        let root = tree.add_node(FormNode {
4917            name: "Root".to_string(),
4918            node_type: FormNodeType::Root,
4919            box_model: BoxModel {
4920                width: Some(400.0),
4921                height: Some(400.0),
4922                max_width: f64::MAX,
4923                max_height: f64::MAX,
4924                ..Default::default()
4925            },
4926            layout: LayoutStrategy::TopToBottom,
4927            children: vec![f1],
4928            occur: Occur::once(),
4929            font: FontMetrics::default(),
4930            calculate: None,
4931            validate: None,
4932            column_widths: vec![],
4933            col_span: 1,
4934        });
4935
4936        let engine = LayoutEngine::new(&tree);
4937        let result = engine.layout(root).unwrap();
4938        assert_eq!(result.pages[0].nodes.len(), 1);
4939    }
4940
4941    #[test]
4942    fn occur_repeating_tb() {
4943        // A subform with occur(initial=3) in tb layout should produce 3 instances
4944        let mut tree = FormTree::new();
4945        let f1 = tree.add_node(FormNode {
4946            name: "Row".to_string(),
4947            node_type: FormNodeType::Subform,
4948            box_model: BoxModel {
4949                width: Some(200.0),
4950                height: Some(30.0),
4951                max_width: f64::MAX,
4952                max_height: f64::MAX,
4953                ..Default::default()
4954            },
4955            layout: LayoutStrategy::Positioned,
4956            children: vec![],
4957            occur: Occur::repeating(1, Some(10), 3),
4958            font: FontMetrics::default(),
4959            calculate: None,
4960            validate: None,
4961            column_widths: vec![],
4962            col_span: 1,
4963        });
4964
4965        let root = tree.add_node(FormNode {
4966            name: "Root".to_string(),
4967            node_type: FormNodeType::Root,
4968            box_model: BoxModel {
4969                width: Some(400.0),
4970                height: Some(400.0),
4971                max_width: f64::MAX,
4972                max_height: f64::MAX,
4973                ..Default::default()
4974            },
4975            layout: LayoutStrategy::TopToBottom,
4976            children: vec![f1],
4977            occur: Occur::once(),
4978            font: FontMetrics::default(),
4979            calculate: None,
4980            validate: None,
4981            column_widths: vec![],
4982            col_span: 1,
4983        });
4984
4985        let engine = LayoutEngine::new(&tree);
4986        let result = engine.layout(root).unwrap();
4987        let page = &result.pages[0];
4988
4989        // 3 instances stacked vertically
4990        assert_eq!(page.nodes.len(), 3);
4991        assert_eq!(page.nodes[0].rect.y, 0.0);
4992        assert_eq!(page.nodes[1].rect.y, 30.0);
4993        assert_eq!(page.nodes[2].rect.y, 60.0);
4994    }
4995
4996    #[test]
4997    fn occur_repeating_lr_tb() {
4998        // Repeating subform in lr-tb layout
4999        let mut tree = FormTree::new();
5000        let f1 = tree.add_node(FormNode {
5001            name: "Cell".to_string(),
5002            node_type: FormNodeType::Field {
5003                value: "X".to_string(),
5004            },
5005            box_model: BoxModel {
5006                width: Some(100.0),
5007                height: Some(30.0),
5008                max_width: f64::MAX,
5009                max_height: f64::MAX,
5010                ..Default::default()
5011            },
5012            layout: LayoutStrategy::Positioned,
5013            children: vec![],
5014            occur: Occur::repeating(1, None, 5),
5015            font: FontMetrics::default(),
5016            calculate: None,
5017            validate: None,
5018            column_widths: vec![],
5019            col_span: 1,
5020        });
5021
5022        let root = tree.add_node(FormNode {
5023            name: "Root".to_string(),
5024            node_type: FormNodeType::Root,
5025            box_model: BoxModel {
5026                width: Some(350.0),
5027                height: Some(400.0),
5028                max_width: f64::MAX,
5029                max_height: f64::MAX,
5030                ..Default::default()
5031            },
5032            layout: LayoutStrategy::LeftToRightTB,
5033            children: vec![f1],
5034            occur: Occur::once(),
5035            font: FontMetrics::default(),
5036            calculate: None,
5037            validate: None,
5038            column_widths: vec![],
5039            col_span: 1,
5040        });
5041
5042        let engine = LayoutEngine::new(&tree);
5043        let result = engine.layout(root).unwrap();
5044        let page = &result.pages[0];
5045
5046        // 5 instances of 100pt wide in 350pt container:
5047        // Row 1: 3 cells (0, 100, 200), Row 2: 2 cells (0, 100)
5048        assert_eq!(page.nodes.len(), 5);
5049        assert_eq!(page.nodes[0].rect.x, 0.0);
5050        assert_eq!(page.nodes[0].rect.y, 0.0);
5051        assert_eq!(page.nodes[1].rect.x, 100.0);
5052        assert_eq!(page.nodes[2].rect.x, 200.0);
5053        assert_eq!(page.nodes[3].rect.x, 0.0);
5054        assert_eq!(page.nodes[3].rect.y, 30.0);
5055        assert_eq!(page.nodes[4].rect.x, 100.0);
5056        assert_eq!(page.nodes[4].rect.y, 30.0);
5057    }
5058
5059    #[test]
5060    fn occur_min_enforced() {
5061        // Occur with min=2, initial=2 should always produce at least 2
5062        let occur = Occur::repeating(2, Some(5), 2);
5063        assert_eq!(occur.count(), 2);
5064        assert!(occur.is_repeating());
5065    }
5066
5067    #[test]
5068    fn occur_max_caps_initial() {
5069        // Occur with max=3 but initial=5 should cap at 3
5070        let occur = Occur::repeating(1, Some(3), 5);
5071        assert_eq!(occur.count(), 3);
5072    }
5073
5074    #[test]
5075    fn occur_initial_raised_to_min() {
5076        // Occur with min=3 but initial=1 should raise to 3
5077        let occur = Occur::repeating(3, Some(10), 1);
5078        assert_eq!(occur.count(), 3);
5079    }
5080
5081    #[test]
5082    fn occur_unlimited_max() {
5083        let occur = Occur::repeating(0, None, 5);
5084        assert_eq!(occur.count(), 5);
5085        assert!(occur.is_repeating());
5086    }
5087
5088    #[test]
5089    fn occur_mixed_children() {
5090        // Mix of repeating and non-repeating children
5091        let mut tree = FormTree::new();
5092        let header = make_field(&mut tree, "Header", 200.0, 40.0);
5093        let row = tree.add_node(FormNode {
5094            name: "DataRow".to_string(),
5095            node_type: FormNodeType::Subform,
5096            box_model: BoxModel {
5097                width: Some(200.0),
5098                height: Some(25.0),
5099                max_width: f64::MAX,
5100                max_height: f64::MAX,
5101                ..Default::default()
5102            },
5103            layout: LayoutStrategy::Positioned,
5104            children: vec![],
5105            occur: Occur::repeating(1, Some(10), 4),
5106            font: FontMetrics::default(),
5107            calculate: None,
5108            validate: None,
5109            column_widths: vec![],
5110            col_span: 1,
5111        });
5112        let footer = make_field(&mut tree, "Footer", 200.0, 30.0);
5113
5114        let root = tree.add_node(FormNode {
5115            name: "Root".to_string(),
5116            node_type: FormNodeType::Root,
5117            box_model: BoxModel {
5118                width: Some(400.0),
5119                height: Some(600.0),
5120                max_width: f64::MAX,
5121                max_height: f64::MAX,
5122                ..Default::default()
5123            },
5124            layout: LayoutStrategy::TopToBottom,
5125            children: vec![header, row, footer],
5126            occur: Occur::once(),
5127            font: FontMetrics::default(),
5128            calculate: None,
5129            validate: None,
5130            column_widths: vec![],
5131            col_span: 1,
5132        });
5133
5134        let engine = LayoutEngine::new(&tree);
5135        let result = engine.layout(root).unwrap();
5136        let page = &result.pages[0];
5137
5138        // Header(1) + DataRow(4) + Footer(1) = 6 nodes
5139        assert_eq!(page.nodes.len(), 6);
5140        assert_eq!(page.nodes[0].name, "Header");
5141        assert_eq!(page.nodes[0].rect.y, 0.0);
5142        // 4 data rows at y=40, 65, 90, 115
5143        assert_eq!(page.nodes[1].name, "DataRow");
5144        assert_eq!(page.nodes[1].rect.y, 40.0);
5145        assert_eq!(page.nodes[2].rect.y, 65.0);
5146        assert_eq!(page.nodes[3].rect.y, 90.0);
5147        assert_eq!(page.nodes[4].rect.y, 115.0);
5148        // Footer at y=140
5149        assert_eq!(page.nodes[5].name, "Footer");
5150        assert_eq!(page.nodes[5].rect.y, 140.0);
5151    }
5152
5153    #[test]
5154    fn occur_growable_extent() {
5155        // A growable container with a repeating child should size to all instances
5156        let mut tree = FormTree::new();
5157        let row = tree.add_node(FormNode {
5158            name: "Row".to_string(),
5159            node_type: FormNodeType::Subform,
5160            box_model: BoxModel {
5161                width: Some(150.0),
5162                height: Some(20.0),
5163                max_width: f64::MAX,
5164                max_height: f64::MAX,
5165                ..Default::default()
5166            },
5167            layout: LayoutStrategy::Positioned,
5168            children: vec![],
5169            occur: Occur::repeating(1, None, 5),
5170            font: FontMetrics::default(),
5171            calculate: None,
5172            validate: None,
5173            column_widths: vec![],
5174            col_span: 1,
5175        });
5176
5177        let container = tree.add_node(FormNode {
5178            name: "Container".to_string(),
5179            node_type: FormNodeType::Subform,
5180            box_model: BoxModel {
5181                width: None,
5182                height: None,
5183                max_width: f64::MAX,
5184                max_height: f64::MAX,
5185                ..Default::default()
5186            },
5187            layout: LayoutStrategy::TopToBottom,
5188            children: vec![row],
5189            occur: Occur::once(),
5190            font: FontMetrics::default(),
5191            calculate: None,
5192            validate: None,
5193            column_widths: vec![],
5194            col_span: 1,
5195        });
5196
5197        let engine = LayoutEngine::new(&tree);
5198        let extent = engine.compute_extent(container);
5199
5200        // 5 rows of 150x20 stacked: width=150, height=100
5201        assert_eq!(extent.width, 150.0);
5202        assert_eq!(extent.height, 100.0);
5203    }
5204
5205    // --- Pagination tests (Epic 3.7) ---
5206
5207    #[test]
5208    fn pagination_single_page_no_overflow() {
5209        // Content fits on one page — no extra pages
5210        let mut tree = FormTree::new();
5211        let f1 = make_field(&mut tree, "F1", 200.0, 30.0);
5212        let f2 = make_field(&mut tree, "F2", 200.0, 30.0);
5213
5214        let root = tree.add_node(FormNode {
5215            name: "Root".to_string(),
5216            node_type: FormNodeType::Root,
5217            box_model: BoxModel {
5218                width: Some(400.0),
5219                height: Some(200.0),
5220                max_width: f64::MAX,
5221                max_height: f64::MAX,
5222                ..Default::default()
5223            },
5224            layout: LayoutStrategy::TopToBottom,
5225            children: vec![f1, f2],
5226            occur: Occur::once(),
5227            font: FontMetrics::default(),
5228            calculate: None,
5229            validate: None,
5230            column_widths: vec![],
5231            col_span: 1,
5232        });
5233
5234        let engine = LayoutEngine::new(&tree);
5235        let result = engine.layout(root).unwrap();
5236
5237        assert_eq!(result.pages.len(), 1);
5238        assert_eq!(result.pages[0].nodes.len(), 2);
5239    }
5240
5241    #[test]
5242    fn pagination_overflow_creates_pages() {
5243        // 10 fields of 30pt each = 300pt total, page height 100pt → 3 pages
5244        let mut tree = FormTree::new();
5245        let mut fields = Vec::new();
5246        for i in 0..10 {
5247            fields.push(make_field(&mut tree, &format!("F{i}"), 200.0, 30.0));
5248        }
5249
5250        let root = tree.add_node(FormNode {
5251            name: "Root".to_string(),
5252            node_type: FormNodeType::Root,
5253            box_model: BoxModel {
5254                width: Some(400.0),
5255                height: Some(100.0),
5256                max_width: f64::MAX,
5257                max_height: f64::MAX,
5258                ..Default::default()
5259            },
5260            layout: LayoutStrategy::TopToBottom,
5261            children: fields,
5262            occur: Occur::once(),
5263            font: FontMetrics::default(),
5264            calculate: None,
5265            validate: None,
5266            column_widths: vec![],
5267            col_span: 1,
5268        });
5269
5270        let engine = LayoutEngine::new(&tree);
5271        let result = engine.layout(root).unwrap();
5272
5273        // 100pt fits 3 fields (0+30+30+30=90 < 100). 4th at 90+30=120 > 100.
5274        // Page 1: 3 fields, Page 2: 3 fields, Page 3: 3 fields, Page 4: 1 field
5275        assert_eq!(result.pages.len(), 4);
5276        assert_eq!(result.pages[0].nodes.len(), 3);
5277        assert_eq!(result.pages[1].nodes.len(), 3);
5278        assert_eq!(result.pages[2].nodes.len(), 3);
5279        assert_eq!(result.pages[3].nodes.len(), 1);
5280    }
5281
5282    #[test]
5283    fn pagination_profile_reports_height_usage_and_overflow_target() {
5284        let mut tree = FormTree::new();
5285        let mut fields = Vec::new();
5286        for i in 0..4 {
5287            let field = make_field(&mut tree, &format!("F{i}"), 200.0, 30.0);
5288            tree.meta_mut(field).xfa_id = Some(format!("row_{i}"));
5289            fields.push(field);
5290        }
5291
5292        let root = tree.add_node(FormNode {
5293            name: "Root".to_string(),
5294            node_type: FormNodeType::Root,
5295            box_model: BoxModel {
5296                width: Some(400.0),
5297                height: Some(100.0),
5298                max_width: f64::MAX,
5299                max_height: f64::MAX,
5300                ..Default::default()
5301            },
5302            layout: LayoutStrategy::TopToBottom,
5303            children: fields,
5304            occur: Occur::once(),
5305            font: FontMetrics::default(),
5306            calculate: None,
5307            validate: None,
5308            column_widths: vec![],
5309            col_span: 1,
5310        });
5311
5312        let engine = LayoutEngine::new(&tree);
5313        let (layout, profile) = engine.layout_with_profile(root).unwrap();
5314
5315        assert_eq!(layout.pages.len(), 2);
5316        assert_eq!(profile.pages.len(), 2);
5317        assert_eq!(profile.pages[0].page_height, 100.0);
5318        assert_eq!(profile.pages[0].used_height, 90.0);
5319        assert!(profile.pages[0].overflow_to_next);
5320        assert_eq!(
5321            profile.pages[0].first_overflow_element.as_deref(),
5322            Some("field#row_3 (h=30.0)")
5323        );
5324        assert_eq!(profile.pages[1].used_height, 30.0);
5325        assert!(!profile.pages[1].overflow_to_next);
5326        assert!(profile.pages[1].first_overflow_element.is_none());
5327    }
5328
5329    #[test]
5330    fn pagination_with_page_area() {
5331        // PageArea with content area, content overflows to multiple pages
5332        let mut tree = FormTree::new();
5333        let mut fields = Vec::new();
5334        for i in 0..6 {
5335            fields.push(make_field(&mut tree, &format!("F{i}"), 200.0, 50.0));
5336        }
5337
5338        let page_area = tree.add_node(FormNode {
5339            name: "Page1".to_string(),
5340            node_type: FormNodeType::PageArea {
5341                content_areas: vec![ContentArea {
5342                    name: "Body".to_string(),
5343                    x: 20.0,
5344                    y: 20.0,
5345                    width: 360.0,
5346                    height: 160.0, // fits 3 fields of 50pt
5347                    leader: None,
5348                    trailer: None,
5349                }],
5350            },
5351            box_model: BoxModel {
5352                width: Some(400.0),
5353                height: Some(200.0),
5354                max_width: f64::MAX,
5355                max_height: f64::MAX,
5356                ..Default::default()
5357            },
5358            layout: LayoutStrategy::Positioned,
5359            children: vec![],
5360            occur: Occur::once(),
5361            font: FontMetrics::default(),
5362            calculate: None,
5363            validate: None,
5364            column_widths: vec![],
5365            col_span: 1,
5366        });
5367
5368        let mut root_children = vec![page_area];
5369        root_children.extend(fields);
5370
5371        let root = tree.add_node(FormNode {
5372            name: "Root".to_string(),
5373            node_type: FormNodeType::Root,
5374            box_model: BoxModel {
5375                width: Some(400.0),
5376                height: Some(200.0),
5377                max_width: f64::MAX,
5378                max_height: f64::MAX,
5379                ..Default::default()
5380            },
5381            layout: LayoutStrategy::TopToBottom,
5382            children: root_children,
5383            occur: Occur::once(),
5384            font: FontMetrics::default(),
5385            calculate: None,
5386            validate: None,
5387            column_widths: vec![],
5388            col_span: 1,
5389        });
5390
5391        let engine = LayoutEngine::new(&tree);
5392        let result = engine.layout(root).unwrap();
5393
5394        // 6 fields × 50pt = 300pt, content area is 160pt → 2 pages
5395        assert_eq!(result.pages.len(), 2);
5396        assert_eq!(result.pages[0].nodes.len(), 3);
5397        assert_eq!(result.pages[1].nodes.len(), 3);
5398
5399        // Nodes should be offset by content area position (20, 20)
5400        assert_eq!(result.pages[0].nodes[0].rect.x, 20.0);
5401        assert_eq!(result.pages[0].nodes[0].rect.y, 20.0);
5402        assert_eq!(result.pages[0].nodes[1].rect.y, 70.0); // 20 + 50
5403    }
5404
5405    #[test]
5406    fn pagination_with_occur_repeating() {
5407        // Repeating subform creating many instances that overflow
5408        let mut tree = FormTree::new();
5409        let row = tree.add_node(FormNode {
5410            name: "DataRow".to_string(),
5411            node_type: FormNodeType::Subform,
5412            box_model: BoxModel {
5413                width: Some(200.0),
5414                height: Some(25.0),
5415                max_width: f64::MAX,
5416                max_height: f64::MAX,
5417                ..Default::default()
5418            },
5419            layout: LayoutStrategy::Positioned,
5420            children: vec![],
5421            occur: Occur::repeating(1, None, 8),
5422            font: FontMetrics::default(),
5423            calculate: None,
5424            validate: None,
5425            column_widths: vec![],
5426            col_span: 1,
5427        });
5428
5429        let root = tree.add_node(FormNode {
5430            name: "Root".to_string(),
5431            node_type: FormNodeType::Root,
5432            box_model: BoxModel {
5433                width: Some(400.0),
5434                height: Some(100.0),
5435                max_width: f64::MAX,
5436                max_height: f64::MAX,
5437                ..Default::default()
5438            },
5439            layout: LayoutStrategy::TopToBottom,
5440            children: vec![row],
5441            occur: Occur::once(),
5442            font: FontMetrics::default(),
5443            calculate: None,
5444            validate: None,
5445            column_widths: vec![],
5446            col_span: 1,
5447        });
5448
5449        let engine = LayoutEngine::new(&tree);
5450        let result = engine.layout(root).unwrap();
5451
5452        // 8 rows × 25pt = 200pt, page 100pt → 2 pages (4+4)
5453        assert_eq!(result.pages.len(), 2);
5454        assert_eq!(result.pages[0].nodes.len(), 4);
5455        assert_eq!(result.pages[1].nodes.len(), 4);
5456    }
5457
5458    #[test]
5459    fn pagination_oversized_item_forced() {
5460        // Single item taller than page — should still be placed (forced)
5461        let mut tree = FormTree::new();
5462        let f1 = make_field(&mut tree, "Big", 200.0, 200.0); // taller than page
5463        let f2 = make_field(&mut tree, "Small", 200.0, 30.0);
5464
5465        let root = tree.add_node(FormNode {
5466            name: "Root".to_string(),
5467            node_type: FormNodeType::Root,
5468            box_model: BoxModel {
5469                width: Some(400.0),
5470                height: Some(100.0), // page shorter than f1
5471                max_width: f64::MAX,
5472                max_height: f64::MAX,
5473                ..Default::default()
5474            },
5475            layout: LayoutStrategy::TopToBottom,
5476            children: vec![f1, f2],
5477            occur: Occur::once(),
5478            font: FontMetrics::default(),
5479            calculate: None,
5480            validate: None,
5481            column_widths: vec![],
5482            col_span: 1,
5483        });
5484
5485        let engine = LayoutEngine::new(&tree);
5486        let result = engine.layout(root).unwrap();
5487
5488        // Big item forced onto page 1, Small on page 2
5489        assert_eq!(result.pages.len(), 2);
5490        assert_eq!(result.pages[0].nodes[0].name, "Big");
5491        assert_eq!(result.pages[1].nodes[0].name, "Small");
5492    }
5493
5494    #[test]
5495    fn pagination_page_dimensions_correct() {
5496        // All pages should have correct dimensions
5497        let mut tree = FormTree::new();
5498        let mut fields = Vec::new();
5499        for i in 0..5 {
5500            fields.push(make_field(&mut tree, &format!("F{i}"), 200.0, 50.0));
5501        }
5502
5503        let root = tree.add_node(FormNode {
5504            name: "Root".to_string(),
5505            node_type: FormNodeType::Root,
5506            box_model: BoxModel {
5507                width: Some(500.0),
5508                height: Some(120.0),
5509                max_width: f64::MAX,
5510                max_height: f64::MAX,
5511                ..Default::default()
5512            },
5513            layout: LayoutStrategy::TopToBottom,
5514            children: fields,
5515            occur: Occur::once(),
5516            font: FontMetrics::default(),
5517            calculate: None,
5518            validate: None,
5519            column_widths: vec![],
5520            col_span: 1,
5521        });
5522
5523        let engine = LayoutEngine::new(&tree);
5524        let result = engine.layout(root).unwrap();
5525
5526        for page in &result.pages {
5527            assert_eq!(page.width, 500.0);
5528            assert_eq!(page.height, 120.0);
5529        }
5530    }
5531
5532    // --- Content splitting tests (Epic 3.8) ---
5533
5534    #[test]
5535    fn split_subform_across_pages() {
5536        // A tb-subform with 6 children of 30pt each (180pt total)
5537        // on a page with only 100pt remaining after a header.
5538        // The subform should be split: some children on page 1, rest on page 2.
5539        let mut tree = FormTree::new();
5540        let header = make_field(&mut tree, "Header", 300.0, 40.0);
5541
5542        let mut sub_children = Vec::new();
5543        for i in 0..6 {
5544            sub_children.push(make_field(&mut tree, &format!("Row{i}"), 300.0, 30.0));
5545        }
5546
5547        let subform = tree.add_node(FormNode {
5548            name: "DataBlock".to_string(),
5549            node_type: FormNodeType::Subform,
5550            box_model: BoxModel {
5551                width: Some(300.0),
5552                height: None, // growable
5553                max_width: f64::MAX,
5554                max_height: f64::MAX,
5555                ..Default::default()
5556            },
5557            layout: LayoutStrategy::TopToBottom,
5558            children: sub_children,
5559            occur: Occur::once(),
5560            font: FontMetrics::default(),
5561            calculate: None,
5562            validate: None,
5563            column_widths: vec![],
5564            col_span: 1,
5565        });
5566
5567        let root = tree.add_node(FormNode {
5568            name: "Root".to_string(),
5569            node_type: FormNodeType::Root,
5570            box_model: BoxModel {
5571                width: Some(400.0),
5572                height: Some(160.0), // header(40) + 120pt left → fits 4 rows
5573                max_width: f64::MAX,
5574                max_height: f64::MAX,
5575                ..Default::default()
5576            },
5577            layout: LayoutStrategy::TopToBottom,
5578            children: vec![header, subform],
5579            occur: Occur::once(),
5580            font: FontMetrics::default(),
5581            calculate: None,
5582            validate: None,
5583            column_widths: vec![],
5584            col_span: 1,
5585        });
5586
5587        let engine = LayoutEngine::new(&tree);
5588        let result = engine.layout(root).unwrap();
5589
5590        // Page 1: header + partial subform (4 rows fit in 120pt)
5591        // Page 2: remaining 2 rows
5592        assert!(result.pages.len() >= 2);
5593        // Page 1 has header + partial subform
5594        let p1 = &result.pages[0];
5595        assert_eq!(p1.nodes[0].name, "Header");
5596        assert_eq!(p1.nodes[1].name, "DataBlock");
5597        let split_sub = &p1.nodes[1];
5598        assert_eq!(split_sub.children.len(), 4); // 4 rows fit
5599
5600        // Page 2 has the remaining 2 rows
5601        let p2 = &result.pages[1];
5602        assert_eq!(p2.nodes.len(), 2);
5603    }
5604
5605    #[test]
5606    fn split_preserves_node_positions() {
5607        // Verify that split children have correct y positions within their partial container
5608        let mut tree = FormTree::new();
5609        let mut sub_children = Vec::new();
5610        for i in 0..4 {
5611            sub_children.push(make_field(&mut tree, &format!("Row{i}"), 200.0, 25.0));
5612        }
5613
5614        let subform = tree.add_node(FormNode {
5615            name: "Block".to_string(),
5616            node_type: FormNodeType::Subform,
5617            box_model: BoxModel {
5618                width: Some(200.0),
5619                height: None,
5620                max_width: f64::MAX,
5621                max_height: f64::MAX,
5622                ..Default::default()
5623            },
5624            layout: LayoutStrategy::TopToBottom,
5625            children: sub_children,
5626            occur: Occur::once(),
5627            font: FontMetrics::default(),
5628            calculate: None,
5629            validate: None,
5630            column_widths: vec![],
5631            col_span: 1,
5632        });
5633
5634        let root = tree.add_node(FormNode {
5635            name: "Root".to_string(),
5636            node_type: FormNodeType::Root,
5637            box_model: BoxModel {
5638                width: Some(400.0),
5639                height: Some(60.0), // fits 2 rows of 25pt (50pt < 60pt, 75pt > 60pt)
5640                max_width: f64::MAX,
5641                max_height: f64::MAX,
5642                ..Default::default()
5643            },
5644            layout: LayoutStrategy::TopToBottom,
5645            children: vec![subform],
5646            occur: Occur::once(),
5647            font: FontMetrics::default(),
5648            calculate: None,
5649            validate: None,
5650            column_widths: vec![],
5651            col_span: 1,
5652        });
5653
5654        let engine = LayoutEngine::new(&tree);
5655        let result = engine.layout(root).unwrap();
5656
5657        // First page: partial subform with 2 children
5658        let split_sub = &result.pages[0].nodes[0];
5659        assert_eq!(split_sub.children.len(), 2);
5660        assert_eq!(split_sub.children[0].rect.y, 0.0);
5661        assert_eq!(split_sub.children[1].rect.y, 25.0);
5662    }
5663
5664    #[test]
5665    fn split_recurses_into_oversized_first_child() {
5666        let mut tree = FormTree::new();
5667
5668        let mut rows = Vec::new();
5669        for i in 0..6 {
5670            rows.push(make_field(&mut tree, &format!("Row{i}"), 300.0, 30.0));
5671        }
5672
5673        let inner = tree.add_node(FormNode {
5674            name: "InnerBlock".to_string(),
5675            node_type: FormNodeType::Subform,
5676            box_model: BoxModel {
5677                width: Some(300.0),
5678                height: None,
5679                max_width: f64::MAX,
5680                max_height: f64::MAX,
5681                ..Default::default()
5682            },
5683            layout: LayoutStrategy::TopToBottom,
5684            children: rows,
5685            occur: Occur::once(),
5686            font: FontMetrics::default(),
5687            calculate: None,
5688            validate: None,
5689            column_widths: vec![],
5690            col_span: 1,
5691        });
5692
5693        let outer = tree.add_node(FormNode {
5694            name: "OuterBlock".to_string(),
5695            node_type: FormNodeType::Subform,
5696            box_model: BoxModel {
5697                width: Some(300.0),
5698                height: None,
5699                max_width: f64::MAX,
5700                max_height: f64::MAX,
5701                ..Default::default()
5702            },
5703            layout: LayoutStrategy::TopToBottom,
5704            children: vec![inner],
5705            occur: Occur::once(),
5706            font: FontMetrics::default(),
5707            calculate: None,
5708            validate: None,
5709            column_widths: vec![],
5710            col_span: 1,
5711        });
5712
5713        let root = tree.add_node(FormNode {
5714            name: "Root".to_string(),
5715            node_type: FormNodeType::Root,
5716            box_model: BoxModel {
5717                width: Some(400.0),
5718                height: Some(100.0),
5719                max_width: f64::MAX,
5720                max_height: f64::MAX,
5721                ..Default::default()
5722            },
5723            layout: LayoutStrategy::TopToBottom,
5724            children: vec![outer],
5725            occur: Occur::once(),
5726            font: FontMetrics::default(),
5727            calculate: None,
5728            validate: None,
5729            column_widths: vec![],
5730            col_span: 1,
5731        });
5732
5733        let engine = LayoutEngine::new(&tree);
5734        let result = engine.layout(root).unwrap();
5735
5736        assert_eq!(result.pages.len(), 2);
5737        assert_eq!(result.pages[0].nodes[0].name, "OuterBlock");
5738        assert_eq!(result.pages[0].nodes[0].children[0].name, "InnerBlock");
5739        assert_eq!(result.pages[0].nodes[0].children[0].children.len(), 3);
5740    }
5741
5742    #[test]
5743    fn no_split_for_non_tb_layout() {
5744        // A positioned subform should NOT be split — goes entirely to next page
5745        let mut tree = FormTree::new();
5746        let header = make_field(&mut tree, "Header", 300.0, 80.0);
5747
5748        let f1 = tree.add_node(FormNode {
5749            name: "Child1".to_string(),
5750            node_type: FormNodeType::Field {
5751                value: "A".to_string(),
5752            },
5753            box_model: BoxModel {
5754                width: Some(100.0),
5755                height: Some(50.0),
5756                x: 0.0,
5757                y: 0.0,
5758                max_width: f64::MAX,
5759                max_height: f64::MAX,
5760                ..Default::default()
5761            },
5762            layout: LayoutStrategy::Positioned,
5763            children: vec![],
5764            occur: Occur::once(),
5765            font: FontMetrics::default(),
5766            calculate: None,
5767            validate: None,
5768            column_widths: vec![],
5769            col_span: 1,
5770        });
5771
5772        let subform = tree.add_node(FormNode {
5773            name: "PositionedBlock".to_string(),
5774            node_type: FormNodeType::Subform,
5775            box_model: BoxModel {
5776                width: Some(200.0),
5777                height: Some(100.0), // fixed size, doesn't fit after header
5778                max_width: f64::MAX,
5779                max_height: f64::MAX,
5780                ..Default::default()
5781            },
5782            layout: LayoutStrategy::Positioned, // can't split
5783            children: vec![f1],
5784            occur: Occur::once(),
5785            font: FontMetrics::default(),
5786            calculate: None,
5787            validate: None,
5788            column_widths: vec![],
5789            col_span: 1,
5790        });
5791
5792        let root = tree.add_node(FormNode {
5793            name: "Root".to_string(),
5794            node_type: FormNodeType::Root,
5795            box_model: BoxModel {
5796                width: Some(400.0),
5797                height: Some(100.0), // header takes 80pt, subform needs 100pt → overflow
5798                max_width: f64::MAX,
5799                max_height: f64::MAX,
5800                ..Default::default()
5801            },
5802            layout: LayoutStrategy::TopToBottom,
5803            children: vec![header, subform],
5804            occur: Occur::once(),
5805            font: FontMetrics::default(),
5806            calculate: None,
5807            validate: None,
5808            column_widths: vec![],
5809            col_span: 1,
5810        });
5811
5812        let engine = LayoutEngine::new(&tree);
5813        let result = engine.layout(root).unwrap();
5814
5815        // Header on page 1, positioned subform on page 2 (not split)
5816        assert_eq!(result.pages.len(), 2);
5817        assert_eq!(result.pages[0].nodes[0].name, "Header");
5818        assert_eq!(result.pages[1].nodes[0].name, "PositionedBlock");
5819    }
5820
5821    #[test]
5822    fn layout_tb_splits_on_overflow() {
5823        let mut tree = FormTree::new();
5824        let mut children = Vec::new();
5825        for i in 0..10 {
5826            children.push(make_field(&mut tree, &format!("F{i}"), 200.0, 100.0));
5827        }
5828        let parent = make_subform(
5829            &mut tree,
5830            "Parent",
5831            LayoutStrategy::TopToBottom,
5832            Some(200.0),
5833            None,
5834            children,
5835        );
5836
5837        let engine = LayoutEngine::new(&tree);
5838        let parent_children = tree.get(parent).children.clone();
5839        let nodes = engine
5840            .layout_tb(
5841                &parent_children,
5842                Size {
5843                    width: 200.0,
5844                    height: 300.0,
5845                },
5846            )
5847            .unwrap();
5848
5849        // Only three 100pt children fit in a 300pt TB container.
5850        assert_eq!(nodes.len(), 3);
5851        assert!(nodes.iter().all(|n| n.rect.y + n.rect.height <= 301.0));
5852    }
5853
5854    #[test]
5855    fn can_split_checks() {
5856        let mut tree = FormTree::new();
5857        let f1 = make_field(&mut tree, "F1", 100.0, 20.0);
5858
5859        let tb_sub = tree.add_node(FormNode {
5860            name: "TB".to_string(),
5861            node_type: FormNodeType::Subform,
5862            box_model: BoxModel {
5863                max_width: f64::MAX,
5864                max_height: f64::MAX,
5865                ..Default::default()
5866            },
5867            layout: LayoutStrategy::TopToBottom,
5868            children: vec![f1],
5869            occur: Occur::once(),
5870            font: FontMetrics::default(),
5871            calculate: None,
5872            validate: None,
5873            column_widths: vec![],
5874            col_span: 1,
5875        });
5876
5877        let pos_sub = tree.add_node(FormNode {
5878            name: "Pos".to_string(),
5879            node_type: FormNodeType::Subform,
5880            box_model: BoxModel {
5881                max_width: f64::MAX,
5882                max_height: f64::MAX,
5883                ..Default::default()
5884            },
5885            layout: LayoutStrategy::Positioned,
5886            children: vec![f1],
5887            occur: Occur::once(),
5888            font: FontMetrics::default(),
5889            calculate: None,
5890            validate: None,
5891            column_widths: vec![],
5892            col_span: 1,
5893        });
5894
5895        let empty_sub = tree.add_node(FormNode {
5896            name: "Empty".to_string(),
5897            node_type: FormNodeType::Subform,
5898            box_model: BoxModel {
5899                max_width: f64::MAX,
5900                max_height: f64::MAX,
5901                ..Default::default()
5902            },
5903            layout: LayoutStrategy::TopToBottom,
5904            children: vec![],
5905            occur: Occur::once(),
5906            font: FontMetrics::default(),
5907            calculate: None,
5908            validate: None,
5909            column_widths: vec![],
5910            col_span: 1,
5911        });
5912
5913        let engine = LayoutEngine::new(&tree);
5914        assert!(engine.can_split(tb_sub));
5915        // Positioned subforms without explicit height can now be split (#736).
5916        assert!(engine.can_split(pos_sub));
5917        assert!(!engine.can_split(empty_sub));
5918    }
5919
5920    #[test]
5921    fn positioned_subform_paginates_across_pages() {
5922        // A positioned subform with many fields should split across pages
5923        // when its content exceeds the page content area (#736).
5924        let mut tree = FormTree::new();
5925
5926        // Create 10 fields at y = 0, 80, 160, ..., 720
5927        // Each field is 60pt tall, total extent = 780pt.
5928        let mut fields = Vec::new();
5929        for i in 0..10 {
5930            let f = tree.add_node(FormNode {
5931                name: format!("F{i}"),
5932                node_type: FormNodeType::Field {
5933                    value: format!("Value{i}"),
5934                },
5935                box_model: BoxModel {
5936                    width: Some(200.0),
5937                    height: Some(60.0),
5938                    x: 10.0,
5939                    y: i as f64 * 80.0,
5940                    max_width: f64::MAX,
5941                    max_height: f64::MAX,
5942                    ..Default::default()
5943                },
5944                layout: LayoutStrategy::Positioned,
5945                children: vec![],
5946                occur: Occur::once(),
5947                font: FontMetrics::default(),
5948                calculate: None,
5949                validate: None,
5950                column_widths: vec![],
5951                col_span: 1,
5952            });
5953            fields.push(f);
5954        }
5955
5956        // Positioned subform with no explicit height containing all fields.
5957        let positioned = tree.add_node(FormNode {
5958            name: "PositionedBody".to_string(),
5959            node_type: FormNodeType::Subform,
5960            box_model: BoxModel {
5961                width: Some(400.0),
5962                max_width: f64::MAX,
5963                max_height: f64::MAX,
5964                ..Default::default()
5965            },
5966            layout: LayoutStrategy::Positioned,
5967            children: fields,
5968            occur: Occur::once(),
5969            font: FontMetrics::default(),
5970            calculate: None,
5971            validate: None,
5972            column_widths: vec![],
5973            col_span: 1,
5974        });
5975
5976        // Page area with 400pt content height (fits ~5 fields)
5977        let page_area = tree.add_node(FormNode {
5978            name: "Page1".to_string(),
5979            node_type: FormNodeType::PageArea {
5980                content_areas: vec![ContentArea {
5981                    name: "Body".to_string(),
5982                    x: 0.0,
5983                    y: 0.0,
5984                    width: 400.0,
5985                    height: 400.0,
5986                    leader: None,
5987                    trailer: None,
5988                }],
5989            },
5990            box_model: BoxModel {
5991                width: Some(400.0),
5992                height: Some(400.0),
5993                max_width: f64::MAX,
5994                max_height: f64::MAX,
5995                ..Default::default()
5996            },
5997            layout: LayoutStrategy::Positioned,
5998            children: vec![],
5999            occur: Occur::once(),
6000            font: FontMetrics::default(),
6001            calculate: None,
6002            validate: None,
6003            column_widths: vec![],
6004            col_span: 1,
6005        });
6006
6007        let root = tree.add_node(FormNode {
6008            name: "Root".to_string(),
6009            node_type: FormNodeType::Root,
6010            box_model: BoxModel {
6011                max_width: f64::MAX,
6012                max_height: f64::MAX,
6013                ..Default::default()
6014            },
6015            layout: LayoutStrategy::TopToBottom,
6016            children: vec![page_area, positioned],
6017            occur: Occur::once(),
6018            font: FontMetrics::default(),
6019            calculate: None,
6020            validate: None,
6021            column_widths: vec![],
6022            col_span: 1,
6023        });
6024
6025        let engine = LayoutEngine::new(&tree);
6026        let result = engine.layout(root).unwrap();
6027
6028        // 10 fields at y=0,80,...,720 with height 60, page height 400.
6029        // Page 1: fields at y=0..319 (y+h<=400) = F0(0+60), F1(80+60), F2(160+60),
6030        //         F3(240+60), F4(320+60=380) -> 5 fields
6031        // Page 2: fields at y=400..719, shifted -> F5(0+60), F6(80+60), F7(160+60),
6032        //         F8(240+60), F9(320+60=380) -> 5 fields
6033        assert!(
6034            result.pages.len() >= 2,
6035            "Expected at least 2 pages, got {}",
6036            result.pages.len()
6037        );
6038
6039        // Page 1 should have the positioned subform with some children
6040        let page1_children = count_leaf_nodes(&result.pages[0]);
6041        let page2_children = count_leaf_nodes(&result.pages[1]);
6042        assert!(page1_children > 0, "Page 1 should have content");
6043        assert!(page2_children > 0, "Page 2 should have content");
6044        assert_eq!(
6045            page1_children + page2_children,
6046            10,
6047            "All 10 fields should be placed across pages"
6048        );
6049    }
6050
6051    #[test]
6052    fn positioned_split_preserves_parent_margin_offset() {
6053        use crate::types::Insets;
6054
6055        let mut tree = FormTree::new();
6056
6057        let first = tree.add_node(FormNode {
6058            name: "First".to_string(),
6059            node_type: FormNodeType::Field {
6060                value: "First".to_string(),
6061            },
6062            box_model: BoxModel {
6063                width: Some(100.0),
6064                height: Some(40.0),
6065                x: 0.0,
6066                y: 0.0,
6067                max_width: f64::MAX,
6068                max_height: f64::MAX,
6069                ..Default::default()
6070            },
6071            layout: LayoutStrategy::Positioned,
6072            children: vec![],
6073            occur: Occur::once(),
6074            font: FontMetrics::default(),
6075            calculate: None,
6076            validate: None,
6077            column_widths: vec![],
6078            col_span: 1,
6079        });
6080        let second = tree.add_node(FormNode {
6081            name: "Second".to_string(),
6082            node_type: FormNodeType::Field {
6083                value: "Second".to_string(),
6084            },
6085            box_model: BoxModel {
6086                width: Some(100.0),
6087                height: Some(40.0),
6088                x: 0.0,
6089                y: 60.0,
6090                max_width: f64::MAX,
6091                max_height: f64::MAX,
6092                ..Default::default()
6093            },
6094            layout: LayoutStrategy::Positioned,
6095            children: vec![],
6096            occur: Occur::once(),
6097            font: FontMetrics::default(),
6098            calculate: None,
6099            validate: None,
6100            column_widths: vec![],
6101            col_span: 1,
6102        });
6103
6104        let positioned = tree.add_node(FormNode {
6105            name: "PositionedBody".to_string(),
6106            node_type: FormNodeType::Subform,
6107            box_model: BoxModel {
6108                width: Some(140.0),
6109                margins: Insets {
6110                    top: 10.0,
6111                    right: 0.0,
6112                    bottom: 0.0,
6113                    left: 8.0,
6114                },
6115                max_width: f64::MAX,
6116                max_height: f64::MAX,
6117                ..Default::default()
6118            },
6119            layout: LayoutStrategy::Positioned,
6120            children: vec![first, second],
6121            occur: Occur::once(),
6122            font: FontMetrics::default(),
6123            calculate: None,
6124            validate: None,
6125            column_widths: vec![],
6126            col_span: 1,
6127        });
6128
6129        let page_area = tree.add_node(FormNode {
6130            name: "Page1".to_string(),
6131            node_type: FormNodeType::PageArea {
6132                content_areas: vec![ContentArea {
6133                    name: "Body".to_string(),
6134                    x: 0.0,
6135                    y: 0.0,
6136                    width: 200.0,
6137                    height: 70.0,
6138                    leader: None,
6139                    trailer: None,
6140                }],
6141            },
6142            box_model: BoxModel {
6143                width: Some(200.0),
6144                height: Some(70.0),
6145                max_width: f64::MAX,
6146                max_height: f64::MAX,
6147                ..Default::default()
6148            },
6149            layout: LayoutStrategy::Positioned,
6150            children: vec![],
6151            occur: Occur::once(),
6152            font: FontMetrics::default(),
6153            calculate: None,
6154            validate: None,
6155            column_widths: vec![],
6156            col_span: 1,
6157        });
6158
6159        let root = tree.add_node(FormNode {
6160            name: "Root".to_string(),
6161            node_type: FormNodeType::Root,
6162            box_model: BoxModel {
6163                max_width: f64::MAX,
6164                max_height: f64::MAX,
6165                ..Default::default()
6166            },
6167            layout: LayoutStrategy::TopToBottom,
6168            children: vec![page_area, positioned],
6169            occur: Occur::once(),
6170            font: FontMetrics::default(),
6171            calculate: None,
6172            validate: None,
6173            column_widths: vec![],
6174            col_span: 1,
6175        });
6176
6177        let engine = LayoutEngine::new(&tree);
6178        let result = engine.layout(root).unwrap();
6179
6180        assert_eq!(result.pages.len(), 2);
6181        assert_eq!(result.pages[0].nodes[0].children.len(), 1);
6182        assert_eq!(result.pages[1].nodes[0].children.len(), 1);
6183        assert_eq!(result.pages[0].nodes[0].children[0].rect.x, 8.0);
6184        assert_eq!(result.pages[0].nodes[0].children[0].rect.y, 10.0);
6185        assert_eq!(result.pages[1].nodes[0].children[0].rect.x, 8.0);
6186        assert_eq!(result.pages[1].nodes[0].children[0].rect.y, 10.0);
6187    }
6188
6189    #[test]
6190    fn single_positioned_child_no_overpagination() {
6191        // #794: A TopToBottom root with a single Positioned child whose
6192        // children are all Positioned and fit on one page should produce
6193        // exactly 1 page, not 2+.
6194        let mut tree = FormTree::new();
6195
6196        // 5 positioned fields that fit within a 792pt page
6197        let mut fields = Vec::new();
6198        for i in 0..5 {
6199            let f = tree.add_node(FormNode {
6200                name: format!("F{i}"),
6201                node_type: FormNodeType::Field {
6202                    value: format!("Value{i}"),
6203                },
6204                box_model: BoxModel {
6205                    width: Some(200.0),
6206                    height: Some(30.0),
6207                    x: 36.0,
6208                    y: 36.0 + i as f64 * 40.0,
6209                    max_width: f64::MAX,
6210                    max_height: f64::MAX,
6211                    ..Default::default()
6212                },
6213                layout: LayoutStrategy::Positioned,
6214                children: vec![],
6215                occur: Occur::once(),
6216                font: FontMetrics::default(),
6217                calculate: None,
6218                validate: None,
6219                column_widths: vec![],
6220                col_span: 1,
6221            });
6222            fields.push(f);
6223        }
6224
6225        // Positioned subform containing all fields — fits on one page
6226        let positioned = tree.add_node(FormNode {
6227            name: "PageSubform".to_string(),
6228            node_type: FormNodeType::Subform,
6229            box_model: BoxModel {
6230                width: Some(612.0),
6231                height: Some(792.0),
6232                max_width: f64::MAX,
6233                max_height: f64::MAX,
6234                ..Default::default()
6235            },
6236            layout: LayoutStrategy::Positioned,
6237            children: fields,
6238            occur: Occur::once(),
6239            font: FontMetrics::default(),
6240            calculate: None,
6241            validate: None,
6242            column_widths: vec![],
6243            col_span: 1,
6244        });
6245
6246        let page_area = tree.add_node(FormNode {
6247            name: "Page1".to_string(),
6248            node_type: FormNodeType::PageArea {
6249                content_areas: vec![ContentArea {
6250                    name: "Body".to_string(),
6251                    x: 0.0,
6252                    y: 0.0,
6253                    width: 612.0,
6254                    height: 792.0,
6255                    leader: None,
6256                    trailer: None,
6257                }],
6258            },
6259            box_model: BoxModel {
6260                width: Some(612.0),
6261                height: Some(792.0),
6262                max_width: f64::MAX,
6263                max_height: f64::MAX,
6264                ..Default::default()
6265            },
6266            layout: LayoutStrategy::Positioned,
6267            children: vec![],
6268            occur: Occur::once(),
6269            font: FontMetrics::default(),
6270            calculate: None,
6271            validate: None,
6272            column_widths: vec![],
6273            col_span: 1,
6274        });
6275
6276        let root = tree.add_node(FormNode {
6277            name: "Root".to_string(),
6278            node_type: FormNodeType::Root,
6279            box_model: BoxModel {
6280                max_width: f64::MAX,
6281                max_height: f64::MAX,
6282                ..Default::default()
6283            },
6284            layout: LayoutStrategy::TopToBottom,
6285            children: vec![page_area, positioned],
6286            occur: Occur::once(),
6287            font: FontMetrics::default(),
6288            calculate: None,
6289            validate: None,
6290            column_widths: vec![],
6291            col_span: 1,
6292        });
6293
6294        let engine = LayoutEngine::new(&tree);
6295        let result = engine.layout(root).unwrap();
6296
6297        assert_eq!(
6298            result.pages.len(),
6299            1,
6300            "Single positioned child fitting on one page should produce 1 page, got {}",
6301            result.pages.len()
6302        );
6303
6304        // All 5 fields should be on the single page
6305        let leaf_count = count_leaf_nodes(&result.pages[0]);
6306        assert_eq!(leaf_count, 5, "All 5 fields should be placed on page 1");
6307    }
6308
6309    #[test]
6310    fn positioned_inside_tb_no_infinite_pagination() {
6311        // Regression test for #737: a positioned subform nested inside a
6312        // tb-layout parent caused infinite pagination because the
6313        // children_override from split_positioned_node was discarded when
6314        // split_tb_node re-wrapped the rest.
6315        //
6316        // Structure:
6317        //   Root (tb)
6318        //     PageArea (content height = 300)
6319        //     TbWrapper (tb, no height)
6320        //       PositionedBody (positioned, no height)
6321        //         8 fields at y = 0, 80, 160, ..., 560 (each 60pt tall)
6322        //
6323        // Content spans 620pt.  With 300pt pages, we expect 3 pages max.
6324        // Before the fix, this produced MAX_PAGES pages.
6325        let mut tree = FormTree::new();
6326
6327        let mut fields = Vec::new();
6328        for i in 0..8 {
6329            let f = tree.add_node(FormNode {
6330                name: format!("F{i}"),
6331                node_type: FormNodeType::Field {
6332                    value: format!("Val{i}"),
6333                },
6334                box_model: BoxModel {
6335                    width: Some(200.0),
6336                    height: Some(60.0),
6337                    x: 10.0,
6338                    y: i as f64 * 80.0,
6339                    max_width: f64::MAX,
6340                    max_height: f64::MAX,
6341                    ..Default::default()
6342                },
6343                layout: LayoutStrategy::Positioned,
6344                children: vec![],
6345                occur: Occur::once(),
6346                font: FontMetrics::default(),
6347                calculate: None,
6348                validate: None,
6349                column_widths: vec![],
6350                col_span: 1,
6351            });
6352            fields.push(f);
6353        }
6354
6355        let positioned = tree.add_node(FormNode {
6356            name: "PositionedBody".to_string(),
6357            node_type: FormNodeType::Subform,
6358            box_model: BoxModel {
6359                width: Some(400.0),
6360                max_width: f64::MAX,
6361                max_height: f64::MAX,
6362                ..Default::default()
6363            },
6364            layout: LayoutStrategy::Positioned,
6365            children: fields,
6366            occur: Occur::once(),
6367            font: FontMetrics::default(),
6368            calculate: None,
6369            validate: None,
6370            column_widths: vec![],
6371            col_span: 1,
6372        });
6373
6374        // Wrap the positioned subform in a tb-layout parent — this is the
6375        // configuration that triggered the bug.
6376        let tb_wrapper = tree.add_node(FormNode {
6377            name: "TbWrapper".to_string(),
6378            node_type: FormNodeType::Subform,
6379            box_model: BoxModel {
6380                width: Some(400.0),
6381                max_width: f64::MAX,
6382                max_height: f64::MAX,
6383                ..Default::default()
6384            },
6385            layout: LayoutStrategy::TopToBottom,
6386            children: vec![positioned],
6387            occur: Occur::once(),
6388            font: FontMetrics::default(),
6389            calculate: None,
6390            validate: None,
6391            column_widths: vec![],
6392            col_span: 1,
6393        });
6394
6395        let page_area = tree.add_node(FormNode {
6396            name: "Page1".to_string(),
6397            node_type: FormNodeType::PageArea {
6398                content_areas: vec![ContentArea {
6399                    name: "Body".to_string(),
6400                    x: 0.0,
6401                    y: 0.0,
6402                    width: 400.0,
6403                    height: 300.0,
6404                    leader: None,
6405                    trailer: None,
6406                }],
6407            },
6408            box_model: BoxModel {
6409                width: Some(400.0),
6410                height: Some(300.0),
6411                max_width: f64::MAX,
6412                max_height: f64::MAX,
6413                ..Default::default()
6414            },
6415            layout: LayoutStrategy::Positioned,
6416            children: vec![],
6417            occur: Occur::once(),
6418            font: FontMetrics::default(),
6419            calculate: None,
6420            validate: None,
6421            column_widths: vec![],
6422            col_span: 1,
6423        });
6424
6425        let root = tree.add_node(FormNode {
6426            name: "Root".to_string(),
6427            node_type: FormNodeType::Root,
6428            box_model: BoxModel {
6429                max_width: f64::MAX,
6430                max_height: f64::MAX,
6431                ..Default::default()
6432            },
6433            layout: LayoutStrategy::TopToBottom,
6434            children: vec![page_area, tb_wrapper],
6435            occur: Occur::once(),
6436            font: FontMetrics::default(),
6437            calculate: None,
6438            validate: None,
6439            column_widths: vec![],
6440            col_span: 1,
6441        });
6442
6443        let engine = LayoutEngine::new(&tree);
6444        let result = engine.layout(root).unwrap();
6445
6446        // 8 fields spanning 620pt, pages of 300pt → should be 3 pages.
6447        // The critical assertion: we must NOT produce hundreds of pages.
6448        assert!(
6449            result.pages.len() <= 5,
6450            "Expected at most 5 pages for 8 fields across 300pt pages, got {} \
6451             (infinite pagination bug #737)",
6452            result.pages.len()
6453        );
6454        assert!(
6455            result.pages.len() >= 2,
6456            "Expected at least 2 pages, got {}",
6457            result.pages.len()
6458        );
6459
6460        // All 8 fields should be distributed across the pages.
6461        let total_leaves: usize = result.pages.iter().map(|p| count_leaf_nodes(p)).sum();
6462        assert_eq!(
6463            total_leaves, 8,
6464            "All 8 fields should appear across pages, found {}",
6465            total_leaves
6466        );
6467    }
6468
6469    #[test]
6470    fn positioned_subform_with_explicit_height_not_split() {
6471        // A positioned subform with explicit height should NOT be split.
6472        let mut tree = FormTree::new();
6473        let f1 = make_field(&mut tree, "F1", 100.0, 20.0);
6474
6475        let positioned = tree.add_node(FormNode {
6476            name: "FixedBlock".to_string(),
6477            node_type: FormNodeType::Subform,
6478            box_model: BoxModel {
6479                width: Some(200.0),
6480                height: Some(500.0), // explicit height
6481                max_width: f64::MAX,
6482                max_height: f64::MAX,
6483                ..Default::default()
6484            },
6485            layout: LayoutStrategy::Positioned,
6486            children: vec![f1],
6487            occur: Occur::once(),
6488            font: FontMetrics::default(),
6489            calculate: None,
6490            validate: None,
6491            column_widths: vec![],
6492            col_span: 1,
6493        });
6494
6495        let engine = LayoutEngine::new(&tree);
6496        assert!(
6497            !engine.can_split(positioned),
6498            "Positioned subform with explicit height should not be splittable"
6499        );
6500    }
6501
6502    // --- Leaders & trailers tests (Epic 3.10) ---
6503
6504    #[test]
6505    fn leader_placed_at_top() {
6506        // Leader (header) should appear at the top of each page
6507        let mut tree = FormTree::new();
6508        let header = make_field(&mut tree, "PageHeader", 300.0, 30.0);
6509        let f1 = make_field(&mut tree, "Content1", 300.0, 50.0);
6510        let f2 = make_field(&mut tree, "Content2", 300.0, 50.0);
6511
6512        let page_area = tree.add_node(FormNode {
6513            name: "Page1".to_string(),
6514            node_type: FormNodeType::PageArea {
6515                content_areas: vec![ContentArea {
6516                    name: "Body".to_string(),
6517                    x: 0.0,
6518                    y: 0.0,
6519                    width: 400.0,
6520                    height: 200.0,
6521                    leader: Some(header),
6522                    trailer: None,
6523                }],
6524            },
6525            box_model: BoxModel {
6526                width: Some(400.0),
6527                height: Some(200.0),
6528                max_width: f64::MAX,
6529                max_height: f64::MAX,
6530                ..Default::default()
6531            },
6532            layout: LayoutStrategy::Positioned,
6533            children: vec![],
6534            occur: Occur::once(),
6535            font: FontMetrics::default(),
6536            calculate: None,
6537            validate: None,
6538            column_widths: vec![],
6539            col_span: 1,
6540        });
6541
6542        let root = tree.add_node(FormNode {
6543            name: "Root".to_string(),
6544            node_type: FormNodeType::Root,
6545            box_model: BoxModel {
6546                width: Some(400.0),
6547                height: Some(200.0),
6548                max_width: f64::MAX,
6549                max_height: f64::MAX,
6550                ..Default::default()
6551            },
6552            layout: LayoutStrategy::TopToBottom,
6553            children: vec![page_area, f1, f2],
6554            occur: Occur::once(),
6555            font: FontMetrics::default(),
6556            calculate: None,
6557            validate: None,
6558            column_widths: vec![],
6559            col_span: 1,
6560        });
6561
6562        let engine = LayoutEngine::new(&tree);
6563        let result = engine.layout(root).unwrap();
6564
6565        let page = &result.pages[0];
6566        // Header at y=0, content starts at y=30
6567        assert_eq!(page.nodes[0].name, "PageHeader");
6568        assert_eq!(page.nodes[0].rect.y, 0.0);
6569        // Content after header
6570        assert!(page.nodes.len() >= 2);
6571        // First content node at y=30 (after header)
6572        let first_content = page.nodes.iter().find(|n| n.name == "Content1").unwrap();
6573        assert_eq!(first_content.rect.y, 30.0);
6574    }
6575
6576    #[test]
6577    fn trailer_placed_at_bottom() {
6578        // Trailer (footer) should appear at the bottom of the content area
6579        let mut tree = FormTree::new();
6580        let footer = make_field(&mut tree, "PageFooter", 300.0, 25.0);
6581        let f1 = make_field(&mut tree, "Content1", 300.0, 50.0);
6582
6583        let page_area = tree.add_node(FormNode {
6584            name: "Page1".to_string(),
6585            node_type: FormNodeType::PageArea {
6586                content_areas: vec![ContentArea {
6587                    name: "Body".to_string(),
6588                    x: 0.0,
6589                    y: 0.0,
6590                    width: 400.0,
6591                    height: 200.0,
6592                    leader: None,
6593                    trailer: Some(footer),
6594                }],
6595            },
6596            box_model: BoxModel {
6597                width: Some(400.0),
6598                height: Some(200.0),
6599                max_width: f64::MAX,
6600                max_height: f64::MAX,
6601                ..Default::default()
6602            },
6603            layout: LayoutStrategy::Positioned,
6604            children: vec![],
6605            occur: Occur::once(),
6606            font: FontMetrics::default(),
6607            calculate: None,
6608            validate: None,
6609            column_widths: vec![],
6610            col_span: 1,
6611        });
6612
6613        let root = tree.add_node(FormNode {
6614            name: "Root".to_string(),
6615            node_type: FormNodeType::Root,
6616            box_model: BoxModel {
6617                width: Some(400.0),
6618                height: Some(200.0),
6619                max_width: f64::MAX,
6620                max_height: f64::MAX,
6621                ..Default::default()
6622            },
6623            layout: LayoutStrategy::TopToBottom,
6624            children: vec![page_area, f1],
6625            occur: Occur::once(),
6626            font: FontMetrics::default(),
6627            calculate: None,
6628            validate: None,
6629            column_widths: vec![],
6630            col_span: 1,
6631        });
6632
6633        let engine = LayoutEngine::new(&tree);
6634        let result = engine.layout(root).unwrap();
6635
6636        let page = &result.pages[0];
6637        // Footer at bottom: y = 200 - 25 = 175
6638        let footer_node = page.nodes.iter().find(|n| n.name == "PageFooter").unwrap();
6639        assert_eq!(footer_node.rect.y, 175.0);
6640    }
6641
6642    #[test]
6643    fn leader_and_trailer_reduce_content_space() {
6644        // With both leader and trailer, content space is reduced
6645        let mut tree = FormTree::new();
6646        let header = make_field(&mut tree, "Header", 300.0, 30.0);
6647        let footer = make_field(&mut tree, "Footer", 300.0, 20.0);
6648
6649        // 5 fields of 30pt each = 150pt total
6650        let mut fields = Vec::new();
6651        for i in 0..5 {
6652            fields.push(make_field(&mut tree, &format!("F{i}"), 300.0, 30.0));
6653        }
6654
6655        let page_area = tree.add_node(FormNode {
6656            name: "Page1".to_string(),
6657            node_type: FormNodeType::PageArea {
6658                content_areas: vec![ContentArea {
6659                    name: "Body".to_string(),
6660                    x: 0.0,
6661                    y: 0.0,
6662                    width: 400.0,
6663                    height: 200.0, // 200 - 30(header) - 20(footer) = 150pt for content
6664                    leader: Some(header),
6665                    trailer: Some(footer),
6666                }],
6667            },
6668            box_model: BoxModel {
6669                width: Some(400.0),
6670                height: Some(200.0),
6671                max_width: f64::MAX,
6672                max_height: f64::MAX,
6673                ..Default::default()
6674            },
6675            layout: LayoutStrategy::Positioned,
6676            children: vec![],
6677            occur: Occur::once(),
6678            font: FontMetrics::default(),
6679            calculate: None,
6680            validate: None,
6681            column_widths: vec![],
6682            col_span: 1,
6683        });
6684
6685        let mut root_children = vec![page_area];
6686        root_children.extend(fields);
6687
6688        let root = tree.add_node(FormNode {
6689            name: "Root".to_string(),
6690            node_type: FormNodeType::Root,
6691            box_model: BoxModel {
6692                width: Some(400.0),
6693                height: Some(200.0),
6694                max_width: f64::MAX,
6695                max_height: f64::MAX,
6696                ..Default::default()
6697            },
6698            layout: LayoutStrategy::TopToBottom,
6699            children: root_children,
6700            occur: Occur::once(),
6701            font: FontMetrics::default(),
6702            calculate: None,
6703            validate: None,
6704            column_widths: vec![],
6705            col_span: 1,
6706        });
6707
6708        let engine = LayoutEngine::new(&tree);
6709        let result = engine.layout(root).unwrap();
6710
6711        // Content space is 150pt, 5 fields × 30pt = 150pt → exactly fits on 1 page
6712        assert_eq!(result.pages.len(), 1);
6713        let page = &result.pages[0];
6714        // header + 5 content + footer = 7 nodes
6715        assert_eq!(page.nodes.len(), 7);
6716    }
6717
6718    #[test]
6719    fn leader_trailer_repeated_on_overflow_pages() {
6720        // When content overflows, leaders/trailers should appear on each page
6721        let mut tree = FormTree::new();
6722        let header = make_field(&mut tree, "Header", 300.0, 30.0);
6723        let footer = make_field(&mut tree, "Footer", 300.0, 20.0);
6724
6725        // 8 fields of 30pt = 240pt, available per page = 200-30-20 = 150pt
6726        // Page 1: 5 fields (150pt), Page 2: 3 fields (90pt)
6727        let mut fields = Vec::new();
6728        for i in 0..8 {
6729            fields.push(make_field(&mut tree, &format!("F{i}"), 300.0, 30.0));
6730        }
6731
6732        let page_area = tree.add_node(FormNode {
6733            name: "Page1".to_string(),
6734            node_type: FormNodeType::PageArea {
6735                content_areas: vec![ContentArea {
6736                    name: "Body".to_string(),
6737                    x: 0.0,
6738                    y: 0.0,
6739                    width: 400.0,
6740                    height: 200.0,
6741                    leader: Some(header),
6742                    trailer: Some(footer),
6743                }],
6744            },
6745            box_model: BoxModel {
6746                width: Some(400.0),
6747                height: Some(200.0),
6748                max_width: f64::MAX,
6749                max_height: f64::MAX,
6750                ..Default::default()
6751            },
6752            layout: LayoutStrategy::Positioned,
6753            children: vec![],
6754            occur: Occur::once(),
6755            font: FontMetrics::default(),
6756            calculate: None,
6757            validate: None,
6758            column_widths: vec![],
6759            col_span: 1,
6760        });
6761
6762        let mut root_children = vec![page_area];
6763        root_children.extend(fields);
6764
6765        let root = tree.add_node(FormNode {
6766            name: "Root".to_string(),
6767            node_type: FormNodeType::Root,
6768            box_model: BoxModel {
6769                width: Some(400.0),
6770                height: Some(200.0),
6771                max_width: f64::MAX,
6772                max_height: f64::MAX,
6773                ..Default::default()
6774            },
6775            layout: LayoutStrategy::TopToBottom,
6776            children: root_children,
6777            occur: Occur::once(),
6778            font: FontMetrics::default(),
6779            calculate: None,
6780            validate: None,
6781            column_widths: vec![],
6782            col_span: 1,
6783        });
6784
6785        let engine = LayoutEngine::new(&tree);
6786        let result = engine.layout(root).unwrap();
6787
6788        assert_eq!(result.pages.len(), 2);
6789
6790        // Both pages should have header and footer
6791        for page in &result.pages {
6792            let has_header = page.nodes.iter().any(|n| n.name == "Header");
6793            let has_footer = page.nodes.iter().any(|n| n.name == "Footer");
6794            assert!(has_header, "Page missing header");
6795            assert!(has_footer, "Page missing footer");
6796        }
6797    }
6798
6799    // ── Text placement tests ──────────────────────────────────────────
6800
6801    #[test]
6802    fn draw_node_growable_height_from_text() {
6803        // A Draw node with fixed width but no height should grow to fit text
6804        let mut tree = FormTree::new();
6805        let draw = tree.add_node(FormNode {
6806            name: "Label".to_string(),
6807            node_type: FormNodeType::Draw(DrawContent::Text("Hello World".to_string())),
6808            box_model: BoxModel {
6809                width: Some(200.0),
6810                height: None, // growable
6811                max_width: f64::MAX,
6812                max_height: f64::MAX,
6813                ..Default::default()
6814            },
6815            layout: LayoutStrategy::Positioned,
6816            children: vec![],
6817            occur: Occur::once(),
6818            font: FontMetrics::default(), // 10pt, avg_char_width=0.5
6819            calculate: None,
6820            validate: None,
6821            column_widths: vec![],
6822            col_span: 1,
6823        });
6824        let root = make_subform(
6825            &mut tree,
6826            "Root",
6827            LayoutStrategy::TopToBottom,
6828            Some(612.0),
6829            Some(792.0),
6830            vec![draw],
6831        );
6832
6833        let engine = LayoutEngine::new(&tree);
6834        let result = engine.layout(root).unwrap();
6835
6836        let page = &result.pages[0];
6837        let label = &page.nodes[0];
6838        // "Hello World" = 11 chars * 10pt * 0.5 = 55pt wide, fits in 200pt
6839        // 1 line * 10pt * 1.2 = 12pt tall
6840        assert_eq!(label.rect.height, 12.0);
6841    }
6842
6843    #[test]
6844    fn draw_node_text_wraps_in_narrow_width() {
6845        // A Draw node with narrow width should wrap text and grow taller
6846        let mut tree = FormTree::new();
6847        let draw = tree.add_node(FormNode {
6848            name: "Label".to_string(),
6849            node_type: FormNodeType::Draw(DrawContent::Text("Hello World".to_string())),
6850            box_model: BoxModel {
6851                width: Some(40.0), // narrow: "Hello" = 25pt fits, "World" wraps
6852                height: None,
6853                max_width: f64::MAX,
6854                max_height: f64::MAX,
6855                ..Default::default()
6856            },
6857            layout: LayoutStrategy::Positioned,
6858            children: vec![],
6859            occur: Occur::once(),
6860            font: FontMetrics::default(),
6861            calculate: None,
6862            validate: None,
6863            column_widths: vec![],
6864            col_span: 1,
6865        });
6866        let root = make_subform(
6867            &mut tree,
6868            "Root",
6869            LayoutStrategy::TopToBottom,
6870            Some(612.0),
6871            Some(792.0),
6872            vec![draw],
6873        );
6874
6875        let engine = LayoutEngine::new(&tree);
6876        let result = engine.layout(root).unwrap();
6877
6878        let label = &result.pages[0].nodes[0];
6879        // 2 lines * 12pt = 24pt
6880        assert_eq!(label.rect.height, 24.0);
6881    }
6882
6883    #[test]
6884    fn field_produces_wrapped_text_content() {
6885        let mut tree = FormTree::new();
6886        let field = tree.add_node(FormNode {
6887            name: "Name".to_string(),
6888            node_type: FormNodeType::Field {
6889                value: "John".to_string(),
6890            },
6891            box_model: BoxModel {
6892                width: Some(200.0),
6893                height: Some(20.0),
6894                max_width: f64::MAX,
6895                max_height: f64::MAX,
6896                ..Default::default()
6897            },
6898            layout: LayoutStrategy::Positioned,
6899            children: vec![],
6900            occur: Occur::once(),
6901            font: FontMetrics::default(),
6902            calculate: None,
6903            validate: None,
6904            column_widths: vec![],
6905            col_span: 1,
6906        });
6907        let root = make_subform(
6908            &mut tree,
6909            "Root",
6910            LayoutStrategy::TopToBottom,
6911            Some(612.0),
6912            Some(792.0),
6913            vec![field],
6914        );
6915
6916        let engine = LayoutEngine::new(&tree);
6917        let result = engine.layout(root).unwrap();
6918
6919        let node = &result.pages[0].nodes[0];
6920        match &node.content {
6921            LayoutContent::WrappedText {
6922                lines, font_size, ..
6923            } => {
6924                assert_eq!(lines.len(), 1);
6925                assert_eq!(lines[0], "John");
6926                assert_eq!(*font_size, 10.0);
6927            }
6928            other => panic!("Expected WrappedText, got {:?}", other),
6929        }
6930    }
6931
6932    #[test]
6933    fn draw_growable_width_and_height_from_text() {
6934        // Both width and height are growable: should size to text content
6935        let mut tree = FormTree::new();
6936        let draw = tree.add_node(FormNode {
6937            name: "Auto".to_string(),
6938            node_type: FormNodeType::Draw(DrawContent::Text("Test".to_string())),
6939            box_model: BoxModel {
6940                width: None,
6941                height: None,
6942                max_width: f64::MAX,
6943                max_height: f64::MAX,
6944                ..Default::default()
6945            },
6946            layout: LayoutStrategy::Positioned,
6947            children: vec![],
6948            occur: Occur::once(),
6949            font: FontMetrics::default(),
6950            calculate: None,
6951            validate: None,
6952            column_widths: vec![],
6953            col_span: 1,
6954        });
6955
6956        let engine = LayoutEngine::new(&tree);
6957        let size = engine.compute_extent(draw);
6958        // "Test" measured with Helvetica AFM widths: T=611 e=556 s=500 t=278 = 1945/1000*10 = 19.45
6959        assert!((size.width - 19.45).abs() < 0.1, "width={}", size.width);
6960        assert_eq!(size.height, 12.0);
6961    }
6962
6963    #[test]
6964    fn custom_font_size_affects_layout() {
6965        let mut tree = FormTree::new();
6966        let draw = tree.add_node(FormNode {
6967            name: "Big".to_string(),
6968            node_type: FormNodeType::Draw(DrawContent::Text("Hi".to_string())),
6969            box_model: BoxModel {
6970                width: None,
6971                height: None,
6972                max_width: f64::MAX,
6973                max_height: f64::MAX,
6974                ..Default::default()
6975            },
6976            layout: LayoutStrategy::Positioned,
6977            children: vec![],
6978            occur: Occur::once(),
6979            font: FontMetrics::new(20.0), // 20pt font
6980            calculate: None,
6981            validate: None,
6982            column_widths: vec![],
6983            col_span: 1,
6984        });
6985
6986        let engine = LayoutEngine::new(&tree);
6987        let size = engine.compute_extent(draw);
6988        // "Hi" measured with Helvetica AFM widths: H=722 i=222 = 944/1000*20 = 18.88
6989        assert!((size.width - 18.88).abs() < 0.1, "width={}", size.width);
6990        assert_eq!(size.height, 24.0);
6991    }
6992
6993    // =========================================================================
6994    // Table Layout Tests
6995    // =========================================================================
6996
6997    fn make_cell(tree: &mut FormTree, name: &str, w: f64, h: f64, col_span: i32) -> FormNodeId {
6998        tree.add_node(FormNode {
6999            name: name.to_string(),
7000            node_type: FormNodeType::Field {
7001                value: name.to_string(),
7002            },
7003            box_model: BoxModel {
7004                width: Some(w),
7005                height: Some(h),
7006                max_width: f64::MAX,
7007                max_height: f64::MAX,
7008                ..Default::default()
7009            },
7010            layout: LayoutStrategy::Positioned,
7011            children: vec![],
7012            occur: Occur::once(),
7013            font: FontMetrics::default(),
7014            calculate: None,
7015            validate: None,
7016            column_widths: vec![],
7017            col_span,
7018        })
7019    }
7020
7021    fn make_row(tree: &mut FormTree, name: &str, cells: Vec<FormNodeId>) -> FormNodeId {
7022        tree.add_node(FormNode {
7023            name: name.to_string(),
7024            node_type: FormNodeType::Subform,
7025            box_model: BoxModel {
7026                max_width: f64::MAX,
7027                max_height: f64::MAX,
7028                ..Default::default()
7029            },
7030            layout: LayoutStrategy::Row,
7031            children: cells,
7032            occur: Occur::once(),
7033            font: FontMetrics::default(),
7034            calculate: None,
7035            validate: None,
7036            column_widths: vec![],
7037            col_span: 1,
7038        })
7039    }
7040
7041    fn make_table(
7042        tree: &mut FormTree,
7043        name: &str,
7044        column_widths: Vec<f64>,
7045        rows: Vec<FormNodeId>,
7046    ) -> FormNodeId {
7047        tree.add_node(FormNode {
7048            name: name.to_string(),
7049            node_type: FormNodeType::Subform,
7050            box_model: BoxModel {
7051                max_width: f64::MAX,
7052                max_height: f64::MAX,
7053                ..Default::default()
7054            },
7055            layout: LayoutStrategy::Table,
7056            children: rows,
7057            occur: Occur::once(),
7058            font: FontMetrics::default(),
7059            calculate: None,
7060            validate: None,
7061            column_widths,
7062            col_span: 1,
7063        })
7064    }
7065
7066    #[test]
7067    fn table_basic_fixed_columns() {
7068        let mut tree = FormTree::new();
7069
7070        // 3 columns: 100, 150, 200
7071        let c1 = make_cell(&mut tree, "A1", 100.0, 30.0, 1);
7072        let c2 = make_cell(&mut tree, "A2", 150.0, 30.0, 1);
7073        let c3 = make_cell(&mut tree, "A3", 200.0, 30.0, 1);
7074        let r1 = make_row(&mut tree, "Row1", vec![c1, c2, c3]);
7075
7076        let c4 = make_cell(&mut tree, "B1", 100.0, 25.0, 1);
7077        let c5 = make_cell(&mut tree, "B2", 150.0, 25.0, 1);
7078        let c6 = make_cell(&mut tree, "B3", 200.0, 25.0, 1);
7079        let r2 = make_row(&mut tree, "Row2", vec![c4, c5, c6]);
7080
7081        let table = make_table(&mut tree, "Table", vec![100.0, 150.0, 200.0], vec![r1, r2]);
7082
7083        let page_area = make_subform(
7084            &mut tree,
7085            "Page",
7086            LayoutStrategy::TopToBottom,
7087            Some(612.0),
7088            Some(792.0),
7089            vec![table],
7090        );
7091
7092        let engine = LayoutEngine::new(&tree);
7093        let layout = engine.layout(page_area).unwrap();
7094
7095        assert_eq!(layout.pages.len(), 1);
7096        let page = &layout.pages[0];
7097        // Table node should be on the page
7098        assert_eq!(page.nodes.len(), 1);
7099        let table_node = &page.nodes[0];
7100        assert_eq!(table_node.name, "Table");
7101
7102        // 2 rows
7103        assert_eq!(table_node.children.len(), 2);
7104        let row1 = &table_node.children[0];
7105        let row2 = &table_node.children[1];
7106
7107        // Row 1: 3 cells at x=0, 100, 250
7108        assert_eq!(row1.children.len(), 3);
7109        assert_eq!(row1.children[0].rect.x, 0.0);
7110        assert_eq!(row1.children[0].rect.width, 100.0);
7111        assert_eq!(row1.children[1].rect.x, 100.0);
7112        assert_eq!(row1.children[1].rect.width, 150.0);
7113        assert_eq!(row1.children[2].rect.x, 250.0);
7114        assert_eq!(row1.children[2].rect.width, 200.0);
7115
7116        // Row 2 stacked below row 1
7117        assert_eq!(row2.rect.y, 30.0); // row 1 height = 30
7118        assert_eq!(row2.children[0].rect.x, 0.0);
7119    }
7120
7121    #[test]
7122    fn table_auto_columns() {
7123        let mut tree = FormTree::new();
7124
7125        // Auto columns: -1 means auto-size
7126        let c1 = make_cell(&mut tree, "A", 80.0, 20.0, 1);
7127        let c2 = make_cell(&mut tree, "B", 120.0, 20.0, 1);
7128        let r1 = make_row(&mut tree, "Row1", vec![c1, c2]);
7129
7130        let c3 = make_cell(&mut tree, "C", 60.0, 20.0, 1);
7131        let c4 = make_cell(&mut tree, "D", 150.0, 20.0, 1);
7132        let r2 = make_row(&mut tree, "Row2", vec![c3, c4]);
7133
7134        // Auto-size: widest in col 0 = 80, col 1 = 150
7135        let table = make_table(&mut tree, "Table", vec![-1.0, -1.0], vec![r1, r2]);
7136
7137        let page = make_subform(
7138            &mut tree,
7139            "Page",
7140            LayoutStrategy::TopToBottom,
7141            Some(612.0),
7142            Some(792.0),
7143            vec![table],
7144        );
7145
7146        let engine = LayoutEngine::new(&tree);
7147        let layout = engine.layout(page).unwrap();
7148
7149        let table_node = &layout.pages[0].nodes[0];
7150        let row1 = &table_node.children[0];
7151
7152        // Column 0 auto-sized to 80 (widest), Column 1 auto-sized to 150
7153        assert_eq!(row1.children[0].rect.width, 80.0);
7154        assert_eq!(row1.children[1].rect.width, 150.0);
7155        assert_eq!(row1.children[1].rect.x, 80.0);
7156    }
7157
7158    #[test]
7159    fn table_col_span() {
7160        let mut tree = FormTree::new();
7161
7162        // 3 fixed columns: 100, 100, 100
7163        let c1 = make_cell(&mut tree, "Span2", 200.0, 20.0, 2); // spans 2 columns
7164        let c2 = make_cell(&mut tree, "Single", 100.0, 20.0, 1);
7165        let r1 = make_row(&mut tree, "Row1", vec![c1, c2]);
7166
7167        let table = make_table(&mut tree, "Table", vec![100.0, 100.0, 100.0], vec![r1]);
7168
7169        let page = make_subform(
7170            &mut tree,
7171            "Page",
7172            LayoutStrategy::TopToBottom,
7173            Some(612.0),
7174            Some(792.0),
7175            vec![table],
7176        );
7177
7178        let engine = LayoutEngine::new(&tree);
7179        let layout = engine.layout(page).unwrap();
7180
7181        let row = &layout.pages[0].nodes[0].children[0];
7182        // First cell spans 2 columns: width = 100 + 100 = 200
7183        assert_eq!(row.children[0].rect.width, 200.0);
7184        assert_eq!(row.children[0].rect.x, 0.0);
7185        // Second cell at x=200, width=100
7186        assert_eq!(row.children[1].rect.x, 200.0);
7187        assert_eq!(row.children[1].rect.width, 100.0);
7188    }
7189
7190    #[test]
7191    fn table_col_span_rest() {
7192        let mut tree = FormTree::new();
7193
7194        // 3 fixed columns: 100, 100, 100
7195        let c1 = make_cell(&mut tree, "First", 100.0, 20.0, 1);
7196        let c2 = make_cell(&mut tree, "Rest", 200.0, 20.0, -1); // span remaining
7197        let r1 = make_row(&mut tree, "Row1", vec![c1, c2]);
7198
7199        let table = make_table(&mut tree, "Table", vec![100.0, 100.0, 100.0], vec![r1]);
7200
7201        let page = make_subform(
7202            &mut tree,
7203            "Page",
7204            LayoutStrategy::TopToBottom,
7205            Some(612.0),
7206            Some(792.0),
7207            vec![table],
7208        );
7209
7210        let engine = LayoutEngine::new(&tree);
7211        let layout = engine.layout(page).unwrap();
7212
7213        let row = &layout.pages[0].nodes[0].children[0];
7214        // First cell: width=100 at x=0
7215        assert_eq!(row.children[0].rect.width, 100.0);
7216        // Second cell: spans remaining = 100 + 100 = 200 at x=100
7217        assert_eq!(row.children[1].rect.x, 100.0);
7218        assert_eq!(row.children[1].rect.width, 200.0);
7219    }
7220
7221    #[test]
7222    fn table_row_height_equalization() {
7223        let mut tree = FormTree::new();
7224
7225        // Cells with different heights: 30, 50, 20
7226        let c1 = make_cell(&mut tree, "Short", 100.0, 30.0, 1);
7227        let c2 = make_cell(&mut tree, "Tall", 100.0, 50.0, 1);
7228        let c3 = make_cell(&mut tree, "Tiny", 100.0, 20.0, 1);
7229        let r1 = make_row(&mut tree, "Row1", vec![c1, c2, c3]);
7230
7231        let table = make_table(&mut tree, "Table", vec![100.0, 100.0, 100.0], vec![r1]);
7232
7233        let page = make_subform(
7234            &mut tree,
7235            "Page",
7236            LayoutStrategy::TopToBottom,
7237            Some(612.0),
7238            Some(792.0),
7239            vec![table],
7240        );
7241
7242        let engine = LayoutEngine::new(&tree);
7243        let layout = engine.layout(page).unwrap();
7244
7245        let row = &layout.pages[0].nodes[0].children[0];
7246        // All cells should have height = 50 (tallest cell)
7247        assert_eq!(row.children[0].rect.height, 50.0);
7248        assert_eq!(row.children[1].rect.height, 50.0);
7249        assert_eq!(row.children[2].rect.height, 50.0);
7250        // Row itself should be 50
7251        assert_eq!(row.rect.height, 50.0);
7252    }
7253
7254    #[test]
7255    fn table_growable_height() {
7256        let mut tree = FormTree::new();
7257
7258        let c1 = make_cell(&mut tree, "A", 100.0, 30.0, 1);
7259        let r1 = make_row(&mut tree, "Row1", vec![c1]);
7260
7261        let c2 = make_cell(&mut tree, "B", 100.0, 40.0, 1);
7262        let r2 = make_row(&mut tree, "Row2", vec![c2]);
7263
7264        // Table with no explicit height (growable)
7265        let table = make_table(&mut tree, "Table", vec![100.0], vec![r1, r2]);
7266
7267        let engine = LayoutEngine::new(&tree);
7268        let extent = engine.compute_extent(table);
7269
7270        // Table height = sum of row heights = 30 + 40 = 70
7271        assert_eq!(extent.height, 70.0);
7272        // Table width = column width = 100
7273        assert_eq!(extent.width, 100.0);
7274    }
7275
7276    #[test]
7277    fn table_empty() {
7278        let mut tree = FormTree::new();
7279        let table = make_table(&mut tree, "EmptyTable", vec![100.0, 200.0], vec![]);
7280
7281        let page = make_subform(
7282            &mut tree,
7283            "Page",
7284            LayoutStrategy::TopToBottom,
7285            Some(612.0),
7286            Some(792.0),
7287            vec![table],
7288        );
7289
7290        let engine = LayoutEngine::new(&tree);
7291        let layout = engine.layout(page).unwrap();
7292
7293        // Table exists but has no row children
7294        let table_node = &layout.pages[0].nodes[0];
7295        assert_eq!(table_node.children.len(), 0);
7296    }
7297
7298    #[test]
7299    fn table_splits_across_pages() {
7300        let mut tree = FormTree::new();
7301
7302        // Rows of 100pt height
7303        let mut rows = Vec::new();
7304        for i in 0..10 {
7305            let cell = make_cell(&mut tree, &format!("C{}", i), 200.0, 100.0, 1);
7306            let row = make_row(&mut tree, &format!("Row{}", i), vec![cell]);
7307            rows.push(row);
7308        }
7309
7310        // Table with 10 rows = 1000pt total height
7311        let table = make_table(&mut tree, "Table", vec![200.0], rows);
7312
7313        // Page area of 400pt height. Should fit 4 rows per page.
7314        let page_area = tree.add_node(FormNode {
7315            name: "PageArea".to_string(),
7316            node_type: FormNodeType::PageArea {
7317                content_areas: vec![ContentArea {
7318                    name: "Body".to_string(),
7319                    x: 0.0,
7320                    y: 0.0,
7321                    width: 400.0,
7322                    height: 400.0,
7323                    leader: None,
7324                    trailer: None,
7325                }],
7326            },
7327            box_model: BoxModel {
7328                width: Some(400.0),
7329                height: Some(400.0),
7330                max_width: f64::MAX,
7331                max_height: f64::MAX,
7332                ..Default::default()
7333            },
7334            layout: LayoutStrategy::Positioned,
7335            children: vec![],
7336            occur: Occur::once(),
7337            font: FontMetrics::default(),
7338            calculate: None,
7339            validate: None,
7340            column_widths: vec![],
7341            col_span: 1,
7342        });
7343
7344        let root = tree.add_node(FormNode {
7345            name: "Root".to_string(),
7346            node_type: FormNodeType::Root,
7347            box_model: BoxModel {
7348                width: Some(400.0),
7349                height: Some(400.0),
7350                max_width: f64::MAX,
7351                max_height: f64::MAX,
7352                ..Default::default()
7353            },
7354            layout: LayoutStrategy::TopToBottom,
7355            children: vec![page_area, table],
7356            occur: Occur::once(),
7357            font: FontMetrics::default(),
7358            calculate: None,
7359            validate: None,
7360            column_widths: vec![],
7361            col_span: 1,
7362        });
7363
7364        let engine = LayoutEngine::new(&tree);
7365        let result = engine.layout(root).unwrap();
7366
7367        // Should have 3 pages (4 rows, 4 rows, 2 rows)
7368        assert_eq!(result.pages.len(), 3);
7369    }
7370
7371    #[test]
7372    fn resolve_display_value_maps_save_to_display() {
7373        let mut meta = FormNodeMeta::default();
7374        meta.field_kind = FieldKind::Dropdown;
7375        meta.display_items = vec![
7376            "United States".to_string(),
7377            "United Kingdom".to_string(),
7378            "Canada".to_string(),
7379        ];
7380        meta.save_items = vec!["US".to_string(), "UK".to_string(), "CA".to_string()];
7381
7382        // Save value "UK" should resolve to "United Kingdom"
7383        assert_eq!(resolve_display_value("UK", &meta), "United Kingdom");
7384        // Save value "CA" should resolve to "Canada"
7385        assert_eq!(resolve_display_value("CA", &meta), "Canada");
7386        // Unknown value stays as-is
7387        assert_eq!(resolve_display_value("DE", &meta), "DE");
7388        // Empty value stays empty
7389        assert_eq!(resolve_display_value("", &meta), "");
7390    }
7391
7392    #[test]
7393    fn resolve_display_value_no_save_items_passthrough() {
7394        let mut meta = FormNodeMeta::default();
7395        meta.field_kind = FieldKind::Dropdown;
7396        meta.display_items = vec!["Red".to_string(), "Green".to_string()];
7397        // No save_items — value passes through unchanged
7398        assert_eq!(resolve_display_value("Red", &meta), "Red");
7399    }
7400
7401    #[test]
7402    fn resolve_display_value_non_dropdown_passthrough() {
7403        let mut meta = FormNodeMeta::default();
7404        meta.field_kind = FieldKind::Text;
7405        meta.save_items = vec!["US".to_string()];
7406        meta.display_items = vec!["United States".to_string()];
7407        // Non-dropdown field: no resolution
7408        assert_eq!(resolve_display_value("US", &meta), "US");
7409    }
7410
7411    #[test]
7412    fn resolve_display_value_numeric_edit_strips_trailing_zeros() {
7413        let mut meta = FormNodeMeta::default();
7414        meta.field_kind = FieldKind::NumericEdit;
7415
7416        assert_eq!(resolve_display_value("1.00000000", &meta), "1");
7417        assert_eq!(resolve_display_value("3.50", &meta), "3.5");
7418        assert_eq!(resolve_display_value("100.00", &meta), "100");
7419        assert_eq!(resolve_display_value("0.12345", &meta), "0.12345");
7420        assert_eq!(resolve_display_value("42", &meta), "42");
7421        // Non-numeric value passes through
7422        assert_eq!(resolve_display_value("abc", &meta), "abc");
7423        assert_eq!(resolve_display_value("", &meta), "");
7424    }
7425
7426    #[test]
7427    fn resolve_display_value_date_time_picker_uses_iso_date_prefix() {
7428        let mut meta = FormNodeMeta::default();
7429        meta.field_kind = FieldKind::DateTimePicker;
7430
7431        assert_eq!(resolve_display_value("2026-04-12", &meta), "2026-04-12");
7432        assert_eq!(
7433            resolve_display_value("2026-04-12T13:45:00Z", &meta),
7434            "2026-04-12"
7435        );
7436        assert_eq!(
7437            resolve_display_value("2026-04-12T13:45:00+02:00", &meta),
7438            "2026-04-12"
7439        );
7440        // Non-ISO values pass through unchanged until picture clauses exist.
7441        assert_eq!(resolve_display_value("12/04/2026", &meta), "12/04/2026");
7442        assert_eq!(resolve_display_value("abc", &meta), "abc");
7443        assert_eq!(resolve_display_value("", &meta), "");
7444    }
7445
7446    // -----------------------------------------------------------------------
7447    // XFA-F8-02 (#1118): estimated_heap_bytes
7448    // -----------------------------------------------------------------------
7449
7450    #[test]
7451    fn estimated_heap_bytes_is_positive_for_non_empty_layout() {
7452        let mut tree = FormTree::new();
7453        let f1 = make_field(&mut tree, "Field1", 100.0, 20.0);
7454        let f2 = make_field(&mut tree, "Field2", 100.0, 20.0);
7455        let root = make_subform(
7456            &mut tree,
7457            "Root",
7458            LayoutStrategy::TopToBottom,
7459            Some(200.0),
7460            Some(200.0),
7461            vec![f1, f2],
7462        );
7463
7464        let engine = LayoutEngine::new(&tree);
7465        let layout = engine.layout(root).unwrap();
7466
7467        // A layout with two fields must report non-zero heap usage.
7468        assert!(!layout.pages.is_empty(), "expected at least one page");
7469        let bytes = layout.estimated_heap_bytes();
7470        assert!(
7471            bytes > 0,
7472            "estimated_heap_bytes should be > 0 for non-empty layout"
7473        );
7474    }
7475}
7476
7477#[cfg(test)]
7478mod halign_tests {
7479    use super::*;
7480    use crate::form::{FormNode, FormNodeType, FormTree, Occur};
7481    use crate::text::FontMetrics;
7482    use crate::types::{BoxModel, LayoutStrategy, TextAlign};
7483
7484    fn make_field(tree: &mut FormTree, name: &str, w: f64, h: f64) -> FormNodeId {
7485        tree.add_node(FormNode {
7486            name: name.to_string(),
7487            node_type: FormNodeType::Field {
7488                value: name.to_string(),
7489            },
7490            box_model: BoxModel {
7491                width: Some(w),
7492                height: Some(h),
7493                max_width: f64::MAX,
7494                max_height: f64::MAX,
7495                ..Default::default()
7496            },
7497            layout: LayoutStrategy::Positioned,
7498            children: vec![],
7499            occur: Occur::once(),
7500            font: FontMetrics::default(),
7501            calculate: None,
7502            validate: None,
7503            column_widths: vec![],
7504            col_span: 1,
7505        })
7506    }
7507
7508    fn make_subform(
7509        tree: &mut FormTree,
7510        name: &str,
7511        strategy: LayoutStrategy,
7512        w: Option<f64>,
7513        h: Option<f64>,
7514        children: Vec<FormNodeId>,
7515    ) -> FormNodeId {
7516        tree.add_node(FormNode {
7517            name: name.to_string(),
7518            node_type: FormNodeType::Subform,
7519            box_model: BoxModel {
7520                width: w,
7521                height: h,
7522                max_width: f64::MAX,
7523                max_height: f64::MAX,
7524                ..Default::default()
7525            },
7526            layout: strategy,
7527            children,
7528            occur: Occur::once(),
7529            font: FontMetrics::default(),
7530            calculate: None,
7531            validate: None,
7532            column_widths: vec![],
7533            col_span: 1,
7534        })
7535    }
7536
7537    /// XFA Spec 3.3 §8.3 Example 8.13 (p284):
7538    /// TB parent w=10cm, child w=8cm hAlign="right" → child at x=2cm.
7539    #[test]
7540    fn tb_halign_right_offsets_child() {
7541        let mut tree = FormTree::new();
7542        let child = make_field(&mut tree, "A", 200.0, 30.0);
7543        tree.meta_mut(child).style.h_align = Some(TextAlign::Right);
7544
7545        let parent = make_subform(
7546            &mut tree,
7547            "Page",
7548            LayoutStrategy::TopToBottom,
7549            Some(500.0),
7550            Some(500.0),
7551            vec![child],
7552        );
7553
7554        let engine = LayoutEngine::new(&tree);
7555        let layout = engine.layout(parent).unwrap();
7556
7557        let child_node = &layout.pages[0].nodes[0];
7558        // child_w=200, parent_w=500 → x = 500 - 200 = 300
7559        assert_eq!(child_node.rect.x, 300.0);
7560    }
7561
7562    /// hAlign="center" centers child within TB parent.
7563    #[test]
7564    fn tb_halign_center_centers_child() {
7565        let mut tree = FormTree::new();
7566        let child = make_field(&mut tree, "A", 200.0, 30.0);
7567        tree.meta_mut(child).style.h_align = Some(TextAlign::Center);
7568
7569        let parent = make_subform(
7570            &mut tree,
7571            "Page",
7572            LayoutStrategy::TopToBottom,
7573            Some(500.0),
7574            Some(500.0),
7575            vec![child],
7576        );
7577
7578        let engine = LayoutEngine::new(&tree);
7579        let layout = engine.layout(parent).unwrap();
7580
7581        let child_node = &layout.pages[0].nodes[0];
7582        // (500 - 200) / 2 = 150
7583        assert_eq!(child_node.rect.x, 150.0);
7584    }
7585
7586    /// Default hAlign (left) keeps x=0 in TB layout.
7587    #[test]
7588    fn tb_halign_default_left() {
7589        let mut tree = FormTree::new();
7590        let child = make_field(&mut tree, "A", 200.0, 30.0);
7591        // No h_align set — defaults to left
7592
7593        let parent = make_subform(
7594            &mut tree,
7595            "Page",
7596            LayoutStrategy::TopToBottom,
7597            Some(500.0),
7598            Some(500.0),
7599            vec![child],
7600        );
7601
7602        let engine = LayoutEngine::new(&tree);
7603        let layout = engine.layout(parent).unwrap();
7604
7605        let child_node = &layout.pages[0].nodes[0];
7606        assert_eq!(child_node.rect.x, 0.0);
7607    }
7608
7609    /// XFA Spec 3.3 §8.3 Example 8.12 (p284):
7610    /// LR-TB parent w=10, three children w=2 hAlign="right" →
7611    /// row right-aligned: A at x=4, B at x=6, C at x=8.
7612    #[test]
7613    fn lr_tb_halign_right_shifts_row() {
7614        let mut tree = FormTree::new();
7615        let a = make_field(&mut tree, "A", 60.0, 20.0);
7616        let b = make_field(&mut tree, "B", 60.0, 20.0);
7617        let c = make_field(&mut tree, "C", 60.0, 20.0);
7618        tree.meta_mut(a).style.h_align = Some(TextAlign::Right);
7619        tree.meta_mut(b).style.h_align = Some(TextAlign::Right);
7620        tree.meta_mut(c).style.h_align = Some(TextAlign::Right);
7621
7622        let parent = make_subform(
7623            &mut tree,
7624            "Page",
7625            LayoutStrategy::LeftToRightTB,
7626            Some(300.0),
7627            Some(300.0),
7628            vec![a, b, c],
7629        );
7630
7631        let engine = LayoutEngine::new(&tree);
7632        let layout = engine.layout(parent).unwrap();
7633        let page = &layout.pages[0];
7634
7635        // Row width = 3 * 60 = 180, parent = 300 → offset = 120
7636        assert_eq!(page.nodes[0].rect.x, 120.0); // A
7637        assert_eq!(page.nodes[1].rect.x, 180.0); // B
7638        assert_eq!(page.nodes[2].rect.x, 240.0); // C
7639    }
7640
7641    /// RL-TB with explicit hAlign="left" overrides default right flow.
7642    #[test]
7643    fn rl_tb_halign_left_overrides_flow() {
7644        let mut tree = FormTree::new();
7645        let child = make_field(&mut tree, "A", 100.0, 30.0);
7646        tree.meta_mut(child).style.h_align = Some(TextAlign::Left);
7647
7648        let parent = make_subform(
7649            &mut tree,
7650            "Page",
7651            LayoutStrategy::RightToLeftTB,
7652            Some(500.0),
7653            Some(500.0),
7654            vec![child],
7655        );
7656
7657        let engine = LayoutEngine::new(&tree);
7658        let layout = engine.layout(parent).unwrap();
7659
7660        let child_node = &layout.pages[0].nodes[0];
7661        // hAlign="left" in rl-tb → x=0
7662        assert_eq!(child_node.rect.x, 0.0);
7663    }
7664
7665    // -------------------------------------------------------------------
7666    // anchorType tests (XFA 3.3 §2.6 + App A p1510)
7667    // -------------------------------------------------------------------
7668
7669    fn make_positioned_field(
7670        tree: &mut FormTree,
7671        name: &str,
7672        x: f64,
7673        y: f64,
7674        w: f64,
7675        h: f64,
7676    ) -> FormNodeId {
7677        tree.add_node(FormNode {
7678            name: name.to_string(),
7679            node_type: FormNodeType::Field {
7680                value: name.to_string(),
7681            },
7682            box_model: BoxModel {
7683                width: Some(w),
7684                height: Some(h),
7685                x,
7686                y,
7687                max_width: f64::MAX,
7688                max_height: f64::MAX,
7689                ..Default::default()
7690            },
7691            layout: LayoutStrategy::Positioned,
7692            children: vec![],
7693            occur: Occur::once(),
7694            font: FontMetrics::default(),
7695            calculate: None,
7696            validate: None,
7697            column_widths: vec![],
7698            col_span: 1,
7699        })
7700    }
7701
7702    fn layout_with_anchor(anchor: crate::form::AnchorType, x: f64, y: f64, w: f64, h: f64) -> Rect {
7703        let mut tree = FormTree::new();
7704        let field = make_positioned_field(&mut tree, "F", x, y, w, h);
7705        tree.meta_mut(field).anchor_type = anchor;
7706        let root = make_subform(
7707            &mut tree,
7708            "Root",
7709            LayoutStrategy::Positioned,
7710            Some(612.0),
7711            Some(792.0),
7712            vec![field],
7713        );
7714        let engine = LayoutEngine::new(&tree);
7715        let result = engine.layout(root).unwrap();
7716        result.pages[0].nodes[0].rect
7717    }
7718
7719    #[test]
7720    fn anchor_top_left_no_adjustment() {
7721        use crate::form::AnchorType;
7722        let r = layout_with_anchor(AnchorType::TopLeft, 100.0, 200.0, 80.0, 40.0);
7723        assert_eq!(r.x, 100.0);
7724        assert_eq!(r.y, 200.0);
7725        assert_eq!(r.width, 80.0);
7726        assert_eq!(r.height, 40.0);
7727    }
7728    #[test]
7729    fn anchor_top_center() {
7730        use crate::form::AnchorType;
7731        let r = layout_with_anchor(AnchorType::TopCenter, 100.0, 200.0, 80.0, 40.0);
7732        assert_eq!(r.x, 60.0);
7733        assert_eq!(r.y, 200.0);
7734    }
7735    #[test]
7736    fn anchor_top_right() {
7737        use crate::form::AnchorType;
7738        let r = layout_with_anchor(AnchorType::TopRight, 100.0, 200.0, 80.0, 40.0);
7739        assert_eq!(r.x, 20.0);
7740        assert_eq!(r.y, 200.0);
7741    }
7742    #[test]
7743    fn anchor_middle_left() {
7744        use crate::form::AnchorType;
7745        let r = layout_with_anchor(AnchorType::MiddleLeft, 100.0, 200.0, 80.0, 40.0);
7746        assert_eq!(r.x, 100.0);
7747        assert_eq!(r.y, 180.0);
7748    }
7749    #[test]
7750    fn anchor_middle_center() {
7751        use crate::form::AnchorType;
7752        let r = layout_with_anchor(AnchorType::MiddleCenter, 100.0, 200.0, 80.0, 40.0);
7753        assert_eq!(r.x, 60.0);
7754        assert_eq!(r.y, 180.0);
7755    }
7756    #[test]
7757    fn anchor_middle_right() {
7758        use crate::form::AnchorType;
7759        let r = layout_with_anchor(AnchorType::MiddleRight, 100.0, 200.0, 80.0, 40.0);
7760        assert_eq!(r.x, 20.0);
7761        assert_eq!(r.y, 180.0);
7762    }
7763    #[test]
7764    fn anchor_bottom_left() {
7765        use crate::form::AnchorType;
7766        let r = layout_with_anchor(AnchorType::BottomLeft, 100.0, 200.0, 80.0, 40.0);
7767        assert_eq!(r.x, 100.0);
7768        assert_eq!(r.y, 160.0);
7769    }
7770    #[test]
7771    fn anchor_bottom_center() {
7772        use crate::form::AnchorType;
7773        let r = layout_with_anchor(AnchorType::BottomCenter, 100.0, 200.0, 80.0, 40.0);
7774        assert_eq!(r.x, 60.0);
7775        assert_eq!(r.y, 160.0);
7776    }
7777    #[test]
7778    fn anchor_bottom_right() {
7779        use crate::form::AnchorType;
7780        let r = layout_with_anchor(AnchorType::BottomRight, 100.0, 200.0, 80.0, 40.0);
7781        assert_eq!(r.x, 20.0);
7782        assert_eq!(r.y, 160.0);
7783    }
7784}
7785
7786// ─────────────────────────────────────────────────────────────────────────────
7787// #1102  XFA-F4-05: area, exclGroup, subformSet container nodes
7788// XFA 3.3 Appendix B — Layout Objects
7789// ─────────────────────────────────────────────────────────────────────────────
7790#[cfg(test)]
7791mod container_node_tests {
7792    use super::*;
7793    use crate::form::{FormNode, FormNodeType, FormTree, Occur};
7794    use crate::text::FontMetrics;
7795    use crate::types::{BoxModel, LayoutStrategy};
7796
7797    fn make_field(tree: &mut FormTree, name: &str, x: f64, y: f64, w: f64, h: f64) -> FormNodeId {
7798        tree.add_node(FormNode {
7799            name: name.to_string(),
7800            node_type: FormNodeType::Field {
7801                value: name.to_string(),
7802            },
7803            box_model: BoxModel {
7804                width: Some(w),
7805                height: Some(h),
7806                x,
7807                y,
7808                max_width: f64::MAX,
7809                max_height: f64::MAX,
7810                ..Default::default()
7811            },
7812            layout: LayoutStrategy::Positioned,
7813            children: vec![],
7814            occur: Occur::once(),
7815            font: FontMetrics::default(),
7816            calculate: None,
7817            validate: None,
7818            column_widths: vec![],
7819            col_span: 1,
7820        })
7821    }
7822
7823    fn make_container(
7824        tree: &mut FormTree,
7825        name: &str,
7826        node_type: FormNodeType,
7827        strategy: LayoutStrategy,
7828        w: f64,
7829        h: f64,
7830        children: Vec<FormNodeId>,
7831    ) -> FormNodeId {
7832        tree.add_node(FormNode {
7833            name: name.to_string(),
7834            node_type,
7835            box_model: BoxModel {
7836                width: Some(w),
7837                height: Some(h),
7838                max_width: f64::MAX,
7839                max_height: f64::MAX,
7840                ..Default::default()
7841            },
7842            layout: strategy,
7843            children,
7844            occur: Occur::once(),
7845            font: FontMetrics::default(),
7846            calculate: None,
7847            validate: None,
7848            column_widths: vec![],
7849            col_span: 1,
7850        })
7851    }
7852
7853    /// XFA 3.3 Appendix B — `<area>` is a positioned container.
7854    /// Children have absolute positions within the area. The area itself
7855    /// is treated exactly like a Subform with positioned layout.
7856    #[test]
7857    fn area_node_positions_children_absolutely() {
7858        let mut tree = FormTree::new();
7859        let child = make_field(&mut tree, "Child", 10.0, 20.0, 50.0, 15.0);
7860        let area = make_container(
7861            &mut tree,
7862            "MyArea",
7863            FormNodeType::Area,
7864            LayoutStrategy::Positioned,
7865            200.0,
7866            100.0,
7867            vec![child],
7868        );
7869        // Root: TB with the area as only child
7870        let root = make_container(
7871            &mut tree,
7872            "Root",
7873            FormNodeType::Subform,
7874            LayoutStrategy::TopToBottom,
7875            200.0,
7876            200.0,
7877            vec![area],
7878        );
7879
7880        let engine = LayoutEngine::new(&tree);
7881        let result = engine.layout(root).unwrap();
7882
7883        assert_eq!(result.pages.len(), 1);
7884        let area_node = &result.pages[0].nodes[0];
7885        assert_eq!(area_node.name, "MyArea");
7886        assert_eq!(area_node.children.len(), 1);
7887        // Child positioned at (10, 20) within the area
7888        let child_node = &area_node.children[0];
7889        assert_eq!(child_node.name, "Child");
7890        assert_eq!(child_node.rect.x, 10.0);
7891        assert_eq!(child_node.rect.y, 20.0);
7892    }
7893
7894    /// XFA 3.3 §7.2 — `<exclGroup>` lays out radio-button fields top-to-bottom.
7895    /// From a layout perspective, each child field is rendered normally.
7896    #[test]
7897    fn excl_group_lays_out_children_top_to_bottom() {
7898        let mut tree = FormTree::new();
7899        let opt_a = make_field(&mut tree, "OptionA", 0.0, 0.0, 100.0, 20.0);
7900        let opt_b = make_field(&mut tree, "OptionB", 0.0, 0.0, 100.0, 20.0);
7901        let excl = make_container(
7902            &mut tree,
7903            "MyGroup",
7904            FormNodeType::ExclGroup,
7905            LayoutStrategy::TopToBottom,
7906            200.0,
7907            100.0,
7908            vec![opt_a, opt_b],
7909        );
7910        let root = make_container(
7911            &mut tree,
7912            "Root",
7913            FormNodeType::Subform,
7914            LayoutStrategy::TopToBottom,
7915            200.0,
7916            200.0,
7917            vec![excl],
7918        );
7919
7920        let engine = LayoutEngine::new(&tree);
7921        let result = engine.layout(root).unwrap();
7922
7923        assert_eq!(result.pages.len(), 1);
7924        let group_node = &result.pages[0].nodes[0];
7925        assert_eq!(group_node.name, "MyGroup");
7926        assert_eq!(group_node.children.len(), 2);
7927        // OptionA at y=0, OptionB stacked below at y=20
7928        assert_eq!(group_node.children[0].rect.y, 0.0);
7929        assert_eq!(group_node.children[1].rect.y, 20.0);
7930    }
7931
7932    /// XFA 3.3 §7.1 — `<subformSet>` is transparent: its children appear as
7933    /// direct siblings of the containing subform's children.
7934    #[test]
7935    fn subform_set_is_transparent_container() {
7936        let mut tree = FormTree::new();
7937        let field_a = make_field(&mut tree, "A", 0.0, 0.0, 100.0, 20.0);
7938        let field_b = make_field(&mut tree, "B", 0.0, 0.0, 100.0, 20.0);
7939        // SubformSet wrapping two fields — should be transparent
7940        let set = make_container(
7941            &mut tree,
7942            "MySet",
7943            FormNodeType::SubformSet,
7944            LayoutStrategy::TopToBottom,
7945            200.0,
7946            100.0,
7947            vec![field_a, field_b],
7948        );
7949        let root = make_container(
7950            &mut tree,
7951            "Root",
7952            FormNodeType::Subform,
7953            LayoutStrategy::TopToBottom,
7954            200.0,
7955            200.0,
7956            vec![set],
7957        );
7958
7959        let engine = LayoutEngine::new(&tree);
7960        let result = engine.layout(root).unwrap();
7961
7962        assert_eq!(result.pages.len(), 1);
7963        // SubformSet itself may appear as a container node; its children should be present
7964        let page = &result.pages[0];
7965        fn count_named<'a>(nodes: &'a [LayoutNode], name: &str) -> usize {
7966            nodes
7967                .iter()
7968                .map(|n| usize::from(n.name == name) + count_named(&n.children, name))
7969                .sum()
7970        }
7971        assert!(
7972            count_named(&page.nodes, "A") >= 1,
7973            "Field A should appear in layout"
7974        );
7975        assert!(
7976            count_named(&page.nodes, "B") >= 1,
7977            "Field B should appear in layout"
7978        );
7979    }
7980}
7981
7982// ─────────────────────────────────────────────────────────────────────────────
7983// #1103  XFA-F4-06: Keep chains and orderedOccurrence
7984// XFA 3.3 §8.9 Adhesion (keep) + §8.8 orderedOccurrence
7985// ─────────────────────────────────────────────────────────────────────────────
7986//
7987// orderedOccurrence: Repeating subform instances maintain their sequential
7988// order (first occurrence first, last last).  This is the default behaviour
7989// when `expand_occur` emits IDs in source order.  If the data-driven
7990// count matches, orderedOccurrence is satisfied without extra work.
7991// See `expand_occur` and the pagination loop in `layout_content_on_page`.
7992//
7993// Keep chains are tested below.
7994#[cfg(test)]
7995mod keep_chain_tests {
7996    use super::*;
7997    use crate::form::{FormNode, FormNodeType, FormTree, Occur};
7998    use crate::text::FontMetrics;
7999    use crate::types::{BoxModel, LayoutStrategy};
8000
8001    fn make_field(tree: &mut FormTree, name: &str, w: f64, h: f64) -> FormNodeId {
8002        tree.add_node(FormNode {
8003            name: name.to_string(),
8004            node_type: FormNodeType::Field {
8005                value: name.to_string(),
8006            },
8007            box_model: BoxModel {
8008                width: Some(w),
8009                height: Some(h),
8010                max_width: f64::MAX,
8011                max_height: f64::MAX,
8012                ..Default::default()
8013            },
8014            layout: LayoutStrategy::Positioned,
8015            children: vec![],
8016            occur: Occur::once(),
8017            font: FontMetrics::default(),
8018            calculate: None,
8019            validate: None,
8020            column_widths: vec![],
8021            col_span: 1,
8022        })
8023    }
8024
8025    /// XFA 3.3 §8.9 Adhesion — keep.next keeps a node on the same page as
8026    /// the following sibling.  When the heading + body together would not fit
8027    /// on the current page, both are pushed to the next page.
8028    ///
8029    /// Setup:
8030    ///   page height = 100pt
8031    ///   filler = 70pt (placed first, leaves 30pt)
8032    ///   heading = 40pt  with keep_next_content_area = true
8033    ///   body    = 40pt  (the kept-with node)
8034    ///
8035    /// Without keep: heading(40pt) > 30pt remaining → goes to page 2, body on page 2.
8036    /// With keep (chain height = 80pt > 30pt): both pushed to page 2 together.
8037    #[test]
8038    fn keep_next_pushes_heading_and_body_to_same_page() {
8039        let mut tree = FormTree::new();
8040
8041        let filler = make_field(&mut tree, "Filler", 200.0, 70.0);
8042        let heading = make_field(&mut tree, "Heading", 200.0, 40.0);
8043        let body = make_field(&mut tree, "Body", 200.0, 40.0);
8044
8045        // Mark heading as keep-with-next
8046        tree.meta_mut(heading).keep_next_content_area = true;
8047
8048        let root = tree.add_node(FormNode {
8049            name: "Root".to_string(),
8050            node_type: FormNodeType::Root,
8051            box_model: BoxModel {
8052                width: Some(200.0),
8053                height: Some(100.0),
8054                max_width: f64::MAX,
8055                max_height: f64::MAX,
8056                ..Default::default()
8057            },
8058            layout: LayoutStrategy::TopToBottom,
8059            children: vec![filler, heading, body],
8060            occur: Occur::once(),
8061            font: FontMetrics::default(),
8062            calculate: None,
8063            validate: None,
8064            column_widths: vec![],
8065            col_span: 1,
8066        });
8067
8068        let engine = LayoutEngine::new(&tree);
8069        let result = engine.layout(root).unwrap();
8070
8071        // Should produce 2 pages
8072        assert_eq!(
8073            result.pages.len(),
8074            2,
8075            "expected filler on page 1, heading+body on page 2"
8076        );
8077
8078        // Page 1: only the filler
8079        let p1_names: Vec<&str> = result.pages[0]
8080            .nodes
8081            .iter()
8082            .map(|n| n.name.as_str())
8083            .collect();
8084        assert!(p1_names.contains(&"Filler"), "Filler should be on page 1");
8085        assert!(
8086            !p1_names.contains(&"Heading"),
8087            "Heading should NOT be on page 1"
8088        );
8089        assert!(!p1_names.contains(&"Body"), "Body should NOT be on page 1");
8090
8091        // Page 2: heading and body together
8092        let p2_names: Vec<&str> = result.pages[1]
8093            .nodes
8094            .iter()
8095            .map(|n| n.name.as_str())
8096            .collect();
8097        assert!(p2_names.contains(&"Heading"), "Heading should be on page 2");
8098        assert!(p2_names.contains(&"Body"), "Body should be on page 2");
8099    }
8100}