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