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 changed something.
148 #[must_use]
149 pub fn with(updates: Vec<AppearanceUpdate>) -> Response {
150 Response {
151 consumed: true,
152 updates,
153 }
154 }
155
156 /// Adds one update, keeping the order.
157 pub fn push(&mut self, update: AppearanceUpdate) {
158 self.updates.push(update);
159 }
160
161 /// Folds another response in, keeping both orders and consuming if either
162 /// did.
163 pub fn absorb(&mut self, other: Response) {
164 self.consumed |= other.consumed;
165 self.updates.extend(other.updates);
166 }
167
168 /// The actions the caller has been asked to perform, in order.
169 pub fn actions(&self) -> impl Iterator<Item = (&Action, Modifiers)> {
170 self.updates.iter().filter_map(|u| match &u.kind {
171 UpdateKind::ActionRequested { action, modifiers } => {
172 Some((action.as_ref(), *modifiers))
173 }
174 UpdateKind::Regenerated(_)
175 | UpdateKind::LiveEdit(_)
176 | UpdateKind::RevertedToFileAppearance
177 | UpdateKind::FocusChanged { .. } => None,
178 })
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185 use crate::session::AnnotId;
186 use pdfrum_object::Dict;
187
188 fn annot() -> AnnotId {
189 AnnotId::new(0, 3)
190 }
191
192 fn action() -> Action {
193 Action::new(Dict::new())
194 }
195
196 fn appearance() -> GeneratedAp {
197 GeneratedAp {
198 stream: Vec::new(),
199 bbox: kurbo::Rect::ZERO,
200 matrix: kurbo::Affine::IDENTITY,
201 resources: Dict::new(),
202 rect_override: None,
203 as_override: None,
204 }
205 }
206
207 /// Consumption and effect are independent, which is the distinction the
208 /// oracle's single boolean cannot make.
209 #[test]
210 fn an_event_can_be_consumed_without_changing_anything() {
211 let response = Response::consumed();
212 assert!(response.consumed);
213 assert!(response.updates.is_empty());
214
215 let ignored = Response::ignored();
216 assert!(!ignored.consumed);
217 assert!(ignored.updates.is_empty());
218 }
219
220 #[test]
221 fn folding_responses_keeps_both_orders() {
222 let mut first = Response::with(vec![AppearanceUpdate::new(
223 annot(),
224 UpdateKind::RevertedToFileAppearance,
225 )]);
226 let second = Response::with(vec![AppearanceUpdate::new(
227 AnnotId::new(0, 4),
228 UpdateKind::FocusChanged {
229 from: None,
230 to: Some(AnnotId::new(0, 4)),
231 },
232 )]);
233
234 first.absorb(second);
235 let order: Vec<AnnotId> = first.updates.iter().map(|u| u.annot).collect();
236 assert_eq!(order, vec![annot(), AnnotId::new(0, 4)]);
237 }
238
239 /// Folding an ignored response into a consuming one keeps it consuming.
240 #[test]
241 fn absorbing_an_ignored_response_does_not_un_consume() {
242 let mut consumed = Response::consumed();
243 consumed.absorb(Response::ignored());
244 assert!(consumed.consumed);
245
246 let mut ignored = Response::ignored();
247 ignored.absorb(Response::consumed());
248 assert!(ignored.consumed, "either half consuming is enough");
249 }
250
251 /// Actions come back as requests in order, with the modifiers that were
252 /// held — which is what a link's action expects to see.
253 #[test]
254 fn actions_come_back_in_order_with_their_modifiers() {
255 let mut response = Response::default();
256 for modifiers in [
257 Modifiers::NONE,
258 Modifiers::CONTROL,
259 Modifiers::SHIFT,
260 Modifiers::SHIFT | Modifiers::CONTROL,
261 ] {
262 response.push(AppearanceUpdate::new(
263 annot(),
264 UpdateKind::ActionRequested {
265 action: Box::new(action()),
266 modifiers,
267 },
268 ));
269 }
270
271 let seen: Vec<u32> = response.actions().map(|(_, m)| m.bits()).collect();
272 // The raw bit values are part of the contract, not an internal choice.
273 assert_eq!(seen, vec![0, 2, 1, 3]);
274 }
275
276 #[test]
277 fn a_response_with_no_actions_yields_none() {
278 let response = Response::with(vec![AppearanceUpdate::new(
279 annot(),
280 UpdateKind::RevertedToFileAppearance,
281 )]);
282 assert_eq!(response.actions().count(), 0);
283 }
284
285 /// Reverting carries no appearance, which is what makes it mean "use the
286 /// file's own" rather than "use this blank one".
287 #[test]
288 fn reverting_carries_no_appearance_of_its_own() {
289 let reverted = UpdateKind::RevertedToFileAppearance;
290 assert!(reverted.appearance().is_none());
291 assert!(!reverted.is_live_edit());
292
293 // And it is a distinct answer from generating one, which is the whole
294 // point: a caller keys an overlay by presence, so these two take
295 // different branches.
296 let generated = UpdateKind::Regenerated(Box::new(appearance()));
297 assert_ne!(reverted, generated);
298 assert!(generated.appearance().is_some());
299 }
300
301 /// The focused and unfocused paths are distinguishable, because which one
302 /// a field took is the question a golden difference asks.
303 #[test]
304 fn the_two_appearance_paths_are_distinguishable() {
305 let live = UpdateKind::LiveEdit(Box::new(appearance()));
306 let regenerated = UpdateKind::Regenerated(Box::new(appearance()));
307
308 assert!(live.is_live_edit());
309 assert!(!regenerated.is_live_edit());
310 assert!(live.appearance().is_some());
311 assert!(regenerated.appearance().is_some());
312 assert!(UpdateKind::RevertedToFileAppearance.appearance().is_none());
313 }
314}