Skip to main content

pdfluent_forms/
button.rs

1//! Checkbox, radio button, and push button implementation (B.3 + B.5).
2
3use crate::flags::FieldFlags;
4use crate::tree::*;
5
6/// Button sub-kind.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum ButtonKind {
9    Checkbox,
10    Radio,
11    PushButton,
12}
13
14/// Determine button sub-kind from flags.
15pub fn button_kind(flags: FieldFlags) -> ButtonKind {
16    if flags.push_button() {
17        ButtonKind::PushButton
18    } else if flags.radio() {
19        ButtonKind::Radio
20    } else {
21        ButtonKind::Checkbox
22    }
23}
24
25/// Check if a button field is currently "on" (checked/selected).
26pub fn is_checked(tree: &FieldTree, id: FieldId) -> bool {
27    if let Some(ref state) = tree.get(id).appearance_state {
28        return state != "Off";
29    }
30    matches!(tree.effective_value(id), Some(FieldValue::Text(s)) if s != "Off")
31}
32
33/// Get the "on" state name for a button widget.
34pub fn on_state_name(tree: &FieldTree, id: FieldId) -> String {
35    if let Some(ref state) = tree.get(id).appearance_state {
36        if state != "Off" {
37            return state.clone();
38        }
39    }
40    if let Some(FieldValue::Text(s)) = tree.effective_value(id) {
41        if s != "Off" {
42            return s.clone();
43        }
44    }
45    "Yes".into()
46}
47
48/// Toggle a checkbox field. Returns `false` if read-only or not a checkbox.
49pub fn toggle_checkbox(tree: &mut FieldTree, id: FieldId) -> bool {
50    let flags = tree.effective_flags(id);
51    if flags.read_only() || button_kind(flags) != ButtonKind::Checkbox {
52        return false;
53    }
54    let new_state = if is_checked(tree, id) {
55        "Off".to_string()
56    } else {
57        on_state_name(tree, id)
58    };
59    tree.get_mut(id).value = Some(FieldValue::Text(new_state.clone()));
60    tree.get_mut(id).appearance_state = Some(new_state);
61    true
62}
63
64/// Select a radio button, deselecting siblings. Returns `false` if read-only.
65pub fn select_radio(tree: &mut FieldTree, id: FieldId) -> bool {
66    if tree.effective_flags(id).read_only() {
67        return false;
68    }
69    let on_name = on_state_name(tree, id);
70    if let Some(pid) = tree.get(id).parent {
71        let siblings: Vec<FieldId> = tree.get(pid).children.clone();
72        for sib in siblings {
73            if sib != id {
74                tree.get_mut(sib).value = Some(FieldValue::Text("Off".into()));
75                tree.get_mut(sib).appearance_state = Some("Off".into());
76            }
77        }
78        tree.get_mut(pid).value = Some(FieldValue::Text(on_name.clone()));
79    }
80    tree.get_mut(id).value = Some(FieldValue::Text(on_name.clone()));
81    tree.get_mut(id).appearance_state = Some(on_name);
82    true
83}
84
85/// Parsed submit-form action.
86#[derive(Debug, Clone)]
87pub struct SubmitAction {
88    pub url: String,
89    pub flags: u32,
90}
91
92/// Parsed reset-form action.
93#[derive(Debug, Clone)]
94pub struct ResetAction {
95    pub fields: Vec<String>,
96    pub flags: u32,
97}
98
99/// Icon/caption layout for push buttons (/TP values).
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum IconCaptionLayout {
102    CaptionOnly,
103    IconOnly,
104    CaptionBelow,
105    CaptionAbove,
106    CaptionRight,
107    CaptionLeft,
108    CaptionOverlay,
109}
110
111impl From<u32> for IconCaptionLayout {
112    fn from(v: u32) -> Self {
113        match v {
114            1 => Self::IconOnly,
115            2 => Self::CaptionBelow,
116            3 => Self::CaptionAbove,
117            4 => Self::CaptionRight,
118            5 => Self::CaptionLeft,
119            6 => Self::CaptionOverlay,
120            _ => Self::CaptionOnly,
121        }
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    fn make_checkbox() -> (FieldTree, FieldId) {
129        let mut tree = FieldTree::new();
130        let id = tree.alloc(FieldNode {
131            partial_name: "cb".into(),
132            alternate_name: None,
133            mapping_name: None,
134            field_type: Some(FieldType::Button),
135            flags: FieldFlags::empty(),
136            value: Some(FieldValue::Text("Off".into())),
137            default_value: None,
138            default_appearance: None,
139            quadding: None,
140            max_len: None,
141            options: vec![],
142            top_index: None,
143            rect: Some([0.0, 0.0, 12.0, 12.0]),
144            appearance_state: Some("Off".into()),
145            page_index: None,
146            parent: None,
147            children: vec![],
148            object_id: None,
149            has_actions: false,
150            mk: None,
151            border_style: None,
152        });
153        (tree, id)
154    }
155
156    #[test]
157    fn checkbox_initially_off() {
158        let (tree, id) = make_checkbox();
159        assert!(!is_checked(&tree, id));
160    }
161    #[test]
162    fn toggle_checkbox_on_off() {
163        let (mut tree, id) = make_checkbox();
164        assert!(toggle_checkbox(&mut tree, id));
165        assert!(is_checked(&tree, id));
166        assert!(toggle_checkbox(&mut tree, id));
167        assert!(!is_checked(&tree, id));
168    }
169    #[test]
170    fn radio_mutual_exclusion() {
171        let mut tree = FieldTree::new();
172        let group = tree.alloc(FieldNode {
173            partial_name: "rg".into(),
174            alternate_name: None,
175            mapping_name: None,
176            field_type: Some(FieldType::Button),
177            flags: FieldFlags::from_bits((1 << 15) | (1 << 14)),
178            value: Some(FieldValue::Text("Off".into())),
179            default_value: None,
180            default_appearance: None,
181            quadding: None,
182            max_len: None,
183            options: vec![],
184            top_index: None,
185            rect: None,
186            appearance_state: None,
187            page_index: None,
188            parent: None,
189            children: vec![],
190            object_id: None,
191            has_actions: false,
192            mk: None,
193            border_style: None,
194        });
195        let mk = |tree: &mut FieldTree, n: &str| -> FieldId {
196            let id = tree.alloc(FieldNode {
197                partial_name: n.into(),
198                alternate_name: None,
199                mapping_name: None,
200                field_type: None,
201                flags: FieldFlags::from_bits((1 << 15) | (1 << 14)),
202                value: Some(FieldValue::Text("Off".into())),
203                default_value: None,
204                default_appearance: None,
205                quadding: None,
206                max_len: None,
207                options: vec![],
208                top_index: None,
209                rect: Some([0.0, 0.0, 12.0, 12.0]),
210                appearance_state: Some("Off".into()),
211                page_index: None,
212                parent: Some(group),
213                children: vec![],
214                object_id: None,
215                has_actions: false,
216                mk: None,
217                border_style: None,
218            });
219            tree.get_mut(group).children.push(id);
220            id
221        };
222        let r1 = mk(&mut tree, "opt1");
223        let r2 = mk(&mut tree, "opt2");
224        assert!(select_radio(&mut tree, r1));
225        assert!(is_checked(&tree, r1));
226        assert!(!is_checked(&tree, r2));
227        assert!(select_radio(&mut tree, r2));
228        assert!(!is_checked(&tree, r1));
229        assert!(is_checked(&tree, r2));
230    }
231}