inset_embedder/
text_editing.rs1use std::fmt;
6use std::hash::{Hash, Hasher};
7
8use crate::{TextAffinity, TextPosition, TextRange};
9
10#[derive(Clone, Copy, Debug)]
15pub struct TextSelection {
16 pub base_offset: i32,
20 pub extent_offset: i32,
28 pub affinity: TextAffinity,
31 pub is_directional: bool,
37}
38
39impl TextSelection {
40 pub const fn new(base_offset: i32, extent_offset: i32) -> TextSelection {
42 TextSelection {
43 base_offset,
44 extent_offset,
45 affinity: TextAffinity::Downstream,
46 is_directional: false,
47 }
48 }
49
50 pub const fn collapsed(offset: i32, affinity: TextAffinity) -> TextSelection {
55 TextSelection {
56 base_offset: offset,
57 extent_offset: offset,
58 affinity,
59 is_directional: false,
60 }
61 }
62
63 pub const fn from_position(position: TextPosition) -> TextSelection {
68 TextSelection {
69 base_offset: position.offset,
70 extent_offset: position.offset,
71 affinity: position.affinity,
72 is_directional: false,
73 }
74 }
75
76 pub fn range(&self) -> TextRange {
78 if self.base_offset < self.extent_offset {
79 TextRange::new(self.base_offset, self.extent_offset)
80 } else {
81 TextRange::new(self.extent_offset, self.base_offset)
82 }
83 }
84
85 pub fn start(&self) -> i32 {
87 self.range().start
88 }
89
90 pub fn end(&self) -> i32 {
92 self.range().end
93 }
94
95 pub fn is_valid(&self) -> bool {
97 self.range().is_valid()
98 }
99
100 pub fn is_collapsed(&self) -> bool {
102 self.range().is_collapsed()
103 }
104
105 pub fn is_normalized(&self) -> bool {
107 self.range().is_normalized()
108 }
109
110 pub fn base(&self) -> TextPosition {
120 let affinity = if !self.is_valid() || self.base_offset == self.extent_offset {
121 self.affinity
122 } else if self.base_offset < self.extent_offset {
123 TextAffinity::Downstream
124 } else {
125 TextAffinity::Upstream
126 };
127 TextPosition::with_affinity(self.base_offset, affinity)
128 }
129
130 pub fn extent(&self) -> TextPosition {
136 let affinity = if !self.is_valid() || self.base_offset == self.extent_offset {
137 self.affinity
138 } else if self.base_offset < self.extent_offset {
139 TextAffinity::Upstream
140 } else {
141 TextAffinity::Downstream
142 };
143 TextPosition::with_affinity(self.extent_offset, affinity)
144 }
145
146 pub fn copy_with(&self) -> TextSelection {
150 *self
151 }
152
153 pub fn base_offset(mut self, base_offset: i32) -> TextSelection {
155 self.base_offset = base_offset;
156 self
157 }
158
159 pub fn extent_offset(mut self, extent_offset: i32) -> TextSelection {
161 self.extent_offset = extent_offset;
162 self
163 }
164
165 pub fn affinity(mut self, affinity: TextAffinity) -> TextSelection {
167 self.affinity = affinity;
168 self
169 }
170
171 pub fn is_directional(mut self, is_directional: bool) -> TextSelection {
173 self.is_directional = is_directional;
174 self
175 }
176
177 pub fn expand_to(&self, position: TextPosition, extent_at_index: bool) -> TextSelection {
191 if position.offset >= self.start() && position.offset <= self.end() {
193 return *self;
194 }
195 let normalized = self.base_offset <= self.extent_offset;
196 if position.offset <= self.start() {
197 if extent_at_index {
199 return self
200 .copy_with()
201 .base_offset(self.end())
202 .extent_offset(position.offset)
203 .affinity(position.affinity);
204 }
205 return self
206 .copy_with()
207 .base_offset(if normalized {
208 position.offset
209 } else {
210 self.base_offset
211 })
212 .extent_offset(if normalized {
213 self.extent_offset
214 } else {
215 position.offset
216 });
217 }
218 if extent_at_index {
220 return self
221 .copy_with()
222 .base_offset(self.start())
223 .extent_offset(position.offset)
224 .affinity(position.affinity);
225 }
226 self.copy_with()
227 .base_offset(if normalized {
228 self.base_offset
229 } else {
230 position.offset
231 })
232 .extent_offset(if normalized {
233 position.offset
234 } else {
235 self.extent_offset
236 })
237 }
238
239 pub fn extend_to(&self, position: TextPosition) -> TextSelection {
245 if self.extent() == position {
248 return *self;
249 }
250 self.copy_with()
251 .extent_offset(position.offset)
252 .affinity(position.affinity)
253 }
254}
255
256impl From<TextSelection> for TextRange {
257 fn from(selection: TextSelection) -> TextRange {
258 selection.range()
259 }
260}
261
262impl PartialEq for TextSelection {
263 fn eq(&self, other: &TextSelection) -> bool {
264 if !self.is_valid() {
265 return !other.is_valid();
266 }
267 other.base_offset == self.base_offset
268 && other.extent_offset == self.extent_offset
269 && (!self.is_collapsed() || other.affinity == self.affinity)
270 && other.is_directional == self.is_directional
271 }
272}
273
274impl Eq for TextSelection {}
275
276impl Hash for TextSelection {
277 fn hash<H: Hasher>(&self, state: &mut H) {
278 if !self.is_valid() {
279 (-1i32).hash(state);
280 (-1i32).hash(state);
281 TextAffinity::Downstream.hash(state);
282 return;
283 }
284 self.base_offset.hash(state);
285 self.extent_offset.hash(state);
286 if self.is_collapsed() {
287 self.affinity.hash(state);
288 } else {
289 TextAffinity::Downstream.hash(state);
290 }
291 self.is_directional.hash(state);
292 }
293}
294
295impl fmt::Display for TextSelection {
296 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
297 if !self.is_valid() {
298 return write!(f, "TextSelection.invalid");
299 }
300 if self.is_collapsed() {
301 write!(
302 f,
303 "TextSelection.collapsed(offset: {}, affinity: {:?}, isDirectional: {})",
304 self.base_offset, self.affinity, self.is_directional
305 )
306 } else {
307 write!(
308 f,
309 "TextSelection(baseOffset: {}, extentOffset: {}, isDirectional: {})",
310 self.base_offset, self.extent_offset, self.is_directional
311 )
312 }
313 }
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319
320 #[test]
321 fn a_reversed_selection_normalizes_its_range() {
322 let selection = TextSelection::new(5, 2);
323 assert_eq!(selection.start(), 2);
324 assert_eq!(selection.end(), 5);
325 assert_eq!(selection.base().affinity, TextAffinity::Upstream);
326 assert_eq!(selection.extent().affinity, TextAffinity::Downstream);
327 assert!(!selection.is_collapsed());
328 }
329
330 #[test]
331 fn expand_to_grows_toward_the_position() {
332 let selection = TextSelection::new(2, 5);
333 assert_eq!(selection.expand_to(TextPosition::new(3), false), selection);
334 let before = selection.expand_to(TextPosition::new(0), false);
335 assert_eq!((before.base_offset, before.extent_offset), (0, 5));
336 let after = selection.expand_to(TextPosition::new(8), true);
337 assert_eq!((after.base_offset, after.extent_offset), (2, 8));
338 }
339
340 #[test]
341 fn extend_to_moves_the_extent_only() {
342 let selection = TextSelection::new(2, 5);
343 let extended = selection.extend_to(TextPosition::new(1));
344 assert_eq!((extended.base_offset, extended.extent_offset), (2, 1));
345 assert_eq!(selection.extend_to(selection.extent()), selection);
346 }
347
348 #[test]
349 fn invalid_selections_compare_equal() {
350 assert_eq!(
351 TextSelection::new(-1, -1),
352 TextSelection::collapsed(-1, TextAffinity::Upstream)
353 );
354 assert_eq!(
355 TextSelection::collapsed(3, TextAffinity::Upstream).to_string(),
356 "TextSelection.collapsed(offset: 3, affinity: Upstream, isDirectional: false)"
357 );
358 }
359}