Skip to main content

xfa_layout_engine/
layout.rs

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