Skip to main content

pdfluent_forms/
choice.rs

1//! Dropdown and listbox implementation (B.4).
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 choice (`/Ch`) field, derived from its flags word.
13///
14/// AcroForm models combo boxes and list boxes as the same field type;
15/// the four variants below distinguish the visual presentation and
16/// selection semantics. Use [`choice_kind`] to derive this enum from a
17/// [`FieldFlags`] value.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum ChoiceKind {
20    /// Closed-list combo box: pick one of the predefined `/Opt` entries.
21    /// Free-text input not allowed. (`Combo` flag set; `Edit` clear.)
22    ComboBox,
23    /// Combo box that also accepts free-text input not in the option list.
24    /// Useful for "common cities, but type your own" patterns.
25    /// (`Combo` and `Edit` both set.)
26    EditableCombo,
27    /// Single-selection list box; one option visible-and-selected at a
28    /// time, others scroll. (`Combo` clear; `MultiSelect` clear.)
29    ListBox,
30    /// List box allowing multiple options to be selected simultaneously
31    /// (Ctrl/Cmd-click). The field's value is then a name array rather
32    /// than a single name. (`MultiSelect` flag set.)
33    MultiSelectListBox,
34}
35
36/// Determine choice sub-kind from flags.
37pub fn choice_kind(flags: FieldFlags) -> ChoiceKind {
38    if flags.combo() {
39        if flags.edit() {
40            ChoiceKind::EditableCombo
41        } else {
42            ChoiceKind::ComboBox
43        }
44    } else if flags.multi_select() {
45        ChoiceKind::MultiSelectListBox
46    } else {
47        ChoiceKind::ListBox
48    }
49}
50
51/// Get the currently selected value(s).
52pub fn get_selection(tree: &FieldTree, id: FieldId) -> Vec<String> {
53    match tree.effective_value(id) {
54        Some(FieldValue::Text(s)) => vec![s.clone()],
55        Some(FieldValue::StringArray(arr)) => arr.clone(),
56        None => vec![],
57    }
58}
59
60/// Get the list of available options.
61pub fn get_options(tree: &FieldTree, id: FieldId) -> &[ChoiceOption] {
62    &tree.get(id).options
63}
64
65/// Set the selection for a single-select choice field.
66/// For non-editable combos, value must match an option. Returns `false` if read-only or invalid.
67pub fn set_selection(tree: &mut FieldTree, id: FieldId, value: &str) -> bool {
68    let flags = tree.effective_flags(id);
69    if flags.read_only() {
70        return false;
71    }
72    if choice_kind(flags) == ChoiceKind::ComboBox
73        && !tree
74            .get(id)
75            .options
76            .iter()
77            .any(|o| o.export == value || o.display == value)
78    {
79        return false;
80    }
81    tree.get_mut(id).value = Some(FieldValue::Text(value.to_string()));
82    true
83}
84
85/// Set multiple selections for a multi-select list box. Returns `false` if read-only or not multi-select.
86pub fn set_multi_selection(tree: &mut FieldTree, id: FieldId, values: Vec<String>) -> bool {
87    let flags = tree.effective_flags(id);
88    if flags.read_only() || !flags.multi_select() {
89        return false;
90    }
91    tree.get_mut(id).value = Some(FieldValue::StringArray(values));
92    true
93}
94
95/// Get the index of the first selected option, if any.
96pub fn selected_index(tree: &FieldTree, id: FieldId) -> Option<usize> {
97    let first = get_selection(tree, id).into_iter().next()?;
98    tree.get(id)
99        .options
100        .iter()
101        .position(|o| o.export == first || o.display == first)
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    fn make_choice_tree() -> (FieldTree, FieldId) {
108        let mut tree = FieldTree::new();
109        let id = tree.alloc(FieldNode {
110            partial_name: "dd".into(),
111            alternate_name: None,
112            mapping_name: None,
113            field_type: Some(FieldType::Choice),
114            flags: FieldFlags::from_bits(1 << 17),
115            value: None,
116            default_value: None,
117            default_appearance: None,
118            quadding: None,
119            max_len: None,
120            options: vec![
121                ChoiceOption {
122                    export: "a".into(),
123                    display: "Alpha".into(),
124                },
125                ChoiceOption {
126                    export: "b".into(),
127                    display: "Beta".into(),
128                },
129                ChoiceOption {
130                    export: "c".into(),
131                    display: "Gamma".into(),
132                },
133            ],
134            top_index: None,
135            rect: Some([0.0, 0.0, 150.0, 20.0]),
136            appearance_state: None,
137            on_state: None,
138            page_index: None,
139            parent: None,
140            children: vec![],
141            object_id: None,
142            has_actions: false,
143            mk: None,
144            border_style: None,
145        });
146        (tree, id)
147    }
148    #[test]
149    fn kind_combo() {
150        assert_eq!(
151            choice_kind(FieldFlags::from_bits(1 << 17)),
152            ChoiceKind::ComboBox
153        );
154    }
155    #[test]
156    fn kind_editable() {
157        assert_eq!(
158            choice_kind(FieldFlags::from_bits((1 << 17) | (1 << 18))),
159            ChoiceKind::EditableCombo
160        );
161    }
162    #[test]
163    fn kind_listbox() {
164        assert_eq!(choice_kind(FieldFlags::empty()), ChoiceKind::ListBox);
165    }
166    #[test]
167    fn kind_multi() {
168        assert_eq!(
169            choice_kind(FieldFlags::from_bits(1 << 21)),
170            ChoiceKind::MultiSelectListBox
171        );
172    }
173    #[test]
174    fn set_valid() {
175        let (mut tree, id) = make_choice_tree();
176        assert!(set_selection(&mut tree, id, "a"));
177        assert_eq!(get_selection(&tree, id), vec!["a"]);
178    }
179    #[test]
180    fn set_invalid() {
181        let (mut tree, id) = make_choice_tree();
182        assert!(!set_selection(&mut tree, id, "nope"));
183    }
184    #[test]
185    fn sel_index() {
186        let (mut tree, id) = make_choice_tree();
187        set_selection(&mut tree, id, "b");
188        assert_eq!(selected_index(&tree, id), Some(1));
189    }
190}