Skip to main content

pdfrum_form/edit/
place.rs

1//! Positions in a laid-out text, and spans between them.
2//!
3//! **The caret invariant.** A [`Place`] names the position **after**
4//! character `word` of line `line` of section `section`, and `word == -1` is
5//! the *line header*, before that line's first character. Reading it as "at
6//! character `word`" is the easiest way to get this module wrong.
7//!
8//! The type is the layout engine's ([`pdfrum_doc::vt::hit::Place`]),
9//! re-exported rather than mirrored. What this module adds is the vocabulary
10//! the *editor* needs on top: the ordered [`Range`] and the predicates in
11//! [`PlaceExt`].
12//!
13//! ```
14//! use pdfrum_form::edit::{Place, PlaceExt, Range};
15//!
16//! // The header of the first line: before its first character, not at it.
17//! assert!(Place::start().at_line_start());
18//! assert_eq!(Place::start().word, None);
19//!
20//! // `word == Some(0)` is *after* the first character, and sorts later.
21//! assert!(Place::start() < Place::new(0, 0, Some(0)));
22//!
23//! // A span between two places is ordered however it is built.
24//! let span = Range::new(Place::new(0, 0, Some(5)), Place::new(0, 0, Some(1)));
25//! assert_eq!(span.begin(), Place::new(0, 0, Some(1)));
26//! ```
27
28pub use pdfrum_doc::vt::hit::Place;
29
30/// The editor's own questions about a place.
31///
32/// An extension trait because the type is the layout engine's: these are
33/// conveniences for the editing machinery, not part of what a place *is*.
34///
35/// ```
36/// use pdfrum_form::edit::{Place, PlaceExt};
37///
38/// // Places order lexicographically over (section, line, word), and the
39/// // header's `None` sorts before every character on its line.
40/// assert!(Place::new(0, 0, None) < Place::new(0, 0, Some(0)));
41/// assert!(Place::new(0, 0, Some(9)) < Place::new(0, 1, None));
42/// assert!(Place::new(0, 9, Some(9)) < Place::new(1, 0, None));
43/// ```
44pub trait PlaceExt {
45    /// The first place of any layout: the header of its first line.
46    ///
47    /// ```
48    /// use pdfrum_form::edit::{Place, PlaceExt};
49    ///
50    /// assert_eq!(Place::start(), Place::new(0, 0, None));
51    /// assert!(Place::start().at_line_start());
52    /// ```
53    #[must_use]
54    fn start() -> Place;
55
56    /// Whether this place sits before its line's first character.
57    ///
58    /// ```
59    /// use pdfrum_form::edit::{Place, PlaceExt};
60    ///
61    /// assert!(Place::new(0, 0, None).at_line_start());
62    /// // `Some(0)` is *after* the first character, so this is not the header.
63    /// assert!(!Place::new(0, 0, Some(0)).at_line_start());
64    /// ```
65    #[must_use]
66    fn at_line_start(self) -> bool;
67
68    /// Whether two places are on the same line, ignoring the character.
69    ///
70    /// The comparison movement and line selection need, and the one a derived
71    /// equality cannot give.
72    ///
73    /// ```
74    /// use pdfrum_form::edit::{Place, PlaceExt};
75    ///
76    /// assert!(Place::new(1, 2, Some(0)).same_line(Place::new(1, 2, Some(7))));
77    /// assert!(!Place::new(1, 2, Some(0)).same_line(Place::new(1, 3, Some(0))));
78    /// assert!(!Place::new(1, 2, Some(0)).same_line(Place::new(2, 2, Some(0))));
79    /// ```
80    #[must_use]
81    fn same_line(self, other: Place) -> bool;
82}
83
84impl PlaceExt for Place {
85    fn start() -> Place {
86        Place::new(0, 0, None)
87    }
88
89    fn at_line_start(self) -> bool {
90        self.word.is_none()
91    }
92
93    fn same_line(self, other: Place) -> bool {
94        self.section == other.section && self.line == other.line
95    }
96}
97
98/// An ordered span between two places, `begin <= end`.
99///
100/// Construction normalizes, so a `Range` can never run backwards. That is the
101/// point: every consumer of a span wants it ordered, and the one thing that
102/// wants direction — a selection — keeps its two places itself.
103///
104/// ```
105/// use pdfrum_form::edit::{Place, Range};
106///
107/// let lo = Place::new(0, 0, Some(1));
108/// let hi = Place::new(0, 0, Some(5));
109/// // Whichever order the two arrive in, the span is the same.
110/// assert_eq!(Range::new(lo, hi), Range::new(hi, lo));
111/// assert_eq!(Range::new(hi, lo).begin(), lo);
112/// assert_eq!(Range::new(hi, lo).end(), hi);
113/// ```
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub struct Range {
116    begin: Place,
117    end: Place,
118}
119
120impl Range {
121    /// The span between two places, in whichever order they arrive.
122    ///
123    /// ```
124    /// use pdfrum_form::edit::{Place, Range};
125    ///
126    /// let backwards = Range::new(Place::new(0, 0, Some(7)), Place::new(0, 0, Some(2)));
127    /// assert!(backwards.begin() <= backwards.end());
128    /// assert_eq!(backwards.begin(), Place::new(0, 0, Some(2)));
129    /// ```
130    #[must_use]
131    pub fn new(a: Place, b: Place) -> Range {
132        if a <= b {
133            Range { begin: a, end: b }
134        } else {
135            Range { begin: b, end: a }
136        }
137    }
138
139    /// An empty span at one place.
140    ///
141    /// ```
142    /// use pdfrum_form::edit::{Place, PlaceExt, Range};
143    ///
144    /// assert!(Range::empty_at(Place::start()).is_empty());
145    /// ```
146    #[must_use]
147    pub fn empty_at(place: Place) -> Range {
148        Range {
149            begin: place,
150            end: place,
151        }
152    }
153
154    /// The earlier end.
155    ///
156    /// ```
157    /// use pdfrum_form::edit::{Place, Range};
158    ///
159    /// let span = Range::new(Place::new(0, 0, Some(5)), Place::new(0, 0, Some(1)));
160    /// assert_eq!(span.begin(), Place::new(0, 0, Some(1)));
161    /// ```
162    #[must_use]
163    pub fn begin(self) -> Place {
164        self.begin
165    }
166
167    /// The later end.
168    ///
169    /// ```
170    /// use pdfrum_form::edit::{Place, Range};
171    ///
172    /// let span = Range::new(Place::new(0, 0, Some(5)), Place::new(0, 0, Some(1)));
173    /// assert_eq!(span.end(), Place::new(0, 0, Some(5)));
174    /// ```
175    #[must_use]
176    pub fn end(self) -> Place {
177        self.end
178    }
179
180    /// Whether the span covers nothing.
181    ///
182    /// ```
183    /// use pdfrum_form::edit::{Place, PlaceExt, Range};
184    ///
185    /// assert!(Range::empty_at(Place::start()).is_empty());
186    /// assert!(!Range::new(Place::start(), Place::new(0, 0, Some(0))).is_empty());
187    /// ```
188    #[must_use]
189    pub fn is_empty(self) -> bool {
190        self.begin == self.end
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    #[test]
199    fn places_order_lexicographically_over_the_triple() {
200        // `None < Some(0)` is what the old `word: -1` bought by being
201        // negative, and `Option`'s derived `Ord` gives it for free — which is
202        // the whole argument for the replacement.
203        assert!(Place::new(0, 0, None) < Place::new(0, 0, Some(0)));
204        assert!(Place::new(0, 0, Some(9)) < Place::new(0, 1, None));
205        assert!(Place::new(0, 9, Some(9)) < Place::new(1, 0, None));
206    }
207
208    #[test]
209    fn the_start_is_before_the_first_character() {
210        assert!(Place::start().at_line_start());
211        assert_eq!(Place::start().word, None);
212        assert!(!Place::new(0, 0, Some(0)).at_line_start());
213    }
214
215    #[test]
216    fn same_line_ignores_the_character() {
217        assert!(Place::new(1, 2, Some(0)).same_line(Place::new(1, 2, Some(7))));
218        assert!(!Place::new(1, 2, Some(0)).same_line(Place::new(1, 3, Some(0))));
219        assert!(!Place::new(1, 2, Some(0)).same_line(Place::new(2, 2, Some(0))));
220    }
221
222    #[test]
223    fn a_range_normalizes_whichever_way_it_is_built() {
224        let lo = Place::new(0, 0, Some(1));
225        let hi = Place::new(0, 0, Some(5));
226        assert_eq!(Range::new(lo, hi), Range::new(hi, lo));
227        assert_eq!(Range::new(hi, lo).begin(), lo);
228        assert_eq!(Range::new(hi, lo).end(), hi);
229    }
230
231    #[test]
232    fn an_empty_range_covers_nothing() {
233        assert!(Range::empty_at(Place::start()).is_empty());
234        assert!(!Range::new(Place::start(), Place::new(0, 0, Some(0))).is_empty());
235    }
236}