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
6// Copyright (c) 2026 Innovation Trigger B.V.
7//
8// PDFluent is available under two licences, at your option: the GNU AGPLv3, or
9// the PDFluent Commercial Licence. See the LICENSE file in this repository --
10// that file travels with the copy you received, which a URL does not.
11
12use std::collections::HashMap;
13
14use crate::text::FontMetrics;
15use crate::types::{BoxModel, LayoutStrategy, TextAlign, VerticalAlign};
16
17/// A unique identifier for a form node.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub struct FormNodeId(pub usize);
20
21/// The form tree: a node-based representation of the merged template+data.
22#[derive(Debug)]
23pub struct FormTree {
24    /// Nodes in the form tree.
25    pub nodes: Vec<FormNode>,
26    /// Per-node metadata (parallel to `nodes`).
27    pub metadata: Vec<FormNodeMeta>,
28    /// Lookup table: XFA `id` attribute -> `FormNodeId`.
29    pub node_ids: HashMap<String, FormNodeId>,
30    /// XFA 3.3 §5.5 `<variables>` `<script name="X">…</script>` blocks
31    /// gathered at merge time. Each entry is `(subform_scope, name, body)`.
32    /// `subform_scope` is `None` for root-level scripts (globally accessible)
33    /// and `Some(subform_name)` for scripts scoped to a named subform
34    /// (accessible as `subform.variables.scriptName`). Empty in default mode.
35    pub variables_scripts: Vec<(Option<String>, String, String)>,
36    /// XFA 3.3 §5.5.2 `<variables>` `<text name="X">value</text>` data items
37    /// gathered at merge time. Each entry is `(subform_scope, name, initial)`.
38    /// `subform_scope` follows the same convention as
39    /// [`Self::variables_scripts`]. Data items are form-level mutable string
40    /// containers — the canonical Canadian IMM template pattern is
41    /// `<variables><text name="globValidatePressed"/></variables>` referenced
42    /// from event scripts as `globValidatePressed.value = "true";`. Empty in
43    /// default mode.
44    ///
45    /// W3-D RETRY: registering these alongside variables-scripts eliminates
46    /// the post-W2-B `implicit_function` residual for the IMM5709/IMM5257/
47    /// IMM5710 family.
48    pub variables_data_items: Vec<(Option<String>, String, String)>,
49}
50
51impl FormTree {
52    /// Create a new form tree.
53    pub fn new() -> Self {
54        Self {
55            nodes: Vec::new(),
56            metadata: Vec::new(),
57            node_ids: HashMap::new(),
58            variables_scripts: Vec::new(),
59            variables_data_items: Vec::new(),
60        }
61    }
62
63    /// Add a node to the form tree.
64    pub fn add_node(&mut self, node: FormNode) -> FormNodeId {
65        let id = FormNodeId(self.nodes.len());
66        self.nodes.push(node);
67        self.metadata.push(FormNodeMeta::default());
68        id
69    }
70
71    /// Add a node together with its metadata. If the meta has an `xfa_id`,
72    /// it is registered in the `node_ids` lookup table.
73    pub fn add_node_with_meta(&mut self, node: FormNode, meta: FormNodeMeta) -> FormNodeId {
74        let id = FormNodeId(self.nodes.len());
75        if let Some(ref xfa_id) = meta.xfa_id {
76            self.node_ids.insert(xfa_id.clone(), id);
77        }
78        self.nodes.push(node);
79        self.metadata.push(meta);
80        id
81    }
82
83    /// Get a node by ID.
84    pub fn get(&self, id: FormNodeId) -> &FormNode {
85        &self.nodes[id.0]
86    }
87
88    /// Get a mutable reference to a node by ID.
89    pub fn get_mut(&mut self, id: FormNodeId) -> &mut FormNode {
90        &mut self.nodes[id.0]
91    }
92
93    /// Access the metadata for a node.
94    pub fn meta(&self, id: FormNodeId) -> &FormNodeMeta {
95        &self.metadata[id.0]
96    }
97
98    /// Mutably access the metadata for a node.
99    pub fn meta_mut(&mut self, id: FormNodeId) -> &mut FormNodeMeta {
100        &mut self.metadata[id.0]
101    }
102
103    /// Look up a node by its XFA `id` attribute.
104    pub fn find_by_xfa_id(&self, id: &str) -> Option<FormNodeId> {
105        self.node_ids.get(id).copied()
106    }
107}
108
109impl Default for FormTree {
110    fn default() -> Self {
111        Self::new()
112    }
113}
114
115/// A single node in the Form DOM.
116#[derive(Debug, Clone)]
117pub struct FormNode {
118    /// Node name.
119    pub name: String,
120    /// Node type.
121    pub node_type: FormNodeType,
122    /// Box model.
123    pub box_model: BoxModel,
124    /// Layout strategy.
125    pub layout: LayoutStrategy,
126    /// Child node IDs.
127    pub children: Vec<FormNodeId>,
128    /// Occurrence rules for repeating subforms.
129    pub occur: Occur,
130    /// Font metrics for text measurement (Draw/Field nodes).
131    pub font: FontMetrics,
132    /// FormCalc calculate script (XFA S14.3.2): runs to compute the field's value.
133    pub calculate: Option<String>,
134    /// FormCalc validate script: runs to validate the field's value, returns bool.
135    pub validate: Option<String>,
136    /// Column widths for table-layout subforms (XFA columnWidths attribute).
137    /// Positive values are fixed widths in points; -1.0 means auto-size.
138    /// Empty for non-table nodes.
139    pub column_widths: Vec<f64>,
140    /// Column span for cells inside a table row (XFA colSpan attribute).
141    /// 1 = single column (default), N = span N columns, -1 = span remaining.
142    pub col_span: i32,
143}
144
145/// The scripting language used by an XFA `<script>` element.
146#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
147pub enum ScriptLanguage {
148    /// FormCalc is the XFA default when `contentType` is omitted.
149    #[default]
150    FormCalc,
151    /// JavaScript event handlers and calculations.
152    JavaScript,
153    /// Any other declared script language (for example VBScript).
154    Other,
155}
156
157/// Script metadata collected from `<event>` / `<calculate>` elements.
158#[derive(Debug, Clone, PartialEq, Eq, Default)]
159pub struct EventScript {
160    /// Script source.
161    pub script: String,
162    /// Script language.
163    pub language: ScriptLanguage,
164    /// Activity event.
165    pub activity: Option<String>,
166    /// Event reference.
167    pub event_ref: Option<String>,
168    /// Run-at location.
169    pub run_at: Option<String>,
170}
171
172impl EventScript {
173    /// Create a new event script.
174    pub fn new(
175        script: String,
176        language: ScriptLanguage,
177        activity: Option<String>,
178        event_ref: Option<String>,
179        run_at: Option<String>,
180    ) -> Self {
181        Self {
182            script,
183            language,
184            activity,
185            event_ref,
186            run_at,
187        }
188    }
189
190    /// Create a FormCalc event script.
191    pub fn formcalc(script: impl Into<String>, activity: Option<&str>) -> Self {
192        Self::new(
193            script.into(),
194            ScriptLanguage::FormCalc,
195            activity.map(str::to_string),
196            None,
197            None,
198        )
199    }
200
201    /// Create a JavaScript event script.
202    pub fn javascript(script: impl Into<String>, activity: Option<&str>) -> Self {
203        Self::new(
204            script.into(),
205            ScriptLanguage::JavaScript,
206            activity.map(str::to_string),
207            None,
208            None,
209        )
210    }
211}
212
213/// Content for draw nodes (static graphic elements).
214///
215/// XFA Spec 3.3 §2.1 (p24) — Draw element: fixed content (boilerplate).
216/// Includes text, lines, rectangles, arcs. Images are handled separately
217/// via `FormNodeType::Image`.
218///
219/// TODO(§2.3): `circle` draw content not implemented (spec allows via arc with
220///   startAngle=0, sweepAngle=360).
221#[derive(Debug, Clone)]
222pub enum DrawContent {
223    /// Text draw content.
224    Text(String),
225    /// Line draw content.
226    Line {
227        /// Start x coordinate.
228        x1: f64,
229        /// Start y coordinate.
230        y1: f64,
231        /// End x coordinate.
232        x2: f64,
233        /// End y coordinate.
234        y2: f64,
235    },
236    /// Rectangle draw content.
237    Rectangle {
238        /// X coordinate.
239        x: f64,
240        /// Y coordinate.
241        y: f64,
242        /// Width.
243        w: f64,
244        /// Height.
245        h: f64,
246        /// Corner radius.
247        radius: f64,
248    },
249    /// Arc draw content.
250    Arc {
251        /// X coordinate.
252        x: f64,
253        /// Y coordinate.
254        y: f64,
255        /// Width.
256        w: f64,
257        /// Height.
258        h: f64,
259        /// Start angle.
260        start_angle: f64,
261        /// Sweep angle.
262        sweep_angle: f64,
263    },
264}
265
266/// The type of form node.
267#[derive(Debug, Clone)]
268pub enum FormNodeType {
269    /// Root subform.
270    Root,
271    /// A page set containing page areas.
272    PageSet,
273    /// A page area (page template) with content areas.
274    PageArea {
275        /// Content areas.
276        content_areas: Vec<ContentArea>,
277    },
278    /// A generic subform container.
279    Subform,
280    /// XFA `<area>` — a positioned container (XFA 3.3 Appendix B).
281    ///
282    /// Semantically identical to a `Subform` with positioned layout: children
283    /// have absolute positions within the area.  The layout engine treats this
284    /// exactly like `Subform` for layout purposes.
285    Area,
286    /// XFA `<exclGroup>` — an exclusive (radio-button) group (XFA 3.3 §7.2).
287    ///
288    /// Contains multiple radio-button `<field>` children where exactly one can
289    /// be selected.  In layout this behaves like a `Subform` with top-to-bottom
290    /// flow; each child field is rendered normally.
291    ExclGroup,
292    /// XFA `<subformSet>` — a transparent set of subforms (XFA 3.3 §7.1).
293    ///
294    /// Used for conditional instantiation.  In layout the set is transparent:
295    /// its children are processed as if they were direct children of the
296    /// containing subform (same data context).
297    SubformSet,
298    /// A form field (text field, checkbox, etc.).
299    Field {
300        /// Field value.
301        value: String,
302    },
303    /// A static draw element (text, image, line, etc.).
304    Draw(DrawContent),
305    /// A static image draw element.
306    Image {
307        /// Image data.
308        data: Vec<u8>,
309        /// Image MIME type.
310        mime_type: String,
311    },
312}
313
314/// Occurrence rules for repeating subforms (XFA S3.3 occur element).
315///
316/// Controls how many instances of a subform are created. The layout engine
317/// expands templates based on the `initial` count, bounded by `min` and `max`.
318#[derive(Debug, Clone)]
319pub struct Occur {
320    /// Minimum number of occurrences (default 1).
321    pub min: u32,
322    /// Maximum number of occurrences (-1 = unlimited). Default 1.
323    /// Using `Option<u32>` where `None` means unlimited.
324    pub max: Option<u32>,
325    /// Initial number of occurrences (default = min).
326    pub initial: u32,
327}
328
329impl Default for Occur {
330    fn default() -> Self {
331        Self {
332            min: 1,
333            max: Some(1),
334            initial: 1,
335        }
336    }
337}
338
339impl Occur {
340    /// Occur rule that means "exactly once" (the default).
341    pub fn once() -> Self {
342        Self::default()
343    }
344
345    /// Occur rule for a repeating subform.
346    pub fn repeating(min: u32, max: Option<u32>, initial: u32) -> Self {
347        let initial = initial.max(min);
348        let initial = match max {
349            Some(m) => initial.min(m),
350            None => initial,
351        };
352        Self { min, max, initial }
353    }
354
355    /// How many instances should be created.
356    pub fn count(&self) -> u32 {
357        self.initial
358    }
359
360    /// Whether the subform can repeat (max > 1 or unlimited).
361    pub fn is_repeating(&self) -> bool {
362        match self.max {
363            Some(m) => m > 1,
364            None => true,
365        }
366    }
367}
368
369/// A content area within a page area.
370#[derive(Debug, Clone)]
371pub struct ContentArea {
372    /// Content area name.
373    pub name: String,
374    /// X coordinate.
375    pub x: f64,
376    /// Y coordinate.
377    pub y: f64,
378    /// Width.
379    pub width: f64,
380    /// Height.
381    pub height: f64,
382    /// Leader (header) node placed at the top of each page's content area.
383    pub leader: Option<FormNodeId>,
384    /// Trailer (footer) node placed at the bottom of each page's content area.
385    pub trailer: Option<FormNodeId>,
386}
387
388impl Default for ContentArea {
389    fn default() -> Self {
390        Self {
391            name: String::new(),
392            x: 0.0,
393            y: 0.0,
394            width: 612.0,  // US Letter width in points
395            height: 792.0, // US Letter height in points
396            leader: None,
397            trailer: None,
398        }
399    }
400}
401
402// ---------------------------------------------------------------------------
403// Metadata, style, and kind types
404// ---------------------------------------------------------------------------
405
406/// XFA `presence` attribute values (XFA 3.3 §2.6 p67-68).
407///
408/// Controls visibility and layout space allocation:
409/// - `Visible` -- all phases: binding, automation, layout, rendering, interaction.
410/// - `Hidden` -- binding + automation only; no layout space, no rendering.
411/// - `Invisible` -- binding + automation + layout; takes up space but not visible.
412/// - `Inactive` -- binding only; completely absent from form.
413///
414/// Note: spec defines `hidden` as "effectively absent" (no space) and `invisible`
415/// as "takes space but not visible". Our `is_layout_hidden` implementation reflects this.
416#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
417pub enum Presence {
418    #[default]
419    /// Visible.
420    Visible,
421    /// Hidden.
422    Hidden,
423    /// Invisible.
424    Invisible,
425    /// Inactive.
426    Inactive,
427}
428
429impl Presence {
430    /// True when the element should not be rendered.
431    pub fn is_not_visible(self) -> bool {
432        !matches!(self, Presence::Visible)
433    }
434
435    /// True when the element should not occupy layout space.
436    ///
437    /// XFA Spec 3.3 §2.6 (p68):
438    /// - `hidden`:   no layout space, no rendering (effectively absent)
439    /// - `invisible`: no layout space in Adobe (spec says "takes space",
440    ///   but empirical testing shows Adobe skips it)
441    /// - `inactive`:  completely absent (no binding, no space)
442    ///
443    /// `Hidden` was previously excluded from this predicate based on an
444    /// incorrect assumption that Adobe reserves space for hidden elements.
445    /// GATE #27 testing proved this wrong: hidden subforms with `<break>`
446    /// elements caused 2-23x overpagination in forms with many
447    /// `presence="hidden"` subforms (fixes #806).
448    pub fn is_layout_hidden(self) -> bool {
449        matches!(
450            self,
451            Presence::Hidden | Presence::Invisible | Presence::Inactive
452        )
453    }
454}
455
456/// Extended metadata for a form node.
457///
458/// Carries XFA attributes that the layout engine and dynamic scripting
459/// system need but that are not part of the core `FormNode` shape.
460#[derive(Debug, Clone, Default)]
461pub struct FormNodeMeta {
462    /// Optional XFA `id` attribute.
463    pub xfa_id: Option<String>,
464    /// XFA presence attribute (visible/hidden/invisible/inactive).
465    pub presence: Presence,
466    /// Whether a page break should be inserted before this node.
467    pub page_break_before: bool,
468    /// Whether a page break should be inserted after this node.
469    pub page_break_after: bool,
470    /// Target page area name/id for the break (e.g. "MP3", "Page4_ID").
471    pub break_target: Option<String>,
472    /// Whether this node targets a specific content area via
473    /// `breakBefore targetType="contentArea"`.  Such nodes should be
474    /// excluded from the primary content flow (they go into small
475    /// decorative areas like "flatten" or "eSign").
476    pub content_area_break: bool,
477    /// Overflow leader reference name.
478    pub overflow_leader: Option<String>,
479    /// Overflow trailer reference name.
480    pub overflow_trailer: Option<String>,
481    /// Bookend leader reference name (XFA 3.3 §17).
482    ///
483    /// Where `overflow` serves the continuation pages, `bookend` serves the
484    /// outer two: the leader goes on the **first** page of this subform, the
485    /// trailer on the **last**. Before this, a `<bookend>` in a template was
486    /// silently ignored -- see #150.
487    pub bookend_leader: Option<String>,
488    /// Bookend trailer reference name.
489    pub bookend_trailer: Option<String>,
490    /// Keep with next content area.
491    pub keep_next_content_area: bool,
492    /// Keep with previous content area.
493    pub keep_previous_content_area: bool,
494    /// Keep intact within content area.
495    pub keep_intact_content_area: bool,
496    /// Layout-ready script (XFA S14.3).
497    pub layout_ready_script: Option<String>,
498    /// Event scripts collected from `<event>` and `<calculate>` children.
499    pub event_scripts: Vec<EventScript>,
500    /// Explicit XFA data binding ref from `<bind ref="...">`.
501    pub data_bind_ref: Option<String>,
502    /// Whether the node explicitly opts out of data binding via `<bind match="none">`.
503    pub data_bind_none: bool,
504    /// Visual style (font, colors, borders).
505    pub style: FormNodeStyle,
506    /// The kind of field (text, checkbox, radio, etc.).
507    pub field_kind: FieldKind,
508    /// The kind of group (none or exclusive choice).
509    pub group_kind: GroupKind,
510    /// Item value for fields inside an exclGroup.
511    pub item_value: Option<String>,
512    /// Check box / radio button size in points.
513    pub check_size: Option<f64>,
514    /// Display items for choice list fields (XFA 3.3 §7.7).
515    pub display_items: Vec<String>,
516    /// Save items for choice list fields (XFA 3.3 §7.7).
517    pub save_items: Vec<String>,
518    /// Runtime-populated choice list items from sandboxed script `addItem` calls.
519    /// Each tuple is `(display_value, save_value)`. Phase D-β only; layout does
520    /// not yet read this in default mode.
521    pub runtime_listbox_items: Vec<(String, String)>,
522    /// XFA anchorType for positioned layout (XFA 3.3 §2.6, App A p1510).
523    pub anchor_type: AnchorType,
524    /// Raw `DataDom` arena index of the data node this form node was bound to
525    /// during the merge phase. `None` means unbound. Phase D-γ: populated by
526    /// `FormMerger` and consumed by the JS runtime to resolve `$record`.
527    pub bound_data_node: Option<usize>,
528    /// True when this node is a synthetic prototype for a repeating subform
529    /// whose live instance count is currently zero. Used by the JS runtime
530    /// to expose an InstanceManager via `parent._child` even when no data
531    /// bindings produced live rows. Layout treats this as `presence = Hidden`.
532    pub is_zero_instance_prototype: bool,
533    /// True when this pageArea was allocated by the XFA runtime (recorded in
534    /// the form-DOM packet of an Adobe-Reader-saved PDF) rather than declared
535    /// once in the template.
536    ///
537    /// XFA 3.3 §8.6 / §3.1: the form DOM enumerates the runtime page-tree
538    /// state.  When the form DOM lists more `<pageArea>` siblings than the
539    /// template defines, those extra instances were created by the
540    /// `pageSet`/`occur` machinery and each one must emit a layout page even
541    /// when the flowing-body queue is exhausted (it is the runtime's record
542    /// of an already-paginated page).
543    pub runtime_instantiated_page: bool,
544    /// XFA `access` attribute (XFA 3.3 §6.1) when explicitly set on this
545    /// node. `None` means not specified — the effective access is inherited
546    /// from the nearest ancestor container that sets one (default `Open`).
547    pub access: Option<Access>,
548    /// True when the field's `<ui><textEdit>` declares `multiLine="1"`.
549    pub multiline: bool,
550    /// True when the field's `<validate>` declares `nullTest="error"`
551    /// (XFA 3.3 §6.3 — a mandatory field).
552    pub required: bool,
553}
554
555/// XFA `access` attribute values (XFA 3.3 §6.1, `field`/`exclGroup`/
556/// `subform` elements). Controls interactive mutability of a node and,
557/// for containers, of all contained fields.
558#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
559pub enum Access {
560    /// Default: the field is interactive and writable.
561    #[default]
562    Open,
563    /// Visible, value selectable/copyable, but not writable.
564    ReadOnly,
565    /// Not interactive at all (no focus, no writes).
566    Protected,
567    /// Rendered like a regular field but excluded from interaction.
568    NonInteractive,
569}
570
571impl Access {
572    /// Parse the template attribute string. Unknown values map to `Open`
573    /// (spec default).
574    pub fn parse(s: &str) -> Access {
575        match s.trim() {
576            "readOnly" => Access::ReadOnly,
577            "protected" => Access::Protected,
578            "nonInteractive" => Access::NonInteractive,
579            _ => Access::Open,
580        }
581    }
582
583    /// Whether a value write should be rejected for this access level.
584    pub fn denies_writes(self) -> bool {
585        !matches!(self, Access::Open)
586    }
587}
588
589/// XFA `anchorType` attribute (XFA 3.3 §2.6, Appendix A p1510).
590///
591/// Determines which point of an element is placed at the (x,y) coordinate
592/// in positioned layout.  Default is `TopLeft`.
593#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
594pub enum AnchorType {
595    #[default]
596    /// Top-left anchor.
597    TopLeft,
598    /// Top-center anchor.
599    TopCenter,
600    /// Top-right anchor.
601    TopRight,
602    /// Middle-left anchor.
603    MiddleLeft,
604    /// Middle-center anchor.
605    MiddleCenter,
606    /// Middle-right anchor.
607    MiddleRight,
608    /// Bottom-left anchor.
609    BottomLeft,
610    /// Bottom-center anchor.
611    BottomCenter,
612    /// Bottom-right anchor.
613    BottomRight,
614}
615
616/// The kind of group container.
617#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
618pub enum GroupKind {
619    #[default]
620    /// No group.
621    None,
622    /// Exclusive choice group.
623    ExclusiveChoice,
624}
625
626/// The kind of form field.
627#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
628pub enum FieldKind {
629    #[default]
630    /// Text field.
631    Text,
632    /// Checkbox field.
633    Checkbox,
634    /// Radio button field.
635    Radio,
636    /// Button field.
637    Button,
638    /// Dropdown field.
639    Dropdown,
640    /// Signature field.
641    Signature,
642    /// Date/time picker field.
643    DateTimePicker,
644    /// Numeric edit field.
645    NumericEdit,
646    /// Password edit field.
647    PasswordEdit,
648    /// Image edit field.
649    ImageEdit,
650    /// Barcode field.
651    Barcode,
652}
653
654/// A span of rich text with per-span style overrides.
655///
656/// XFA Spec 3.3 §4.2.7 (p155) — `<exData contentType="text/html">` stores
657/// XHTML content with inline CSS. Each span carries its own formatting
658/// (font, color, weight, etc.) that overrides the node-level defaults.
659#[derive(Debug, Clone, PartialEq)]
660pub struct RichTextSpan {
661    /// Text content.
662    pub text: String,
663    /// Font size.
664    pub font_size: Option<f64>,
665    /// Font family.
666    pub font_family: Option<String>,
667    /// Font weight.
668    pub font_weight: Option<String>,
669    /// Font style.
670    pub font_style: Option<String>,
671    /// Text color.
672    pub text_color: Option<(u8, u8, u8)>,
673    /// Underline flag.
674    pub underline: bool,
675    /// Line-through flag.
676    pub line_through: bool,
677    /// Vertical shift of the baseline, as a fraction of the font size.
678    ///
679    /// Positive is up (`<sup>`), negative is down (`<sub>`), `0.0` is plain.
680    /// A fraction rather than an absolute measure, so the shift scales with the
681    /// font size -- otherwise a superscript in 8pt text would sit as high as one
682    /// in 24pt text.
683    ///
684    /// Added for #151: `<sub>` and `<sup>` were silently ignored. The text
685    /// stayed, the meaning went -- and in legal and scientific forms that is the
686    /// difference between a footnote marker and an ordinary number.
687    pub baseline_shift: f64,
688}
689
690/// Visual style properties for a form node.
691#[derive(Debug, Clone, PartialEq)]
692pub struct FormNodeStyle {
693    /// Font family.
694    pub font_family: Option<String>,
695    /// Font size.
696    pub font_size: Option<f64>,
697    /// Font weight.
698    pub font_weight: Option<String>,
699    /// Font style.
700    pub font_style: Option<String>,
701    /// Text color.
702    pub text_color: Option<(u8, u8, u8)>,
703    /// Background color.
704    pub bg_color: Option<(u8, u8, u8)>,
705    /// Border color.
706    pub border_color: Option<(u8, u8, u8)>,
707    /// Per-edge border colors (top, right, bottom, left) in RGB 0-255.
708    /// When set, overrides `border_color` for individual edges.
709    pub border_colors: Option<[(u8, u8, u8); 4]>,
710    /// Border width in points.
711    pub border_width_pt: Option<f64>,
712    /// Per-edge border widths (top, right, bottom, left) in points.
713    /// When set, overrides `border_width_pt` for individual edges.
714    pub border_widths: Option<[f64; 4]>,
715    /// Paragraph space above in points (XFA `<para spaceAbove>`).
716    pub space_above_pt: Option<f64>,
717    /// Paragraph space below in points (XFA `<para spaceBelow>`).
718    pub space_below_pt: Option<f64>,
719    /// Paragraph left margin in points (XFA `<para marginLeft>`).
720    pub margin_left_pt: Option<f64>,
721    /// Paragraph right margin in points (XFA `<para marginRight>`).
722    pub margin_right_pt: Option<f64>,
723    /// XFA Spec 3.3 §17 "para" (p803) — lineHeight: baseline-to-baseline
724    /// distance in points. When `None`, use font metrics.
725    pub line_height_pt: Option<f64>,
726    /// XFA Spec 3.3 §17 "para" (p803) — textIndent: indentation of the first
727    /// line of each paragraph in points.
728    pub text_indent_pt: Option<f64>,
729    /// Margin top inset in points (XFA `<margin topInset>`).
730    pub inset_top_pt: Option<f64>,
731    /// Margin bottom inset in points (XFA `<margin bottomInset>`).
732    pub inset_bottom_pt: Option<f64>,
733    /// Margin left inset in points (XFA `<margin leftInset>`).
734    pub inset_left_pt: Option<f64>,
735    /// Margin right inset in points (XFA `<margin rightInset>`).
736    pub inset_right_pt: Option<f64>,
737    /// Vertical text alignment (XFA `<para vAlign>`).
738    pub v_align: Option<VerticalAlign>,
739    /// Horizontal alignment for layout positioning (XFA `<para hAlign>`, §8.3).
740    pub h_align: Option<TextAlign>,
741    /// Border corner radius in points (XFA `<border><corner radius>`).
742    pub border_radius_pt: Option<f64>,
743    /// Border edge stroke style (XFA `<border><edge stroke>`).
744    pub border_style: Option<String>,
745    /// Per-edge visibility: [top, right, bottom, left]. All true when absent.
746    pub border_edges: [bool; 4],
747    /// XFA Spec 3.3 §17 (p716) — genericFamily fallback hint.
748    /// Values: serif, sansSerif, monospaced, decorative, fantasy, cursive.
749    pub generic_family: Option<String>,
750    /// Font horizontal scale factor (XFA `<font fontHorizontalScale>`).
751    /// 1.0 = 100% (default), 0.96 = 96%, etc.
752    pub font_horizontal_scale: Option<f64>,
753    /// Letter spacing in points (XFA `<font letterSpacing>`).
754    /// 0.0 = normal (default). Negative values tighten, positive widen.
755    pub letter_spacing_pt: Option<f64>,
756    /// Caption text (XFA `<caption><value><text>`).
757    pub caption_text: Option<String>,
758    /// Caption placement (left/right/top/bottom/inline).
759    pub caption_placement: Option<String>,
760    /// Caption reserve width/height in points.
761    pub caption_reserve: Option<f64>,
762    /// Caption's own typeface (XFA `<caption><font typeface>`), independent of
763    /// the field's font. When set, the caption label is drawn in this face.
764    pub caption_font_family: Option<String>,
765    /// Caption's own font size in points (XFA `<caption><font size>`).
766    pub caption_font_size: Option<f64>,
767    /// CheckButton mark style (XFA `<checkButton mark="...">`).
768    /// Values: "check", "circle", "cross", "diamond", "square", "star".
769    pub check_button_mark: Option<String>,
770    /// CheckButton on-value (first `<items>` entry, XFA 3.3 §17.8).
771    pub check_button_on_value: Option<String>,
772    /// CheckButton off-value (second `<items>` entry). When omitted, the
773    /// spec default is the null string.
774    pub check_button_off_value: Option<String>,
775    /// CheckButton neutral-value (third `<items>` entry, checkbox only).
776    pub check_button_neutral_value: Option<String>,
777    /// Rich text spans parsed from `<exData contentType="text/html">` XHTML.
778    pub rich_text_spans: Option<Vec<RichTextSpan>>,
779    /// Font underline (XFA `<font underline="1">`).
780    pub underline: bool,
781    /// Font line-through / strikethrough (XFA `<font lineThrough="1">`).
782    pub line_through: bool,
783    /// XFA format picture clause (e.g. `num{z,zzz.99}`).
784    pub format_pattern: Option<String>,
785}
786
787impl Default for FormNodeStyle {
788    fn default() -> Self {
789        Self {
790            font_family: None,
791            font_size: None,
792            font_weight: None,
793            font_style: None,
794            text_color: None,
795            bg_color: None,
796            border_color: None,
797            border_colors: None,
798            border_width_pt: None,
799            border_widths: None,
800            space_above_pt: None,
801            space_below_pt: None,
802            margin_left_pt: None,
803            margin_right_pt: None,
804            line_height_pt: None,
805            text_indent_pt: None,
806            inset_top_pt: None,
807            inset_bottom_pt: None,
808            inset_left_pt: None,
809            inset_right_pt: None,
810            v_align: None,
811            h_align: None,
812            border_radius_pt: None,
813            border_style: None,
814            border_edges: [true, true, true, true],
815            generic_family: None,
816            font_horizontal_scale: None,
817            letter_spacing_pt: None,
818            caption_text: None,
819            caption_placement: None,
820            caption_reserve: None,
821            caption_font_family: None,
822            caption_font_size: None,
823            check_button_mark: None,
824            check_button_on_value: None,
825            check_button_off_value: None,
826            check_button_neutral_value: None,
827            rich_text_spans: None,
828            underline: false,
829            line_through: false,
830            format_pattern: None,
831        }
832    }
833}