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