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}