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}