Skip to main content

pdfrum_form/edit/
select.rs

1//! The text selection: two places, stored directional.
2//!
3//! `begin` is the **anchor**, the end that stays put; `end` is the **active**
4//! end, the one the caret drags around. Nothing orders them on the way in, so
5//! a backwards selection is a legal, reachable state — a shift-left from the
6//! middle of a field produces one, and replacing it has to put the caret in
7//! the same place a forwards selection of the same span would.
8//!
9//! # The distinction that is easy to miss
10//!
11//! *Collapsed* and *reset* are different states and the difference is
12//! observable. After every mutation and after a mouse-down, the selection is
13//! set to `(caret, caret)`: it selects nothing, but it is a **live anchor**,
14//! so the next shift-arrow or drag extends from exactly there. A reset
15//! selection has no anchor at all, and a shift-arrow starting from one has to
16//! anchor on the caret's *previous* position instead. Both report
17//! `is_empty()`; only [`Selection::is_reset`] tells them apart.
18//!
19//! ```
20//! use pdfrum_form::edit::{Place, Selection};
21//!
22//! let collapsed = Selection::collapsed_at(Place::new(0, 0, Some(3)));
23//! let reset = Selection::reset();
24//!
25//! // Both select nothing, and both report empty.
26//! assert!(collapsed.is_empty());
27//! assert!(reset.is_empty());
28//! // Only one of them has an anchor for a shift-move to extend from.
29//! assert!(!collapsed.is_reset());
30//! assert!(reset.is_reset());
31//! ```
32
33use super::place::{Place, Range};
34
35/// A selection: an anchor and an active end, in that order, unordered by
36/// value.
37///
38/// ```
39/// use pdfrum_form::edit::{Place, Selection};
40///
41/// let lo = Place::new(0, 0, Some(1));
42/// let hi = Place::new(0, 0, Some(5));
43/// let forwards = Selection::new(lo, hi);
44/// let backwards = Selection::new(hi, lo);
45///
46/// // The direction is kept, so the two are not equal.
47/// assert!(!forwards.is_backwards());
48/// assert!(backwards.is_backwards());
49/// assert_ne!(forwards, backwards);
50/// // They still cover the same text.
51/// assert_eq!(forwards.range(), backwards.range());
52/// ```
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub struct Selection {
55    /// The fixed end.
56    pub begin: Place,
57    /// The end that moves with the caret.
58    pub end: Place,
59}
60
61impl Selection {
62    /// A collapsed selection at the start of the text.
63    ///
64    /// ```
65    /// use pdfrum_form::edit::{Place, PlaceExt, Selection};
66    ///
67    /// assert!(Selection::empty().is_empty());
68    /// assert_eq!(Selection::empty().begin, Place::start());
69    /// // An anchor at the start is still an anchor, so this is no reset.
70    /// assert!(!Selection::empty().is_reset());
71    /// ```
72    #[must_use]
73    pub fn empty() -> Selection {
74        Selection {
75            begin: Place::start(),
76            end: Place::start(),
77        }
78    }
79
80    /// A selection anchored at `begin`, active at `end`.
81    ///
82    /// ```
83    /// use pdfrum_form::edit::{Place, Selection};
84    ///
85    /// let sel = Selection::new(Place::new(0, 0, Some(7)), Place::new(0, 0, Some(2)));
86    /// // Nothing orders the two on the way in: this one runs backwards.
87    /// assert!(sel.is_backwards());
88    /// assert_eq!(sel.range().begin(), Place::new(0, 0, Some(2)));
89    /// ```
90    #[must_use]
91    pub fn new(begin: Place, end: Place) -> Selection {
92        Selection { begin, end }
93    }
94
95    /// A live anchor at one place, selecting nothing.
96    ///
97    /// What a mutation or a mouse-down leaves behind, and what a following
98    /// shift-move extends from.
99    ///
100    /// ```
101    /// use pdfrum_form::edit::{Place, Selection};
102    ///
103    /// let here = Place::new(0, 0, Some(3));
104    /// let sel = Selection::collapsed_at(here);
105    /// assert!(sel.is_empty());
106    /// assert!(!sel.is_reset(), "the anchor is live");
107    /// assert_eq!(sel.begin, here);
108    /// ```
109    #[must_use]
110    pub fn collapsed_at(place: Place) -> Selection {
111        Selection {
112            begin: place,
113            end: place,
114        }
115    }
116
117    /// No selection and no anchor.
118    ///
119    /// Distinct from a collapsed selection: a shift-move from here anchors on
120    /// the caret's previous position rather than on this one.
121    ///
122    /// ```
123    /// use pdfrum_form::edit::{Place, Selection};
124    ///
125    /// assert!(Selection::reset().is_reset());
126    /// assert_ne!(
127    ///     Selection::reset(),
128    ///     Selection::collapsed_at(Place::new(0, 0, Some(3)))
129    /// );
130    /// ```
131    #[must_use]
132    pub fn reset() -> Selection {
133        Selection {
134            begin: Place {
135                section: u32::MAX,
136                line: u32::MAX,
137                word: None,
138            },
139            end: Place {
140                section: u32::MAX,
141                line: u32::MAX,
142                word: None,
143            },
144        }
145    }
146
147    /// Whether the selection covers no text.
148    ///
149    /// Says nothing about whether an anchor exists; see
150    /// [`Selection::is_reset`].
151    ///
152    /// ```
153    /// use pdfrum_form::edit::{Place, Selection};
154    ///
155    /// let here = Place::new(0, 0, Some(3));
156    /// assert!(Selection::collapsed_at(here).is_empty());
157    /// assert!(!Selection::new(here, Place::new(0, 0, Some(5))).is_empty());
158    /// ```
159    #[must_use]
160    pub fn is_empty(self) -> bool {
161        self.begin == self.end
162    }
163
164    /// Whether the selection has no anchor at all.
165    ///
166    /// ```
167    /// use pdfrum_form::edit::{Place, Selection};
168    ///
169    /// assert!(Selection::reset().is_reset());
170    /// assert!(!Selection::collapsed_at(Place::new(0, 0, Some(3))).is_reset());
171    /// ```
172    #[must_use]
173    pub fn is_reset(self) -> bool {
174        self.begin
175            == Place {
176                section: u32::MAX,
177                line: u32::MAX,
178                word: None,
179            }
180            && self.end
181                == Place {
182                    section: u32::MAX,
183                    line: u32::MAX,
184                    word: None,
185                }
186    }
187
188    /// Whether the active end lies before the anchor.
189    ///
190    /// ```
191    /// use pdfrum_form::edit::{Place, Selection};
192    ///
193    /// let lo = Place::new(0, 0, Some(1));
194    /// let hi = Place::new(0, 0, Some(5));
195    /// assert!(Selection::new(hi, lo).is_backwards());
196    /// assert!(!Selection::new(lo, hi).is_backwards());
197    /// ```
198    #[must_use]
199    pub fn is_backwards(self) -> bool {
200        self.end < self.begin
201    }
202
203    /// The span, ordered. Direction is discarded here and nowhere else.
204    ///
205    /// ```
206    /// use pdfrum_form::edit::{Place, Selection};
207    ///
208    /// let sel = Selection::new(Place::new(0, 0, Some(7)), Place::new(0, 0, Some(2)));
209    /// assert!(sel.range().begin() <= sel.range().end());
210    /// assert_eq!(sel.range().begin(), Place::new(0, 0, Some(2)));
211    /// ```
212    #[must_use]
213    pub fn range(self) -> Range {
214        Range::new(self.begin, self.end)
215    }
216
217    /// Moves the active end, keeping the anchor. What a shift-move does once
218    /// an anchor exists.
219    ///
220    /// ```
221    /// use pdfrum_form::edit::{Place, Selection};
222    ///
223    /// let anchor = Place::new(0, 0, Some(1));
224    /// let mut sel = Selection::collapsed_at(anchor);
225    /// sel.set_active(Place::new(0, 0, Some(4)));
226    ///
227    /// assert_eq!(sel.begin, anchor, "the anchor stays put");
228    /// assert_eq!(sel.end, Place::new(0, 0, Some(4)));
229    /// assert!(!sel.is_empty());
230    /// ```
231    pub fn set_active(&mut self, place: Place) {
232        self.end = place;
233    }
234}
235
236impl Default for Selection {
237    fn default() -> Selection {
238        Selection::empty()
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    #[test]
247    fn a_selection_keeps_the_direction_it_was_built_with() {
248        let lo = Place::new(0, 0, Some(1));
249        let hi = Place::new(0, 0, Some(5));
250        let forwards = Selection::new(lo, hi);
251        let backwards = Selection::new(hi, lo);
252
253        assert!(!forwards.is_backwards());
254        assert!(backwards.is_backwards());
255        assert_ne!(forwards, backwards);
256        // …but they cover the same text.
257        assert_eq!(forwards.range(), backwards.range());
258    }
259
260    /// Both report empty; only one has an anchor to extend from.
261    #[test]
262    fn collapsed_and_reset_are_different_states() {
263        let here = Place::new(0, 0, Some(3));
264        let collapsed = Selection::collapsed_at(here);
265        let reset = Selection::reset();
266
267        assert!(collapsed.is_empty());
268        assert!(reset.is_empty());
269
270        assert!(!collapsed.is_reset());
271        assert!(reset.is_reset());
272        assert_ne!(collapsed, reset);
273    }
274
275    #[test]
276    fn extending_moves_only_the_active_end() {
277        let anchor = Place::new(0, 0, Some(1));
278        let mut sel = Selection::collapsed_at(anchor);
279        sel.set_active(Place::new(0, 0, Some(4)));
280
281        assert_eq!(sel.begin, anchor);
282        assert_eq!(sel.end, Place::new(0, 0, Some(4)));
283        assert!(!sel.is_empty());
284    }
285
286    #[test]
287    fn the_range_is_always_ordered() {
288        let sel = Selection::new(Place::new(0, 0, Some(7)), Place::new(0, 0, Some(2)));
289        assert!(sel.range().begin() <= sel.range().end());
290        assert_eq!(sel.range().begin(), Place::new(0, 0, Some(2)));
291    }
292}