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 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}