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 is fully visible (rendered and occupying layout
431    /// space). The inverse of [`is_not_visible`](Self::is_not_visible).
432    pub fn is_visible(self) -> bool {
433        matches!(self, Presence::Visible)
434    }
435
436    /// True when the element should not be rendered.
437    pub fn is_not_visible(self) -> bool {
438        !matches!(self, Presence::Visible)
439    }
440
441    /// True when the element should not occupy layout space.
442    ///
443    /// XFA Spec 3.3 §2.6 (p68):
444    /// - `hidden`:   no layout space, no rendering (effectively absent)
445    /// - `invisible`: no layout space in Adobe (spec says "takes space",
446    ///   but empirical testing shows Adobe skips it)
447    /// - `inactive`:  completely absent (no binding, no space)
448    ///
449    /// `Hidden` was previously excluded from this predicate based on an
450    /// incorrect assumption that Adobe reserves space for hidden elements.
451    /// GATE #27 testing proved this wrong: hidden subforms with `<break>`
452    /// elements caused 2-23x overpagination in forms with many
453    /// `presence="hidden"` subforms (fixes #806).
454    pub fn is_layout_hidden(self) -> bool {
455        matches!(
456            self,
457            Presence::Hidden | Presence::Invisible | Presence::Inactive
458        )
459    }
460}
461
462/// Extended metadata for a form node.
463///
464/// Carries XFA attributes that the layout engine and dynamic scripting
465/// system need but that are not part of the core `FormNode` shape.
466#[derive(Debug, Clone, Default)]
467pub struct FormNodeMeta {
468    /// Optional XFA `id` attribute.
469    pub xfa_id: Option<String>,
470    /// XFA presence attribute (visible/hidden/invisible/inactive).
471    pub presence: Presence,
472    /// Whether a page break should be inserted before this node.
473    pub page_break_before: bool,
474    /// Whether a page break should be inserted after this node.
475    pub page_break_after: bool,
476    /// Target page area name/id for the break (e.g. "MP3", "Page4_ID").
477    pub break_target: Option<String>,
478    /// Whether this node targets a specific content area via
479    /// `breakBefore targetType="contentArea"`.  Such nodes should be
480    /// excluded from the primary content flow (they go into small
481    /// decorative areas like "flatten" or "eSign").
482    pub content_area_break: bool,
483    /// Overflow leader reference name.
484    pub overflow_leader: Option<String>,
485    /// Overflow trailer reference name.
486    pub overflow_trailer: Option<String>,
487    /// Bookend leader reference name (XFA 3.3 §17).
488    ///
489    /// Where `overflow` serves the continuation pages, `bookend` serves the
490    /// outer two: the leader goes on the **first** page of this subform, the
491    /// trailer on the **last**. Before this, a `<bookend>` in a template was
492    /// silently ignored -- see #150.
493    pub bookend_leader: Option<String>,
494    /// Bookend trailer reference name.
495    pub bookend_trailer: Option<String>,
496    /// Keep with next content area.
497    pub keep_next_content_area: bool,
498    /// Keep with previous content area.
499    pub keep_previous_content_area: bool,
500    /// Keep intact within content area.
501    pub keep_intact_content_area: bool,
502    /// Layout-ready script (XFA S14.3).
503    pub layout_ready_script: Option<String>,
504    /// Event scripts collected from `<event>` and `<calculate>` children.
505    pub event_scripts: Vec<EventScript>,
506    /// Explicit XFA data binding ref from `<bind ref="...">`.
507    pub data_bind_ref: Option<String>,
508    /// Whether the node explicitly opts out of data binding via `<bind match="none">`.
509    pub data_bind_none: bool,
510    /// Visual style (font, colors, borders).
511    pub style: FormNodeStyle,
512    /// The kind of field (text, checkbox, radio, etc.).
513    pub field_kind: FieldKind,
514    /// The kind of group (none or exclusive choice).
515    pub group_kind: GroupKind,
516    /// Item value for fields inside an exclGroup.
517    pub item_value: Option<String>,
518    /// Check box / radio button size in points.
519    pub check_size: Option<f64>,
520    /// Display items for choice list fields (XFA 3.3 §7.7).
521    pub display_items: Vec<String>,
522    /// Save items for choice list fields (XFA 3.3 §7.7).
523    pub save_items: Vec<String>,
524    /// Runtime-populated choice list items from sandboxed script `addItem` calls.
525    /// Each tuple is `(display_value, save_value)`. Phase D-β only; layout does
526    /// not yet read this in default mode.
527    pub runtime_listbox_items: Vec<(String, String)>,
528    /// XFA anchorType for positioned layout (XFA 3.3 §2.6, App A p1510).
529    pub anchor_type: AnchorType,
530    /// Raw `DataDom` arena index of the data node this form node was bound to
531    /// during the merge phase. `None` means unbound. Phase D-γ: populated by
532    /// `FormMerger` and consumed by the JS runtime to resolve `$record`.
533    pub bound_data_node: Option<usize>,
534    /// True when this node is a synthetic prototype for a repeating subform
535    /// whose live instance count is currently zero. Used by the JS runtime
536    /// to expose an InstanceManager via `parent._child` even when no data
537    /// bindings produced live rows. Layout treats this as `presence = Hidden`.
538    pub is_zero_instance_prototype: bool,
539    /// True when this pageArea was allocated by the XFA runtime (recorded in
540    /// the form-DOM packet of an Adobe-Reader-saved PDF) rather than declared
541    /// once in the template.
542    ///
543    /// XFA 3.3 §8.6 / §3.1: the form DOM enumerates the runtime page-tree
544    /// state.  When the form DOM lists more `<pageArea>` siblings than the
545    /// template defines, those extra instances were created by the
546    /// `pageSet`/`occur` machinery and each one must emit a layout page even
547    /// when the flowing-body queue is exhausted (it is the runtime's record
548    /// of an already-paginated page).
549    pub runtime_instantiated_page: bool,
550    /// XFA `access` attribute (XFA 3.3 §6.1) when explicitly set on this
551    /// node. `None` means not specified — the effective access is inherited
552    /// from the nearest ancestor container that sets one (default `Open`).
553    pub access: Option<Access>,
554    /// True when the field's `<ui><textEdit>` declares `multiLine="1"`.
555    pub multiline: bool,
556    /// True when the field's `<validate>` declares `nullTest="error"`
557    /// (XFA 3.3 §6.3 — a mandatory field).
558    pub required: bool,
559}
560
561/// XFA `access` attribute values (XFA 3.3 §6.1, `field`/`exclGroup`/
562/// `subform` elements). Controls interactive mutability of a node and,
563/// for containers, of all contained fields.
564#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
565pub enum Access {
566    /// Default: the field is interactive and writable.
567    #[default]
568    Open,
569    /// Visible, value selectable/copyable, but not writable.
570    ReadOnly,
571    /// Not interactive at all (no focus, no writes).
572    Protected,
573    /// Rendered like a regular field but excluded from interaction.
574    NonInteractive,
575}
576
577impl Access {
578    /// Parse the template attribute string. Unknown values map to `Open`
579    /// (spec default).
580    pub fn parse(s: &str) -> Access {
581        match s.trim() {
582            "readOnly" => Access::ReadOnly,
583            "protected" => Access::Protected,
584            "nonInteractive" => Access::NonInteractive,
585            _ => Access::Open,
586        }
587    }
588
589    /// Whether a value write should be rejected for this access level.
590    pub fn denies_writes(self) -> bool {
591        !matches!(self, Access::Open)
592    }
593}
594
595/// XFA `anchorType` attribute (XFA 3.3 §2.6, Appendix A p1510).
596///
597/// Determines which point of an element is placed at the (x,y) coordinate
598/// in positioned layout.  Default is `TopLeft`.
599#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
600pub enum AnchorType {
601    #[default]
602    /// Top-left anchor.
603    TopLeft,
604    /// Top-center anchor.
605    TopCenter,
606    /// Top-right anchor.
607    TopRight,
608    /// Middle-left anchor.
609    MiddleLeft,
610    /// Middle-center anchor.
611    MiddleCenter,
612    /// Middle-right anchor.
613    MiddleRight,
614    /// Bottom-left anchor.
615    BottomLeft,
616    /// Bottom-center anchor.
617    BottomCenter,
618    /// Bottom-right anchor.
619    BottomRight,
620}
621
622/// The kind of group container.
623#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
624pub enum GroupKind {
625    #[default]
626    /// No group.
627    None,
628    /// Exclusive choice group.
629    ExclusiveChoice,
630}
631
632/// The kind of form field.
633#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
634pub enum FieldKind {
635    #[default]
636    /// Text field.
637    Text,
638    /// Checkbox field.
639    Checkbox,
640    /// Radio button field.
641    Radio,
642    /// Button field.
643    Button,
644    /// Dropdown field.
645    Dropdown,
646    /// Signature field.
647    Signature,
648    /// Date/time picker field.
649    DateTimePicker,
650    /// Numeric edit field.
651    NumericEdit,
652    /// Password edit field.
653    PasswordEdit,
654    /// Image edit field.
655    ImageEdit,
656    /// Barcode field.
657    Barcode,
658}
659
660/// A span of rich text with per-span style overrides.
661///
662/// XFA Spec 3.3 §4.2.7 (p155) — `<exData contentType="text/html">` stores
663/// XHTML content with inline CSS. Each span carries its own formatting
664/// (font, color, weight, etc.) that overrides the node-level defaults.
665#[derive(Debug, Clone, PartialEq)]
666pub struct RichTextSpan {
667    /// Text content.
668    pub text: String,
669    /// Font size.
670    pub font_size: Option<f64>,
671    /// Font family.
672    pub font_family: Option<String>,
673    /// Font weight.
674    pub font_weight: Option<String>,
675    /// Font style.
676    pub font_style: Option<String>,
677    /// Text color.
678    pub text_color: Option<(u8, u8, u8)>,
679    /// Underline flag.
680    pub underline: bool,
681    /// Line-through flag.
682    pub line_through: bool,
683    /// Vertical shift of the baseline, as a fraction of the font size.
684    ///
685    /// Positive is up (`<sup>`), negative is down (`<sub>`), `0.0` is plain.
686    /// A fraction rather than an absolute measure, so the shift scales with the
687    /// font size -- otherwise a superscript in 8pt text would sit as high as one
688    /// in 24pt text.
689    ///
690    /// Added for #151: `<sub>` and `<sup>` were silently ignored. The text
691    /// stayed, the meaning went -- and in legal and scientific forms that is the
692    /// difference between a footnote marker and an ordinary number.
693    pub baseline_shift: f64,
694}
695
696/// Visual style properties for a form node.
697#[derive(Debug, Clone, PartialEq)]
698pub struct FormNodeStyle {
699    /// Font family.
700    pub font_family: Option<String>,
701    /// Font size.
702    pub font_size: Option<f64>,
703    /// Font weight.
704    pub font_weight: Option<String>,
705    /// Font style.
706    pub font_style: Option<String>,
707    /// Text color.
708    pub text_color: Option<(u8, u8, u8)>,
709    /// Background color.
710    pub bg_color: Option<(u8, u8, u8)>,
711    /// Border color.
712    pub border_color: Option<(u8, u8, u8)>,
713    /// Per-edge border colors (top, right, bottom, left) in RGB 0-255.
714    /// When set, overrides `border_color` for individual edges.
715    pub border_colors: Option<[(u8, u8, u8); 4]>,
716    /// Border width in points.
717    pub border_width_pt: Option<f64>,
718    /// Per-edge border widths (top, right, bottom, left) in points.
719    /// When set, overrides `border_width_pt` for individual edges.
720    pub border_widths: Option<[f64; 4]>,
721    /// Paragraph space above in points (XFA `<para spaceAbove>`).
722    pub space_above_pt: Option<f64>,
723    /// Paragraph space below in points (XFA `<para spaceBelow>`).
724    pub space_below_pt: Option<f64>,
725    /// Paragraph left margin in points (XFA `<para marginLeft>`).
726    pub margin_left_pt: Option<f64>,
727    /// Paragraph right margin in points (XFA `<para marginRight>`).
728    pub margin_right_pt: Option<f64>,
729    /// XFA Spec 3.3 §17 "para" (p803) — lineHeight: baseline-to-baseline
730    /// distance in points. When `None`, use font metrics.
731    pub line_height_pt: Option<f64>,
732    /// XFA Spec 3.3 §17 "para" (p803) — textIndent: indentation of the first
733    /// line of each paragraph in points.
734    pub text_indent_pt: Option<f64>,
735    /// Margin top inset in points (XFA `<margin topInset>`).
736    pub inset_top_pt: Option<f64>,
737    /// Margin bottom inset in points (XFA `<margin bottomInset>`).
738    pub inset_bottom_pt: Option<f64>,
739    /// Margin left inset in points (XFA `<margin leftInset>`).
740    pub inset_left_pt: Option<f64>,
741    /// Margin right inset in points (XFA `<margin rightInset>`).
742    pub inset_right_pt: Option<f64>,
743    /// Vertical text alignment (XFA `<para vAlign>`).
744    pub v_align: Option<VerticalAlign>,
745    /// Horizontal alignment for layout positioning (XFA `<para hAlign>`, §8.3).
746    pub h_align: Option<TextAlign>,
747    /// Border corner radius in points (XFA `<border><corner radius>`).
748    pub border_radius_pt: Option<f64>,
749    /// Border edge stroke style (XFA `<border><edge stroke>`).
750    pub border_style: Option<String>,
751    /// Per-edge visibility: [top, right, bottom, left]. All true when absent.
752    pub border_edges: [bool; 4],
753    /// XFA Spec 3.3 §17 (p716) — genericFamily fallback hint.
754    /// Values: serif, sansSerif, monospaced, decorative, fantasy, cursive.
755    pub generic_family: Option<String>,
756    /// Font horizontal scale factor (XFA `<font fontHorizontalScale>`).
757    /// 1.0 = 100% (default), 0.96 = 96%, etc.
758    pub font_horizontal_scale: Option<f64>,
759    /// Letter spacing in points (XFA `<font letterSpacing>`).
760    /// 0.0 = normal (default). Negative values tighten, positive widen.
761    pub letter_spacing_pt: Option<f64>,
762    /// Caption text (XFA `<caption><value><text>`).
763    pub caption_text: Option<String>,
764    /// Caption placement (left/right/top/bottom/inline).
765    pub caption_placement: Option<String>,
766    /// Caption reserve width/height in points.
767    pub caption_reserve: Option<f64>,
768    /// Caption's own typeface (XFA `<caption><font typeface>`), independent of
769    /// the field's font. When set, the caption label is drawn in this face.
770    pub caption_font_family: Option<String>,
771    /// Caption's own font size in points (XFA `<caption><font size>`).
772    pub caption_font_size: Option<f64>,
773    /// CheckButton mark style (XFA `<checkButton mark="...">`).
774    /// Values: "check", "circle", "cross", "diamond", "square", "star".
775    pub check_button_mark: Option<String>,
776    /// CheckButton on-value (first `<items>` entry, XFA 3.3 §17.8).
777    pub check_button_on_value: Option<String>,
778    /// CheckButton off-value (second `<items>` entry). When omitted, the
779    /// spec default is the null string.
780    pub check_button_off_value: Option<String>,
781    /// CheckButton neutral-value (third `<items>` entry, checkbox only).
782    pub check_button_neutral_value: Option<String>,
783    /// Rich text spans parsed from `<exData contentType="text/html">` XHTML.
784    pub rich_text_spans: Option<Vec<RichTextSpan>>,
785    /// Font underline (XFA `<font underline="1">`).
786    pub underline: bool,
787    /// Font line-through / strikethrough (XFA `<font lineThrough="1">`).
788    pub line_through: bool,
789    /// XFA format picture clause (e.g. `num{z,zzz.99}`).
790    pub format_pattern: Option<String>,
791}
792
793impl Default for FormNodeStyle {
794    fn default() -> Self {
795        Self {
796            font_family: None,
797            font_size: None,
798            font_weight: None,
799            font_style: None,
800            text_color: None,
801            bg_color: None,
802            border_color: None,
803            border_colors: None,
804            border_width_pt: None,
805            border_widths: None,
806            space_above_pt: None,
807            space_below_pt: None,
808            margin_left_pt: None,
809            margin_right_pt: None,
810            line_height_pt: None,
811            text_indent_pt: None,
812            inset_top_pt: None,
813            inset_bottom_pt: None,
814            inset_left_pt: None,
815            inset_right_pt: None,
816            v_align: None,
817            h_align: None,
818            border_radius_pt: None,
819            border_style: None,
820            border_edges: [true, true, true, true],
821            generic_family: None,
822            font_horizontal_scale: None,
823            letter_spacing_pt: None,
824            caption_text: None,
825            caption_placement: None,
826            caption_reserve: None,
827            caption_font_family: None,
828            caption_font_size: None,
829            check_button_mark: None,
830            check_button_on_value: None,
831            check_button_off_value: None,
832            check_button_neutral_value: None,
833            rich_text_spans: None,
834            underline: false,
835            line_through: false,
836            format_pattern: None,
837        }
838    }
839}