Skip to main content

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}