Skip to main content

pdfluent_forms/
text.rs

1//! Text field implementation (B.2).
2
3// Copyright (c) 2026 Innovation Trigger B.V.
4//
5// PDFluent is available under two licences, at your option: the GNU AGPLv3, or
6// the PDFluent Commercial Licence. See the LICENSE file in this repository --
7// that file travels with the copy you received, which a URL does not.
8
9use crate::flags::FieldFlags;
10use crate::tree::*;
11
12/// Sub-kind of a text (`/Tx`) field, derived from its flags word.
13///
14/// AcroForm models all text input variants as the same field type; the
15/// flags below distinguish the visual presentation and validation rules.
16/// Use [`text_field_kind`] to derive this enum from a [`FieldFlags`] value.
17/// The variants are checked in priority order — for example, a field with
18/// both `FileSelect` and `Multiline` set resolves to [`Self::FileSelect`].
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum TextFieldKind {
21    /// Single-line free-text input. Default when no specialization flag is
22    /// set.
23    Normal,
24    /// Multi-line text input — text wraps at the field width and accepts
25    /// embedded newlines. (`Multiline` flag set.)
26    Multiline,
27    /// Single-line input that masks each character (typical for password
28    /// entry). (`Password` flag set.)
29    Password,
30    /// Fixed-pitch input divided into `MaxLen` equally-spaced cells —
31    /// useful for serial numbers, postal codes. Requires `MaxLen` to be
32    /// set on the field. (`Comb` flag set.)
33    Comb,
34    /// Rich-text input where the value carries XHTML formatting in
35    /// addition to the plain text. (`RichText` flag set.)
36    RichText,
37    /// File-selector field: the value is a path to a local file the user
38    /// selected via the viewer's file dialog. (`FileSelect` flag set.)
39    FileSelect,
40}
41
42/// Determine the text field sub-kind from field flags.
43pub fn text_field_kind(flags: FieldFlags) -> TextFieldKind {
44    if flags.file_select() {
45        TextFieldKind::FileSelect
46    } else if flags.comb() {
47        TextFieldKind::Comb
48    } else if flags.rich_text() {
49        TextFieldKind::RichText
50    } else if flags.password() {
51        TextFieldKind::Password
52    } else if flags.multiline() {
53        TextFieldKind::Multiline
54    } else {
55        TextFieldKind::Normal
56    }
57}
58
59/// Get the current text value of a text field.
60pub fn get_text_value(tree: &FieldTree, id: FieldId) -> Option<String> {
61    match tree.effective_value(id)? {
62        FieldValue::Text(s) => Some(s.clone()),
63        FieldValue::StringArray(arr) => arr.first().cloned(),
64    }
65}
66
67/// Set a text field's value, enforcing MaxLen if present.
68/// Returns `false` if the field is read-only.
69pub fn set_text_value(tree: &mut FieldTree, id: FieldId, text: &str) -> bool {
70    if tree.effective_flags(id).read_only() {
71        return false;
72    }
73    let max_len = tree.effective_max_len(id);
74    let value = if let Some(ml) = max_len {
75        text.chars().take(ml as usize).collect()
76    } else {
77        text.to_string()
78    };
79    tree.get_mut(id).value = Some(FieldValue::Text(value));
80    true
81}
82
83/// For comb fields, compute the width of each cell.
84pub fn comb_cell_width(tree: &FieldTree, id: FieldId) -> Option<f32> {
85    let max_len = tree.effective_max_len(id)?;
86    if max_len == 0 {
87        return None;
88    }
89    let rect = tree.get(id).rect?;
90    Some((rect[2] - rect[0]) / max_len as f32)
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    fn make_text_tree() -> (FieldTree, FieldId) {
97        let mut tree = FieldTree::new();
98        let id = tree.alloc(FieldNode {
99            partial_name: "text1".into(),
100            alternate_name: None,
101            mapping_name: None,
102            field_type: Some(FieldType::Text),
103            flags: FieldFlags::empty(),
104            value: Some(FieldValue::Text("hello".into())),
105            default_value: None,
106            default_appearance: None,
107            quadding: None,
108            max_len: None,
109            options: vec![],
110            top_index: None,
111            rect: Some([0.0, 0.0, 200.0, 20.0]),
112            appearance_state: None,
113            on_state: None,
114            page_index: None,
115            parent: None,
116            children: vec![],
117            object_id: None,
118            has_actions: false,
119            mk: None,
120            border_style: None,
121        });
122        (tree, id)
123    }
124
125    #[test]
126    fn get_value() {
127        let (tree, id) = make_text_tree();
128        assert_eq!(get_text_value(&tree, id), Some("hello".into()));
129    }
130    #[test]
131    fn set_value() {
132        let (mut tree, id) = make_text_tree();
133        assert!(set_text_value(&mut tree, id, "world"));
134        assert_eq!(get_text_value(&tree, id), Some("world".into()));
135    }
136    #[test]
137    fn set_value_readonly() {
138        let (mut tree, id) = make_text_tree();
139        tree.get_mut(id).flags = FieldFlags::from_bits(1);
140        assert!(!set_text_value(&mut tree, id, "nope"));
141    }
142    #[test]
143    fn set_value_maxlen() {
144        let (mut tree, id) = make_text_tree();
145        tree.get_mut(id).max_len = Some(3);
146        assert!(set_text_value(&mut tree, id, "abcdef"));
147        assert_eq!(get_text_value(&tree, id), Some("abc".into()));
148    }
149    #[test]
150    fn kind_detection() {
151        assert_eq!(text_field_kind(FieldFlags::empty()), TextFieldKind::Normal);
152        assert_eq!(
153            text_field_kind(FieldFlags::from_bits(1 << 12)),
154            TextFieldKind::Multiline
155        );
156        assert_eq!(
157            text_field_kind(FieldFlags::from_bits(1 << 13)),
158            TextFieldKind::Password
159        );
160        assert_eq!(
161            text_field_kind(FieldFlags::from_bits(1 << 24)),
162            TextFieldKind::Comb
163        );
164    }
165    #[test]
166    fn comb_width() {
167        let (mut tree, id) = make_text_tree();
168        tree.get_mut(id).max_len = Some(10);
169        assert_eq!(comb_cell_width(&tree, id), Some(20.0));
170    }
171}