pdfrum_form/update.rs
1//! What applying an event hands back.
2//!
3//! This is the crate's **entire** output channel: nothing is pushed at a
4//! caller. `apply` **returns** what changed, and the caller re-renders
5//! whatever it likes — which is what a golden comparison does anyway, since
6//! it diffs whole pages. Dirty rectangles describe pixels to someone else; a
7//! library that hands back appearance streams has nobody to describe them to.
8//!
9//! Actions come back as a **request** the caller may inspect, ignore or
10//! perform. A focus change is an ordinary observation in the returned list.
11
12use pdfrum_doc::GeneratedAp;
13use pdfrum_doc::nav::Action;
14
15use crate::event::Modifiers;
16use crate::session::AnnotId;
17
18/// One thing that changed, and what the caller may do about it.
19#[derive(Debug, Clone, PartialEq)]
20pub struct AppearanceUpdate {
21 /// Which annotation it concerns.
22 pub annot: AnnotId,
23 /// What happened to it.
24 pub kind: UpdateKind,
25}
26
27impl AppearanceUpdate {
28 /// An update about one annotation.
29 #[must_use]
30 pub fn new(annot: AnnotId, kind: UpdateKind) -> AppearanceUpdate {
31 AppearanceUpdate { annot, kind }
32 }
33}
34
35/// What happened to an annotation.
36#[derive(Debug, Clone, PartialEq)]
37pub enum UpdateKind {
38 /// A committed value produced a new appearance stream.
39 ///
40 /// The unfocused path: the field is drawn from what it now stores.
41 Regenerated(Box<GeneratedAp>),
42 /// A focused field's live editor state, with its caret and selection
43 /// band.
44 ///
45 /// The focused path. Which of these two a field takes is the whole seam
46 /// between appearance generation and interaction, and it is one `if`.
47 LiveEdit(Box<GeneratedAp>),
48 /// The widget has no generated appearance any more: it falls back to
49 /// the **file's own** `/AP`.
50 ///
51 /// # This is not "draw nothing"
52 ///
53 /// The distinction matters because the two are easy to conflate and only
54 /// one of them is expressible. A caller applies these updates by keying
55 /// an appearance overlay, and *absence* from that overlay already means
56 /// "use whatever the file declares". So this variant says: drop any
57 /// appearance an earlier event generated for this widget, and let the
58 /// file's own stream show through again.
59 ///
60 /// Suppressing a widget that has an `/AP` — painting nothing where the
61 /// file says something — is a different instruction, and this cannot
62 /// carry it: an overlay keyed by presence has no way to spell a positive
63 /// "blank". Nothing here needs it. A widget that genuinely draws nothing
64 /// is one whose field type gets no appearance at all — a signature, or a
65 /// type the classifier could not name — and those never receive
66 /// interaction state, so they never produce an update of any kind. Real
67 /// suppression would need a new variant *and* a positive marker in the
68 /// overlay, not a re-reading of this one.
69 RevertedToFileAppearance,
70 /// An action fired and the caller decides whether to perform it.
71 ///
72 /// The modifiers held when it fired ride along, because a link's action
73 /// is expected to see them.
74 ActionRequested {
75 /// What was asked for.
76 action: Box<Action>,
77 /// Which modifiers were held.
78 modifiers: Modifiers,
79 },
80 /// Focus moved.
81 ///
82 /// An observation rather than a callback, which is why there is no
83 /// version of this interface that omits it.
84 FocusChanged {
85 /// What had focus before, if anything.
86 from: Option<AnnotId>,
87 /// What has it now, if anything.
88 to: Option<AnnotId>,
89 },
90}
91
92impl UpdateKind {
93 /// Whether this update carries a new appearance stream, by either path.
94 #[must_use]
95 pub fn appearance(&self) -> Option<&GeneratedAp> {
96 match self {
97 UpdateKind::Regenerated(ap) | UpdateKind::LiveEdit(ap) => Some(ap),
98 UpdateKind::RevertedToFileAppearance
99 | UpdateKind::ActionRequested { .. }
100 | UpdateKind::FocusChanged { .. } => None,
101 }
102 }
103
104 /// Whether this update is the focused-field path.
105 #[must_use]
106 pub fn is_live_edit(&self) -> bool {
107 matches!(self, UpdateKind::LiveEdit(_))
108 }
109}
110
111/// What applying an event produced.
112///
113/// Both halves of the answer, because the oracle's single boolean conflates
114/// them: `consumed` says the event was handled — which is **not** the same as
115/// saying it had an effect, since a read-only control consumes a keystroke
116/// and does nothing — and `updates` says what changed.
117#[derive(Debug, Clone, Default, PartialEq)]
118pub struct Response {
119 /// Whether the event was handled.
120 pub consumed: bool,
121 /// What changed, in the order it changed.
122 pub updates: Vec<AppearanceUpdate>,
123}
124
125impl Response {
126 /// An event nothing handled.
127 #[must_use]
128 pub fn ignored() -> Response {
129 Response {
130 consumed: false,
131 updates: Vec::new(),
132 }
133 }
134
135 /// An event that was handled but changed nothing to draw.
136 ///
137 /// The answer a read-only control gives: it took the keystroke and
138 /// declined to act on it.
139 #[must_use]
140 pub fn consumed() -> Response {
141 Response {
142 consumed: true,
143 updates: Vec::new(),
144 }
145 }
146
147 /// An event that was handled and produced one update.
148 #[must_use]
149 pub fn one(update: AppearanceUpdate) -> Response {
150 Response {
151 consumed: true,
152 updates: vec![update],
153 }
154 }
155
156 /// An event that was handled and produced several updates.
157 ///
158 /// [`Response::one`] when there is only one.
159 #[must_use]
160 pub fn with(updates: Vec<AppearanceUpdate>) -> Response {
161 Response {
162 consumed: true,
163 updates,
164 }
165 }
166
167 /// Adds one update, keeping the order.
168 pub fn push(&mut self, update: AppearanceUpdate) {
169 self.updates.push(update);
170 }
171
172 /// Folds another response in, keeping both orders and consuming if either
173 /// did.
174 pub fn absorb(&mut self, other: Response) {
175 self.consumed |= other.consumed;
176 self.updates.extend(other.updates);
177 }
178
179 /// The actions the caller has been asked to perform, in order.
180 pub fn actions(&self) -> impl Iterator<Item = (&Action, Modifiers)> {
181 self.updates.iter().filter_map(|u| match &u.kind {
182 UpdateKind::ActionRequested { action, modifiers } => {
183 Some((action.as_ref(), *modifiers))
184 }
185 UpdateKind::Regenerated(_)
186 | UpdateKind::LiveEdit(_)
187 | UpdateKind::RevertedToFileAppearance
188 | UpdateKind::FocusChanged { .. } => None,
189 })
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196 use crate::session::AnnotId;
197 use pdfrum_object::Dict;
198
199 fn annot() -> AnnotId {
200 AnnotId::new(0, 3)
201 }
202
203 fn action() -> Action {
204 Action::new(Dict::new())
205 }
206
207 fn appearance() -> GeneratedAp {
208 GeneratedAp {
209 stream: Vec::new(),
210 bbox: kurbo::Rect::ZERO,
211 matrix: kurbo::Affine::IDENTITY,
212 resources: Dict::new(),
213 rect_override: None,
214 as_override: None,
215 }
216 }
217
218 /// Consumption and effect are independent, which is the distinction the
219 /// oracle's single boolean cannot make.
220 #[test]
221 fn an_event_can_be_consumed_without_changing_anything() {
222 let response = Response::consumed();
223 assert!(response.consumed);
224 assert!(response.updates.is_empty());
225
226 let ignored = Response::ignored();
227 assert!(!ignored.consumed);
228 assert!(ignored.updates.is_empty());
229 }
230
231 #[test]
232 fn folding_responses_keeps_both_orders() {
233 let mut first = Response::one(AppearanceUpdate::new(
234 annot(),
235 UpdateKind::RevertedToFileAppearance,
236 ));
237 let second = Response::one(AppearanceUpdate::new(
238 AnnotId::new(0, 4),
239 UpdateKind::FocusChanged {
240 from: None,
241 to: Some(AnnotId::new(0, 4)),
242 },
243 ));
244
245 first.absorb(second);
246 let order: Vec<AnnotId> = first.updates.iter().map(|u| u.annot).collect();
247 assert_eq!(order, vec![annot(), AnnotId::new(0, 4)]);
248 }
249
250 /// Folding an ignored response into a consuming one keeps it consuming.
251 #[test]
252 fn absorbing_an_ignored_response_does_not_un_consume() {
253 let mut consumed = Response::consumed();
254 consumed.absorb(Response::ignored());
255 assert!(consumed.consumed);
256
257 let mut ignored = Response::ignored();
258 ignored.absorb(Response::consumed());
259 assert!(ignored.consumed, "either half consuming is enough");
260 }
261
262 /// Actions come back as requests in order, with the modifiers that were
263 /// held — which is what a link's action expects to see.
264 #[test]
265 fn actions_come_back_in_order_with_their_modifiers() {
266 let mut response = Response::default();
267 for modifiers in [
268 Modifiers::NONE,
269 Modifiers::CONTROL,
270 Modifiers::SHIFT,
271 Modifiers::SHIFT | Modifiers::CONTROL,
272 ] {
273 response.push(AppearanceUpdate::new(
274 annot(),
275 UpdateKind::ActionRequested {
276 action: Box::new(action()),
277 modifiers,
278 },
279 ));
280 }
281
282 let seen: Vec<u32> = response.actions().map(|(_, m)| m.bits()).collect();
283 // The raw bit values are part of the contract, not an internal choice.
284 assert_eq!(seen, vec![0, 2, 1, 3]);
285 }
286
287 #[test]
288 fn a_response_with_no_actions_yields_none() {
289 let response = Response::one(AppearanceUpdate::new(
290 annot(),
291 UpdateKind::RevertedToFileAppearance,
292 ));
293 assert_eq!(response.actions().count(), 0);
294 }
295
296 /// Reverting carries no appearance, which is what makes it mean "use the
297 /// file's own" rather than "use this blank one".
298 #[test]
299 fn reverting_carries_no_appearance_of_its_own() {
300 let reverted = UpdateKind::RevertedToFileAppearance;
301 assert!(reverted.appearance().is_none());
302 assert!(!reverted.is_live_edit());
303
304 // And it is a distinct answer from generating one, which is the whole
305 // point: a caller keys an overlay by presence, so these two take
306 // different branches.
307 let generated = UpdateKind::Regenerated(Box::new(appearance()));
308 assert_ne!(reverted, generated);
309 assert!(generated.appearance().is_some());
310 }
311
312 /// The focused and unfocused paths are distinguishable, because which one
313 /// a field took is the question a golden difference asks.
314 #[test]
315 fn the_two_appearance_paths_are_distinguishable() {
316 let live = UpdateKind::LiveEdit(Box::new(appearance()));
317 let regenerated = UpdateKind::Regenerated(Box::new(appearance()));
318
319 assert!(live.is_live_edit());
320 assert!(!regenerated.is_live_edit());
321 assert!(live.appearance().is_some());
322 assert!(regenerated.appearance().is_some());
323 assert!(UpdateKind::RevertedToFileAppearance.appearance().is_none());
324 }
325}