Skip to main content

xfa_layout_engine/
form.rs

1//! Form node types — the input to the layout engine.
2//!
3//! These represent the merged Form DOM nodes that the layout engine processes.
4//! In a full implementation, these would come from xfa-dom-resolver's merge step.
5
6use std::collections::HashMap;
7
8use crate::text::FontMetrics;
9use crate::types::{BoxModel, LayoutStrategy, TextAlign, VerticalAlign};
10
11/// A unique identifier for a form node.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub struct FormNodeId(pub usize);
14
15/// The form tree: a node-based representation of the merged template+data.
16#[derive(Debug)]
17pub struct FormTree {
18    pub nodes: Vec<FormNode>,
19    /// Per-node metadata (parallel to `nodes`).
20    pub metadata: Vec<FormNodeMeta>,
21    /// Lookup table: XFA `id` attribute -> `FormNodeId`.
22    pub node_ids: HashMap<String, FormNodeId>,
23}
24
25impl FormTree {
26    pub fn new() -> Self {
27        Self {
28            nodes: Vec::new(),
29            metadata: Vec::new(),
30            node_ids: HashMap::new(),
31        }
32    }
33
34    pub fn add_node(&mut self, node: FormNode) -> FormNodeId {
35        let id = FormNodeId(self.nodes.len());
36        self.nodes.push(node);
37        self.metadata.push(FormNodeMeta::default());
38        id
39    }
40
41    /// Add a node together with its metadata. If the meta has an `xfa_id`,
42    /// it is registered in the `node_ids` lookup table.
43    pub fn add_node_with_meta(&mut self, node: FormNode, meta: FormNodeMeta) -> FormNodeId {
44        let id = FormNodeId(self.nodes.len());
45        if let Some(ref xfa_id) = meta.xfa_id {
46            self.node_ids.insert(xfa_id.clone(), id);
47        }
48        self.nodes.push(node);
49        self.metadata.push(meta);
50        id
51    }
52
53    pub fn get(&self, id: FormNodeId) -> &FormNode {
54        &self.nodes[id.0]
55    }
56
57    pub fn get_mut(&mut self, id: FormNodeId) -> &mut FormNode {
58        &mut self.nodes[id.0]
59    }
60
61    /// Access the metadata for a node.
62    pub fn meta(&self, id: FormNodeId) -> &FormNodeMeta {
63        &self.metadata[id.0]
64    }
65
66    /// Mutably access the metadata for a node.
67    pub fn meta_mut(&mut self, id: FormNodeId) -> &mut FormNodeMeta {
68        &mut self.metadata[id.0]
69    }
70
71    /// Look up a node by its XFA `id` attribute.
72    pub fn find_by_xfa_id(&self, id: &str) -> Option<FormNodeId> {
73        self.node_ids.get(id).copied()
74    }
75}
76
77impl Default for FormTree {
78    fn default() -> Self {
79        Self::new()
80    }
81}
82
83/// A single node in the Form DOM.
84#[derive(Debug, Clone)]
85pub struct FormNode {
86    pub name: String,
87    pub node_type: FormNodeType,
88    pub box_model: BoxModel,
89    pub layout: LayoutStrategy,
90    pub children: Vec<FormNodeId>,
91    /// Occurrence rules for repeating subforms.
92    pub occur: Occur,
93    /// Font metrics for text measurement (Draw/Field nodes).
94    pub font: FontMetrics,
95    /// FormCalc calculate script (XFA S14.3.2): runs to compute the field's value.
96    pub calculate: Option<String>,
97    /// FormCalc validate script: runs to validate the field's value, returns bool.
98    pub validate: Option<String>,
99    /// Column widths for table-layout subforms (XFA columnWidths attribute).
100    /// Positive values are fixed widths in points; -1.0 means auto-size.
101    /// Empty for non-table nodes.
102    pub column_widths: Vec<f64>,
103    /// Column span for cells inside a table row (XFA colSpan attribute).
104    /// 1 = single column (default), N = span N columns, -1 = span remaining.
105    pub col_span: i32,
106}
107
108/// The scripting language used by an XFA `<script>` element.
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
110pub enum ScriptLanguage {
111    /// FormCalc is the XFA default when `contentType` is omitted.
112    #[default]
113    FormCalc,
114    /// JavaScript event handlers and calculations.
115    JavaScript,
116    /// Any other declared script language (for example VBScript).
117    Other,
118}
119
120/// Script metadata collected from `<event>` / `<calculate>` elements.
121#[derive(Debug, Clone, PartialEq, Eq, Default)]
122pub struct EventScript {
123    pub script: String,
124    pub language: ScriptLanguage,
125    pub activity: Option<String>,
126    pub event_ref: Option<String>,
127    pub run_at: Option<String>,
128}
129
130impl EventScript {
131    pub fn new(
132        script: String,
133        language: ScriptLanguage,
134        activity: Option<String>,
135        event_ref: Option<String>,
136        run_at: Option<String>,
137    ) -> Self {
138        Self {
139            script,
140            language,
141            activity,
142            event_ref,
143            run_at,
144        }
145    }
146
147    pub fn formcalc(script: impl Into<String>, activity: Option<&str>) -> Self {
148        Self::new(
149            script.into(),
150            ScriptLanguage::FormCalc,
151            activity.map(str::to_string),
152            None,
153            None,
154        )
155    }
156
157    pub fn javascript(script: impl Into<String>, activity: Option<&str>) -> Self {
158        Self::new(
159            script.into(),
160            ScriptLanguage::JavaScript,
161            activity.map(str::to_string),
162            None,
163            None,
164        )
165    }
166}
167
168/// Content for draw nodes (static graphic elements).
169///
170/// XFA Spec 3.3 §2.1 (p24) — Draw element: fixed content (boilerplate).
171/// Includes text, lines, rectangles, arcs. Images are handled separately
172/// via `FormNodeType::Image`.
173///
174/// TODO(§2.3): `circle` draw content not implemented (spec allows via arc with
175///   startAngle=0, sweepAngle=360).
176#[derive(Debug, Clone)]
177pub enum DrawContent {
178    Text(String),
179    Line {
180        x1: f64,
181        y1: f64,
182        x2: f64,
183        y2: f64,
184    },
185    Rectangle {
186        x: f64,
187        y: f64,
188        w: f64,
189        h: f64,
190        radius: f64,
191    },
192    Arc {
193        x: f64,
194        y: f64,
195        w: f64,
196        h: f64,
197        start_angle: f64,
198        sweep_angle: f64,
199    },
200}
201
202/// The type of form node.
203#[derive(Debug, Clone)]
204pub enum FormNodeType {
205    /// Root subform.
206    Root,
207    /// A page set containing page areas.
208    PageSet,
209    /// A page area (page template) with content areas.
210    PageArea { content_areas: Vec<ContentArea> },
211    /// A generic subform container.
212    Subform,
213    /// XFA `<area>` — a positioned container (XFA 3.3 Appendix B).
214    ///
215    /// Semantically identical to a `Subform` with positioned layout: children
216    /// have absolute positions within the area.  The layout engine treats this
217    /// exactly like `Subform` for layout purposes.
218    Area,
219    /// XFA `<exclGroup>` — an exclusive (radio-button) group (XFA 3.3 §7.2).
220    ///
221    /// Contains multiple radio-button `<field>` children where exactly one can
222    /// be selected.  In layout this behaves like a `Subform` with top-to-bottom
223    /// flow; each child field is rendered normally.
224    ExclGroup,
225    /// XFA `<subformSet>` — a transparent set of subforms (XFA 3.3 §7.1).
226    ///
227    /// Used for conditional instantiation.  In layout the set is transparent:
228    /// its children are processed as if they were direct children of the
229    /// containing subform (same data context).
230    SubformSet,
231    /// A form field (text field, checkbox, etc.).
232    Field { value: String },
233    /// A static draw element (text, image, line, etc.).
234    Draw(DrawContent),
235    /// A static image draw element.
236    Image { data: Vec<u8>, mime_type: String },
237}
238
239/// Occurrence rules for repeating subforms (XFA S3.3 occur element).
240///
241/// Controls how many instances of a subform are created. The layout engine
242/// expands templates based on the `initial` count, bounded by `min` and `max`.
243#[derive(Debug, Clone)]
244pub struct Occur {
245    /// Minimum number of occurrences (default 1).
246    pub min: u32,
247    /// Maximum number of occurrences (-1 = unlimited). Default 1.
248    /// Using `Option<u32>` where `None` means unlimited.
249    pub max: Option<u32>,
250    /// Initial number of occurrences (default = min).
251    pub initial: u32,
252}
253
254impl Default for Occur {
255    fn default() -> Self {
256        Self {
257            min: 1,
258            max: Some(1),
259            initial: 1,
260        }
261    }
262}
263
264impl Occur {
265    /// Occur rule that means "exactly once" (the default).
266    pub fn once() -> Self {
267        Self::default()
268    }
269
270    /// Occur rule for a repeating subform.
271    pub fn repeating(min: u32, max: Option<u32>, initial: u32) -> Self {
272        let initial = initial.max(min);
273        let initial = match max {
274            Some(m) => initial.min(m),
275            None => initial,
276        };
277        Self { min, max, initial }
278    }
279
280    /// How many instances should be created.
281    pub fn count(&self) -> u32 {
282        self.initial
283    }
284
285    /// Whether the subform can repeat (max > 1 or unlimited).
286    pub fn is_repeating(&self) -> bool {
287        match self.max {
288            Some(m) => m > 1,
289            None => true,
290        }
291    }
292}
293
294/// A content area within a page area.
295#[derive(Debug, Clone)]
296pub struct ContentArea {
297    pub name: String,
298    pub x: f64,
299    pub y: f64,
300    pub width: f64,
301    pub height: f64,
302    /// Leader (header) node placed at the top of each page's content area.
303    pub leader: Option<FormNodeId>,
304    /// Trailer (footer) node placed at the bottom of each page's content area.
305    pub trailer: Option<FormNodeId>,
306}
307
308impl Default for ContentArea {
309    fn default() -> Self {
310        Self {
311            name: String::new(),
312            x: 0.0,
313            y: 0.0,
314            width: 612.0,  // US Letter width in points
315            height: 792.0, // US Letter height in points
316            leader: None,
317            trailer: None,
318        }
319    }
320}
321
322// ---------------------------------------------------------------------------
323// Metadata, style, and kind types
324// ---------------------------------------------------------------------------
325
326/// XFA `presence` attribute values (XFA 3.3 §2.6 p67-68).
327///
328/// Controls visibility and layout space allocation:
329/// - `Visible` -- all phases: binding, automation, layout, rendering, interaction.
330/// - `Hidden` -- binding + automation only; no layout space, no rendering.
331/// - `Invisible` -- binding + automation + layout; takes up space but not visible.
332/// - `Inactive` -- binding only; completely absent from form.
333///
334/// Note: spec defines `hidden` as "effectively absent" (no space) and `invisible`
335/// as "takes space but not visible". Our `is_layout_hidden` implementation reflects this.
336#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
337pub enum Presence {
338    #[default]
339    Visible,
340    Hidden,
341    Invisible,
342    Inactive,
343}
344
345impl Presence {
346    /// True when the element should not be rendered.
347    pub fn is_not_visible(self) -> bool {
348        !matches!(self, Presence::Visible)
349    }
350
351    /// True when the element should not occupy layout space.
352    ///
353    /// XFA Spec 3.3 §2.6 (p68):
354    /// - `hidden`:   no layout space, no rendering (effectively absent)
355    /// - `invisible`: no layout space in Adobe (spec says "takes space",
356    ///   but empirical testing shows Adobe skips it)
357    /// - `inactive`:  completely absent (no binding, no space)
358    ///
359    /// `Hidden` was previously excluded from this predicate based on an
360    /// incorrect assumption that Adobe reserves space for hidden elements.
361    /// GATE #27 testing proved this wrong: hidden subforms with `<break>`
362    /// elements caused 2-23x overpagination in forms with many
363    /// `presence="hidden"` subforms (fixes #806).
364    pub fn is_layout_hidden(self) -> bool {
365        matches!(
366            self,
367            Presence::Hidden | Presence::Invisible | Presence::Inactive
368        )
369    }
370}
371
372/// Extended metadata for a form node.
373///
374/// Carries XFA attributes that the layout engine and dynamic scripting
375/// system need but that are not part of the core `FormNode` shape.
376#[derive(Debug, Clone, Default)]
377pub struct FormNodeMeta {
378    /// Optional XFA `id` attribute.
379    pub xfa_id: Option<String>,
380    /// XFA presence attribute (visible/hidden/invisible/inactive).
381    pub presence: Presence,
382    /// Whether a page break should be inserted before this node.
383    pub page_break_before: bool,
384    /// Whether a page break should be inserted after this node.
385    pub page_break_after: bool,
386    /// Target page area name/id for the break (e.g. "MP3", "Page4_ID").
387    pub break_target: Option<String>,
388    /// Whether this node targets a specific content area via
389    /// `breakBefore targetType="contentArea"`.  Such nodes should be
390    /// excluded from the primary content flow (they go into small
391    /// decorative areas like "flatten" or "eSign").
392    pub content_area_break: bool,
393    /// Overflow leader reference name.
394    pub overflow_leader: Option<String>,
395    /// Overflow trailer reference name.
396    pub overflow_trailer: Option<String>,
397    /// Keep with next content area.
398    pub keep_next_content_area: bool,
399    /// Keep with previous content area.
400    pub keep_previous_content_area: bool,
401    /// Keep intact within content area.
402    pub keep_intact_content_area: bool,
403    /// Layout-ready script (XFA S14.3).
404    pub layout_ready_script: Option<String>,
405    /// Event scripts collected from `<event>` and `<calculate>` children.
406    pub event_scripts: Vec<EventScript>,
407    /// Explicit XFA data binding ref from `<bind ref="...">`.
408    pub data_bind_ref: Option<String>,
409    /// Whether the node explicitly opts out of data binding via `<bind match="none">`.
410    pub data_bind_none: bool,
411    /// Visual style (font, colors, borders).
412    pub style: FormNodeStyle,
413    /// The kind of field (text, checkbox, radio, etc.).
414    pub field_kind: FieldKind,
415    /// The kind of group (none or exclusive choice).
416    pub group_kind: GroupKind,
417    /// Item value for fields inside an exclGroup.
418    pub item_value: Option<String>,
419    /// Check box / radio button size in points.
420    pub check_size: Option<f64>,
421    /// Display items for choice list fields (XFA 3.3 §7.7).
422    pub display_items: Vec<String>,
423    /// Save items for choice list fields (XFA 3.3 §7.7).
424    pub save_items: Vec<String>,
425    /// XFA anchorType for positioned layout (XFA 3.3 §2.6, App A p1510).
426    pub anchor_type: AnchorType,
427}
428
429/// XFA `anchorType` attribute (XFA 3.3 §2.6, Appendix A p1510).
430///
431/// Determines which point of an element is placed at the (x,y) coordinate
432/// in positioned layout.  Default is `TopLeft`.
433#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
434pub enum AnchorType {
435    #[default]
436    TopLeft,
437    TopCenter,
438    TopRight,
439    MiddleLeft,
440    MiddleCenter,
441    MiddleRight,
442    BottomLeft,
443    BottomCenter,
444    BottomRight,
445}
446
447/// The kind of group container.
448#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
449pub enum GroupKind {
450    #[default]
451    None,
452    ExclusiveChoice,
453}
454
455/// The kind of form field.
456#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
457pub enum FieldKind {
458    #[default]
459    Text,
460    Checkbox,
461    Radio,
462    Button,
463    Dropdown,
464    Signature,
465    DateTimePicker,
466    NumericEdit,
467    PasswordEdit,
468    ImageEdit,
469    Barcode,
470}
471
472/// A span of rich text with per-span style overrides.
473///
474/// XFA Spec 3.3 §4.2.7 (p155) — `<exData contentType="text/html">` stores
475/// XHTML content with inline CSS. Each span carries its own formatting
476/// (font, color, weight, etc.) that overrides the node-level defaults.
477#[derive(Debug, Clone, PartialEq)]
478pub struct RichTextSpan {
479    pub text: String,
480    pub font_size: Option<f64>,
481    pub font_family: Option<String>,
482    pub font_weight: Option<String>,
483    pub font_style: Option<String>,
484    pub text_color: Option<(u8, u8, u8)>,
485    pub underline: bool,
486    pub line_through: bool,
487}
488
489/// Visual style properties for a form node.
490#[derive(Debug, Clone, PartialEq)]
491pub struct FormNodeStyle {
492    pub font_family: Option<String>,
493    pub font_size: Option<f64>,
494    pub font_weight: Option<String>,
495    pub font_style: Option<String>,
496    pub text_color: Option<(u8, u8, u8)>,
497    pub bg_color: Option<(u8, u8, u8)>,
498    pub border_color: Option<(u8, u8, u8)>,
499    /// Per-edge border colors (top, right, bottom, left) in RGB 0-255.
500    /// When set, overrides `border_color` for individual edges.
501    pub border_colors: Option<[(u8, u8, u8); 4]>,
502    pub border_width_pt: Option<f64>,
503    /// Per-edge border widths (top, right, bottom, left) in points.
504    /// When set, overrides `border_width_pt` for individual edges.
505    pub border_widths: Option<[f64; 4]>,
506    /// Paragraph space above in points (XFA `<para spaceAbove>`).
507    pub space_above_pt: Option<f64>,
508    /// Paragraph space below in points (XFA `<para spaceBelow>`).
509    pub space_below_pt: Option<f64>,
510    /// Paragraph left margin in points (XFA `<para marginLeft>`).
511    pub margin_left_pt: Option<f64>,
512    /// Paragraph right margin in points (XFA `<para marginRight>`).
513    pub margin_right_pt: Option<f64>,
514    /// XFA Spec 3.3 §17 "para" (p803) — lineHeight: baseline-to-baseline
515    /// distance in points. When `None`, use font metrics.
516    pub line_height_pt: Option<f64>,
517    /// XFA Spec 3.3 §17 "para" (p803) — textIndent: indentation of the first
518    /// line of each paragraph in points.
519    pub text_indent_pt: Option<f64>,
520    /// Margin top inset in points (XFA `<margin topInset>`).
521    pub inset_top_pt: Option<f64>,
522    /// Margin bottom inset in points (XFA `<margin bottomInset>`).
523    pub inset_bottom_pt: Option<f64>,
524    /// Margin left inset in points (XFA `<margin leftInset>`).
525    pub inset_left_pt: Option<f64>,
526    /// Margin right inset in points (XFA `<margin rightInset>`).
527    pub inset_right_pt: Option<f64>,
528    /// Vertical text alignment (XFA `<para vAlign>`).
529    pub v_align: Option<VerticalAlign>,
530    /// Horizontal alignment for layout positioning (XFA `<para hAlign>`, §8.3).
531    pub h_align: Option<TextAlign>,
532    /// Border corner radius in points (XFA `<border><corner radius>`).
533    pub border_radius_pt: Option<f64>,
534    /// Border edge stroke style (XFA `<border><edge stroke>`).
535    pub border_style: Option<String>,
536    /// Per-edge visibility: [top, right, bottom, left]. All true when absent.
537    pub border_edges: [bool; 4],
538    /// XFA Spec 3.3 §17 (p716) — genericFamily fallback hint.
539    /// Values: serif, sansSerif, monospaced, decorative, fantasy, cursive.
540    pub generic_family: Option<String>,
541    /// Font horizontal scale factor (XFA `<font fontHorizontalScale>`).
542    /// 1.0 = 100% (default), 0.96 = 96%, etc.
543    pub font_horizontal_scale: Option<f64>,
544    /// Letter spacing in points (XFA `<font letterSpacing>`).
545    /// 0.0 = normal (default). Negative values tighten, positive widen.
546    pub letter_spacing_pt: Option<f64>,
547    /// Caption text (XFA `<caption><value><text>`).
548    pub caption_text: Option<String>,
549    /// Caption placement (left/right/top/bottom/inline).
550    pub caption_placement: Option<String>,
551    /// Caption reserve width/height in points.
552    pub caption_reserve: Option<f64>,
553    /// CheckButton mark style (XFA `<checkButton mark="...">`).
554    /// Values: "check", "circle", "cross", "diamond", "square", "star".
555    pub check_button_mark: Option<String>,
556    /// CheckButton on-value (first `<items>` entry, XFA 3.3 §17.8).
557    pub check_button_on_value: Option<String>,
558    /// CheckButton off-value (second `<items>` entry). When omitted, the
559    /// spec default is the null string.
560    pub check_button_off_value: Option<String>,
561    /// CheckButton neutral-value (third `<items>` entry, checkbox only).
562    pub check_button_neutral_value: Option<String>,
563    /// Rich text spans parsed from `<exData contentType="text/html">` XHTML.
564    pub rich_text_spans: Option<Vec<RichTextSpan>>,
565    /// Font underline (XFA `<font underline="1">`).
566    pub underline: bool,
567    /// Font line-through / strikethrough (XFA `<font lineThrough="1">`).
568    pub line_through: bool,
569    /// XFA format picture clause (e.g. `num{z,zzz.99}`).
570    pub format_pattern: Option<String>,
571}
572
573impl Default for FormNodeStyle {
574    fn default() -> Self {
575        Self {
576            font_family: None,
577            font_size: None,
578            font_weight: None,
579            font_style: None,
580            text_color: None,
581            bg_color: None,
582            border_color: None,
583            border_colors: None,
584            border_width_pt: None,
585            border_widths: None,
586            space_above_pt: None,
587            space_below_pt: None,
588            margin_left_pt: None,
589            margin_right_pt: None,
590            line_height_pt: None,
591            text_indent_pt: None,
592            inset_top_pt: None,
593            inset_bottom_pt: None,
594            inset_left_pt: None,
595            inset_right_pt: None,
596            v_align: None,
597            h_align: None,
598            border_radius_pt: None,
599            border_style: None,
600            border_edges: [true, true, true, true],
601            generic_family: None,
602            font_horizontal_scale: None,
603            letter_spacing_pt: None,
604            caption_text: None,
605            caption_placement: None,
606            caption_reserve: None,
607            check_button_mark: None,
608            check_button_on_value: None,
609            check_button_off_value: None,
610            check_button_neutral_value: None,
611            rich_text_spans: None,
612            underline: false,
613            line_through: false,
614            format_pattern: None,
615        }
616    }
617}