pdfrum_form/field/toggle.rs
1//! Check boxes and radio buttons: the two controls whose value is a state
2//! name rather than text.
3//!
4//! They share a machine and differ in one line. A check box **toggles**; a
5//! radio button **sets**, unconditionally, and there is no way to un-select
6//! one by clicking it.
7//!
8//! Two rules that look like bugs and are not: **a read-only control consumes
9//! the keystroke and does nothing** (consumption and effect are independent),
10//! and **neither control handles arrow keys at all**.
11
12/// The off state's appearance name, which every check box and radio button
13/// shares.
14///
15/// A file may name the *on* state anything; this is the only reserved
16/// spelling.
17///
18/// ```
19/// use pdfrum_form::field::{ToggleState, toggle::OFF_STATE};
20///
21/// let mut state = ToggleState::new(OFF_STATE, "Yes");
22/// state.set_checked(true);
23/// assert_eq!(state.state, "Yes");
24/// ```
25pub const OFF_STATE: &str = "Off";
26
27/// A check box or radio button's interaction state.
28///
29/// One fact: which appearance state the control is showing.
30///
31/// ```
32/// use pdfrum_form::field::{ToggleKind, ToggleState, activate, toggle::OFF_STATE};
33///
34/// let mut state = ToggleState::new(OFF_STATE, "On");
35/// assert!(!state.is_checked());
36/// activate(&mut state, ToggleKind::Check, false);
37/// assert_eq!(state.state, "On");
38/// ```
39#[derive(Debug, Clone, PartialEq, Eq, Default)]
40pub struct ToggleState {
41 /// The appearance state name currently selected — the "on" name when the
42 /// control is checked, [`OFF_STATE`] when it is not.
43 pub state: String,
44 /// The name this control shows when checked, from its appearance
45 /// dictionary. Empty when the control offers no on state at all.
46 pub on_state: String,
47 /// Which **control** of the field is the checked one, by its raw
48 /// `/Annots` index, once one has been chosen.
49 ///
50 /// A field's controls share one state here, which is right for a check
51 /// box (a field with one kid) and not enough for a radio group, where the
52 /// clicked control shows its own on state and every other control of the
53 /// field shows `Off`. This records the half of that a shared state *can*
54 /// express — **which** control is on — so a sibling's appearance can be
55 /// answered `Off` without a second state record.
56 ///
57 /// `None` before any control has been activated, which is the state a
58 /// group loaded from a file with no `/V` is in.
59 pub checked_control: Option<crate::session::AnnotId>,
60}
61
62impl ToggleState {
63 /// A control showing `state`, whose checked appearance is `on_state`.
64 ///
65 /// An empty `on_state` means the control offers no checked appearance at
66 /// all, and it can then never be checked.
67 ///
68 /// ```
69 /// use pdfrum_form::field::{ToggleState, toggle::OFF_STATE};
70 ///
71 /// let state = ToggleState::new(OFF_STATE, "On");
72 /// assert!(!state.is_checked());
73 /// assert_eq!(state.checked_control, None, "nothing has been clicked yet");
74 /// ```
75 #[must_use]
76 pub fn new(state: impl Into<String>, on_state: impl Into<String>) -> ToggleState {
77 ToggleState {
78 state: state.into(),
79 on_state: on_state.into(),
80 checked_control: None,
81 }
82 }
83
84 /// Whether the control is currently checked.
85 ///
86 /// An empty state reads as clear rather than as ambiguous: a widget with
87 /// no appearance state at all is not checked.
88 ///
89 /// ```
90 /// use pdfrum_form::field::ToggleState;
91 ///
92 /// assert!(ToggleState::new("On", "On").is_checked());
93 /// assert!(!ToggleState::new("Off", "On").is_checked());
94 /// assert!(!ToggleState::new("", "On").is_checked());
95 /// ```
96 #[must_use]
97 pub fn is_checked(&self) -> bool {
98 self.state != OFF_STATE && !self.state.is_empty()
99 }
100
101 /// The appearance state one **control** of this field should draw.
102 ///
103 /// The per-control answer a shared [`ToggleState`] can give: the clicked
104 /// control shows its own on state and every other control of the same
105 /// field shows `Off`.
106 ///
107 /// Three cases, and the middle one is why this is not simply
108 /// [`Self::state`]:
109 ///
110 /// - **Nothing has been clicked** ([`Self::checked_control`] is `None`):
111 /// `None`, meaning "read the widget's own `/AS`" — a group loaded from a
112 /// file must render exactly as the file wrote it, kid by kid.
113 /// - **This control is the chosen one**: its own on state, which for a
114 /// radio group is a name only this kid carries.
115 /// - **A sibling was chosen**: [`OFF_STATE`], whatever the file's `/AS`
116 /// still says.
117 ///
118 /// ```
119 /// use pdfrum_form::AnnotId;
120 /// use pdfrum_form::field::{ToggleState, toggle::OFF_STATE};
121 ///
122 /// let clicked = AnnotId::new(0u32, 1);
123 /// let sibling = AnnotId::new(0u32, 2);
124 ///
125 /// let mut state = ToggleState::new(OFF_STATE, "Choice1");
126 /// // Nothing clicked yet: read each widget's own `/AS`.
127 /// assert_eq!(state.state_for_control(clicked), None);
128 ///
129 /// state.set_checked(true);
130 /// state.checked_control = Some(clicked);
131 /// assert_eq!(state.state_for_control(clicked), Some("Choice1"));
132 /// assert_eq!(state.state_for_control(sibling), Some(OFF_STATE));
133 /// ```
134 // A check box is a field with one control, so `control` is always the
135 // chosen one once anything has been clicked and the answer is its own
136 // state either way.
137 #[must_use]
138 pub fn state_for_control(&self, control: crate::session::AnnotId) -> Option<&str> {
139 let chosen = self.checked_control?;
140 if chosen == control {
141 Some(&self.state)
142 } else {
143 Some(OFF_STATE)
144 }
145 }
146
147 /// Sets the control checked or clear.
148 ///
149 /// Checking a control with no on state leaves it clear: there is no
150 /// appearance to show.
151 ///
152 /// ```
153 /// use pdfrum_form::field::{ToggleState, toggle::OFF_STATE};
154 ///
155 /// let mut none = ToggleState::new(OFF_STATE, "");
156 /// none.set_checked(true);
157 /// assert!(!none.is_checked());
158 /// ```
159 pub fn set_checked(&mut self, checked: bool) {
160 if checked && !self.on_state.is_empty() {
161 self.state.clone_from(&self.on_state);
162 } else {
163 self.state = OFF_STATE.to_string();
164 }
165 }
166
167 /// Flips the control. What a click on a check box does.
168 ///
169 /// ```
170 /// use pdfrum_form::field::{ToggleState, toggle::OFF_STATE};
171 ///
172 /// let mut state = ToggleState::new(OFF_STATE, "On");
173 /// state.toggle();
174 /// assert!(state.is_checked());
175 /// state.toggle();
176 /// assert!(!state.is_checked());
177 /// ```
178 pub fn toggle(&mut self) {
179 let checked = self.is_checked();
180 self.set_checked(!checked);
181 }
182}
183
184/// Which of the two controls a widget is, for the one line where they differ.
185///
186/// ```
187/// use pdfrum_form::field::{ToggleKind, ToggleState, activate, toggle::OFF_STATE};
188///
189/// // A check box flips; a radio button sets and never clears.
190/// let mut check = ToggleState::new(OFF_STATE, "On");
191/// activate(&mut check, ToggleKind::Check, false);
192/// activate(&mut check, ToggleKind::Check, false);
193/// assert!(!check.is_checked());
194///
195/// let mut radio = ToggleState::new(OFF_STATE, "On");
196/// activate(&mut radio, ToggleKind::Radio, false);
197/// activate(&mut radio, ToggleKind::Radio, false);
198/// assert!(radio.is_checked());
199/// ```
200#[derive(Debug, Clone, Copy, PartialEq, Eq)]
201pub enum ToggleKind {
202 /// A check box: activation flips it.
203 Check,
204 /// A radio button: activation sets it, and never clears it.
205 Radio,
206}
207
208/// Activates a control — what a click or a Return or Space does.
209///
210/// Returns whether the state moved. A read-only control never moves, but its
211/// caller still reports the event as consumed.
212///
213/// ```
214/// use pdfrum_form::field::{ToggleKind, ToggleState, activate, toggle::OFF_STATE};
215///
216/// let mut state = ToggleState::new(OFF_STATE, "On");
217/// assert!(activate(&mut state, ToggleKind::Check, false));
218/// assert!(state.is_checked());
219///
220/// // A radio button re-activated is a no-op, not a clear.
221/// let mut radio = ToggleState::new(OFF_STATE, "On");
222/// activate(&mut radio, ToggleKind::Radio, false);
223/// assert!(!activate(&mut radio, ToggleKind::Radio, false));
224/// assert!(radio.is_checked());
225///
226/// // A read-only control does not move.
227/// let mut fixed = ToggleState::new(OFF_STATE, "On");
228/// assert!(!activate(&mut fixed, ToggleKind::Check, true));
229/// assert!(!fixed.is_checked());
230/// ```
231pub fn activate(state: &mut ToggleState, kind: ToggleKind, read_only: bool) -> bool {
232 if read_only {
233 return false;
234 }
235 let before = state.state.clone();
236 match kind {
237 ToggleKind::Check => state.toggle(),
238 // A radio button sets. Clicking a selected one again leaves it
239 // selected; only a sibling can clear it.
240 ToggleKind::Radio => state.set_checked(true),
241 }
242 state.state != before
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248
249 fn check() -> ToggleState {
250 ToggleState::new(OFF_STATE, "On")
251 }
252
253 #[test]
254 fn a_check_box_flips_each_time_it_is_activated() {
255 let mut state = check();
256 assert!(!state.is_checked());
257
258 assert!(activate(&mut state, ToggleKind::Check, false));
259 assert!(state.is_checked());
260 assert_eq!(state.state, "On");
261
262 assert!(activate(&mut state, ToggleKind::Check, false));
263 assert!(!state.is_checked());
264 assert_eq!(state.state, OFF_STATE);
265 }
266
267 /// The asymmetry with a check box, and the whole reason the two share a
268 /// machine rather than a function.
269 #[test]
270 fn a_radio_button_cannot_be_cleared_by_activating_it_again() {
271 let mut state = check();
272 assert!(activate(&mut state, ToggleKind::Radio, false));
273 assert!(state.is_checked());
274
275 // Activating it again is a no-op, not a clear.
276 assert!(!activate(&mut state, ToggleKind::Radio, false));
277 assert!(state.is_checked());
278 }
279
280 /// The event is consumed by the caller; the state does not move. Both
281 /// halves are asserted upstream.
282 #[test]
283 fn a_read_only_control_does_not_move() {
284 let mut state = check();
285 assert!(!activate(&mut state, ToggleKind::Check, true));
286 assert!(!state.is_checked());
287
288 let mut radio = check();
289 assert!(!activate(&mut radio, ToggleKind::Radio, true));
290 assert!(!radio.is_checked());
291 }
292
293 #[test]
294 fn a_control_with_no_on_state_stays_clear() {
295 let mut state = ToggleState::new(OFF_STATE, "");
296 assert!(!activate(&mut state, ToggleKind::Check, false));
297 assert!(!state.is_checked());
298 }
299
300 /// A file may name the on state anything; "Off" is the only reserved
301 /// spelling.
302 #[test]
303 fn the_on_state_name_comes_from_the_file() {
304 let mut state = ToggleState::new(OFF_STATE, "Yes");
305 state.set_checked(true);
306 assert_eq!(state.state, "Yes");
307 assert!(state.is_checked());
308
309 state.set_checked(false);
310 assert_eq!(state.state, OFF_STATE);
311 }
312
313 /// An empty state is not a checked one — a widget with no appearance
314 /// state at all reads as clear rather than as ambiguous.
315 #[test]
316 fn an_empty_state_reads_as_clear() {
317 assert!(!ToggleState::new("", "On").is_checked());
318 }
319}