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/// Sub-kind of a button field.
7///
8/// AcroForm models all of checkbox, radio button, and push button as the
9/// same `/Btn` field type, distinguished only by the `Pushbutton` and `Radio`
10/// flags in the field flags word. Use [`button_kind`] to derive this enum
11/// from a [`FieldFlags`] value.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum ButtonKind {
14    /// Two-state toggle. Default kind when neither `Pushbutton` nor `Radio`
15    /// flags are set. Drawn as a tickbox; user clicks to flip its value
16    /// between the off-state and an "on" appearance state.
17    Checkbox,
18    /// Mutually-exclusive option within a parent radio group. Selecting one
19    /// radio child automatically de-selects its siblings — see
20    /// [`select_radio`].
21    Radio,
22    /// Click-to-action button without persistent state. Typically wired to
23    /// a JavaScript or submit/reset action via the `/AA` dictionary; its
24    /// `value` is not meaningful as form data.
25    PushButton,
26}
27
28/// Determine button sub-kind from flags.
29pub fn button_kind(flags: FieldFlags) -> ButtonKind {
30    if flags.push_button() {
31        ButtonKind::PushButton
32    } else if flags.radio() {
33        ButtonKind::Radio
34    } else {
35        ButtonKind::Checkbox
36    }
37}
38
39/// Check if a button field is currently "on" (checked/selected).
40pub fn is_checked(tree: &FieldTree, id: FieldId) -> bool {
41    if let Some(ref state) = tree.get(id).appearance_state {
42        return state != "Off";
43    }
44    matches!(tree.effective_value(id), Some(FieldValue::Text(s)) if s != "Off")
45}
46
47/// Get the "on" state name for a button widget.
48pub fn on_state_name(tree: &FieldTree, id: FieldId) -> String {
49    if let Some(ref state) = tree.get(id).appearance_state {
50        if state != "Off" {
51            return state.clone();
52        }
53    }
54    if let Some(FieldValue::Text(s)) = tree.effective_value(id) {
55        if s != "Off" {
56            return s.clone();
57        }
58    }
59    "Yes".into()
60}
61
62/// Toggle a checkbox field. Returns `false` if read-only or not a checkbox.
63pub fn toggle_checkbox(tree: &mut FieldTree, id: FieldId) -> bool {
64    let flags = tree.effective_flags(id);
65    if flags.read_only() || button_kind(flags) != ButtonKind::Checkbox {
66        return false;
67    }
68    let new_state = if is_checked(tree, id) {
69        "Off".to_string()
70    } else {
71        on_state_name(tree, id)
72    };
73    tree.get_mut(id).value = Some(FieldValue::Text(new_state.clone()));
74    tree.get_mut(id).appearance_state = Some(new_state);
75    true
76}
77
78/// Select a radio button, deselecting siblings. Returns `false` if read-only.
79pub fn select_radio(tree: &mut FieldTree, id: FieldId) -> bool {
80    if tree.effective_flags(id).read_only() {
81        return false;
82    }
83    let on_name = on_state_name(tree, id);
84    if let Some(pid) = tree.get(id).parent {
85        let siblings: Vec<FieldId> = tree.get(pid).children.clone();
86        for sib in siblings {
87            if sib != id {
88                tree.get_mut(sib).value = Some(FieldValue::Text("Off".into()));
89                tree.get_mut(sib).appearance_state = Some("Off".into());
90            }
91        }
92        tree.get_mut(pid).value = Some(FieldValue::Text(on_name.clone()));
93    }
94    tree.get_mut(id).value = Some(FieldValue::Text(on_name.clone()));
95    tree.get_mut(id).appearance_state = Some(on_name);
96    true
97}
98
99/// Parsed submit-form action attached to a button.
100///
101/// Produced when the parser encounters an `/A` dictionary with `/S /SubmitForm`.
102/// Triggered when the user clicks a push button configured to send form data
103/// to a remote endpoint.
104#[derive(Debug, Clone)]
105pub struct SubmitAction {
106    /// The submission target URL (`/F` entry of the action). Usually HTTP/HTTPS;
107    /// PDF also allows `mailto:` and FTP URLs.
108    pub url: String,
109    /// Bit flags from the `/Flags` entry controlling submit format (FDF, HTML,
110    /// XFDF, JSON), field inclusion (include vs. exclude), and HTTP method.
111    /// See ISO 32000-2 §12.7.5.2 Table 257 for the bit assignments.
112    pub flags: u32,
113}
114
115/// Parsed reset-form action attached to a button.
116///
117/// Produced when the parser encounters an `/A` dictionary with `/S /ResetForm`.
118/// Triggered when the user clicks a push button configured to clear form
119/// fields back to their default values.
120#[derive(Debug, Clone)]
121pub struct ResetAction {
122    /// Fully-qualified field names (parent.child notation) targeted by the
123    /// reset. Empty means "reset all fields in the form".
124    pub fields: Vec<String>,
125    /// Bit flags controlling include-vs-exclude semantics (`/Fields` is the
126    /// list to reset, or the list to skip, depending on bit 0). See ISO
127    /// 32000-2 §12.7.5.3.
128    pub flags: u32,
129}
130
131/// Icon and caption arrangement for a push button's appearance.
132///
133/// Maps to the `/TP` entry of the button's appearance characteristics
134/// dictionary (`/MK`). Determines whether the button shows text only, an
135/// image only, or both — and where the caption sits relative to the icon.
136/// PDF default when `/TP` is absent or unrecognized is [`Self::CaptionOnly`].
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub enum IconCaptionLayout {
139    /// `/TP 0` — show only the caption (`/CA`). Icon (if any) is ignored.
140    CaptionOnly,
141    /// `/TP 1` — show only the icon. Caption is ignored.
142    IconOnly,
143    /// `/TP 2` — caption rendered below the icon.
144    CaptionBelow,
145    /// `/TP 3` — caption rendered above the icon.
146    CaptionAbove,
147    /// `/TP 4` — caption rendered to the right of the icon.
148    CaptionRight,
149    /// `/TP 5` — caption rendered to the left of the icon.
150    CaptionLeft,
151    /// `/TP 6` — caption overlaid on top of the icon.
152    CaptionOverlay,
153}
154
155impl From<u32> for IconCaptionLayout {
156    fn from(v: u32) -> Self {
157        match v {
158            1 => Self::IconOnly,
159            2 => Self::CaptionBelow,
160            3 => Self::CaptionAbove,
161            4 => Self::CaptionRight,
162            5 => Self::CaptionLeft,
163            6 => Self::CaptionOverlay,
164            _ => Self::CaptionOnly,
165        }
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172    fn make_checkbox() -> (FieldTree, FieldId) {
173        let mut tree = FieldTree::new();
174        let id = tree.alloc(FieldNode {
175            partial_name: "cb".into(),
176            alternate_name: None,
177            mapping_name: None,
178            field_type: Some(FieldType::Button),
179            flags: FieldFlags::empty(),
180            value: Some(FieldValue::Text("Off".into())),
181            default_value: None,
182            default_appearance: None,
183            quadding: None,
184            max_len: None,
185            options: vec![],
186            top_index: None,
187            rect: Some([0.0, 0.0, 12.0, 12.0]),
188            appearance_state: Some("Off".into()),
189            on_state: None,
190            page_index: None,
191            parent: None,
192            children: vec![],
193            object_id: None,
194            has_actions: false,
195            mk: None,
196            border_style: None,
197        });
198        (tree, id)
199    }
200
201    #[test]
202    fn checkbox_initially_off() {
203        let (tree, id) = make_checkbox();
204        assert!(!is_checked(&tree, id));
205    }
206    #[test]
207    fn toggle_checkbox_on_off() {
208        let (mut tree, id) = make_checkbox();
209        assert!(toggle_checkbox(&mut tree, id));
210        assert!(is_checked(&tree, id));
211        assert!(toggle_checkbox(&mut tree, id));
212        assert!(!is_checked(&tree, id));
213    }
214    #[test]
215    fn radio_mutual_exclusion() {
216        let mut tree = FieldTree::new();
217        let group = tree.alloc(FieldNode {
218            partial_name: "rg".into(),
219            alternate_name: None,
220            mapping_name: None,
221            field_type: Some(FieldType::Button),
222            flags: FieldFlags::from_bits((1 << 15) | (1 << 14)),
223            value: Some(FieldValue::Text("Off".into())),
224            default_value: None,
225            default_appearance: None,
226            quadding: None,
227            max_len: None,
228            options: vec![],
229            top_index: None,
230            rect: None,
231            appearance_state: None,
232            on_state: None,
233            page_index: None,
234            parent: None,
235            children: vec![],
236            object_id: None,
237            has_actions: false,
238            mk: None,
239            border_style: None,
240        });
241        let mk = |tree: &mut FieldTree, n: &str| -> FieldId {
242            let id = tree.alloc(FieldNode {
243                partial_name: n.into(),
244                alternate_name: None,
245                mapping_name: None,
246                field_type: None,
247                flags: FieldFlags::from_bits((1 << 15) | (1 << 14)),
248                value: Some(FieldValue::Text("Off".into())),
249                default_value: None,
250                default_appearance: None,
251                quadding: None,
252                max_len: None,
253                options: vec![],
254                top_index: None,
255                rect: Some([0.0, 0.0, 12.0, 12.0]),
256                appearance_state: Some("Off".into()),
257                on_state: None,
258                page_index: None,
259                parent: Some(group),
260                children: vec![],
261                object_id: None,
262                has_actions: false,
263                mk: None,
264                border_style: None,
265            });
266            tree.get_mut(group).children.push(id);
267            id
268        };
269        let r1 = mk(&mut tree, "opt1");
270        let r2 = mk(&mut tree, "opt2");
271        assert!(select_radio(&mut tree, r1));
272        assert!(is_checked(&tree, r1));
273        assert!(!is_checked(&tree, r2));
274        assert!(select_radio(&mut tree, r2));
275        assert!(!is_checked(&tree, r1));
276        assert!(is_checked(&tree, r2));
277    }
278}