Skip to main content

pdfluent_forms/
text.rs

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