iced_selection/
selection.rs

1//! Provides a [`Selection`] type for working with text selections in [`Paragraph`].
2//!
3//! [`Paragraph`]: https://docs.iced.rs/iced_graphics/text/paragraph/struct.Paragraph.html
4
5use std::cmp::Ordering;
6
7use iced_widget::{graphics::text::Paragraph, text_input::Value};
8
9/// The direction of a selection.
10#[derive(Debug, Default, Clone, Copy, PartialEq)]
11#[allow(missing_docs)]
12pub enum Direction {
13    Left,
14    #[default]
15    Right,
16}
17
18/// A text selection.
19#[derive(Debug, Default, Clone, Copy, PartialEq)]
20pub struct Selection {
21    /// The start of the selection.
22    pub start: SelectionEnd,
23    /// The end of the selection.
24    pub end: SelectionEnd,
25    /// The last direction of the selection.
26    pub direction: Direction,
27    moving_line_index: Option<usize>,
28}
29
30/// One of the ends of a [`Selection`].
31///
32/// Note that the index refers to [`graphemes`], not glyphs or bytes.
33///
34/// [`graphemes`]: https://docs.rs/unicode-segmentation/latest/unicode_segmentation/trait.UnicodeSegmentation.html#tymethod.graphemes
35#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
36#[allow(missing_docs)]
37pub struct SelectionEnd {
38    pub line: usize,
39    pub index: usize,
40}
41
42impl SelectionEnd {
43    /// Creates a new [`SelectionEnd`].
44    pub fn new(line: usize, index: usize) -> Self {
45        Self { line, index }
46    }
47}
48
49impl PartialOrd for SelectionEnd {
50    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
51        Some(self.cmp(other))
52    }
53}
54
55impl Ord for SelectionEnd {
56    fn cmp(&self, other: &Self) -> Ordering {
57        self.line
58            .cmp(&other.line)
59            .then(self.index.cmp(&other.index))
60    }
61}
62
63impl Selection {
64    /// Creates a new empty [`Selection`].
65    pub fn new() -> Self {
66        Self::default()
67    }
68
69    /// A selection is empty when the start and end are the same.
70    pub fn is_empty(&self) -> bool {
71        self.start == self.end
72    }
73
74    /// Returns the selected text from the given [`Paragraph`].
75    ///
76    /// [`Paragraph`]: https://docs.iced.rs/iced_graphics/text/paragraph/struct.Paragraph.html
77    pub fn text(&self, paragraph: &Paragraph) -> String {
78        let Selection { start, end, .. } = *self;
79
80        let mut value = String::new();
81        let buffer_lines = &paragraph.buffer().lines;
82        let lines_total = end.line - start.line + 1;
83
84        for (idx, line) in buffer_lines
85            .iter()
86            .skip(start.line)
87            .enumerate()
88            .take(lines_total)
89        {
90            let text = Value::new(line.text());
91            let length = text.len();
92
93            if idx == 0 {
94                if lines_total == 1 {
95                    value.push_str(
96                        &text
97                            .select(
98                                start.index.min(length),
99                                end.index.min(length),
100                            )
101                            .to_string(),
102                    );
103                } else {
104                    value.push_str(
105                        &text
106                            .select(start.index.min(length), length)
107                            .to_string(),
108                    );
109                    value.push_str(line.ending().as_str());
110                }
111            } else if idx == lines_total - 1 {
112                value.push_str(&text.until(end.index.min(length)).to_string());
113            } else {
114                value.push_str(&text.to_string());
115                value.push_str(line.ending().as_str());
116            }
117        }
118
119        value
120    }
121
122    /// Returns the currently active [`SelectionEnd`].
123    ///
124    /// `self.end` if `self.direction` is [`Right`], `self.start` otherwise.
125    ///
126    /// [`Right`]: Direction::Right
127    pub fn active_end(&self) -> SelectionEnd {
128        if self.direction == Direction::Right {
129            self.end
130        } else {
131            self.start
132        }
133    }
134
135    /// Select a new range.
136    ///
137    /// `self.start` will be set to the smaller value, `self.end` to the larger.
138    ///
139    /// # Example
140    ///
141    /// ```
142    /// use iced_selection::selection::{Selection, SelectionEnd};
143    ///
144    /// let mut selection = Selection::default();
145    ///
146    /// let start = SelectionEnd::new(5, 17);
147    /// let end = SelectionEnd::new(2, 8);
148    ///
149    /// selection.select_range(start, end);
150    ///
151    /// assert_eq!(selection.start, end);
152    /// assert_eq!(selection.end, start);
153    /// ```
154    pub fn select_range(&mut self, start: SelectionEnd, end: SelectionEnd) {
155        self.start = start.min(end);
156        self.end = end.max(start);
157    }
158
159    /// Updates the current selection by setting a new end point.
160    ///
161    /// This method adjusts the selection range based on the provided `new_end` position. The
162    /// current [`Direction`] is used to determine the new values:
163    ///
164    /// - If the current direction is [`Right`] (i.e., the selection goes from `start` to `end`), the
165    ///   range becomes `(start, new_end)`. If `new_end` is before `start`, the direction is flipped to [`Left`].
166    ///
167    /// - If it's [`Left`], the range becomes `(new_end, end)`. If `new_end` is after `end`, the
168    ///   direction is flipped to [`Right`].
169    ///
170    /// # Example
171    ///
172    /// ```
173    /// use iced_selection::selection::{Direction, Selection, SelectionEnd};
174    ///
175    /// let mut selection = Selection::default();
176    ///
177    /// let start = SelectionEnd::new(5, 17);
178    /// let end = SelectionEnd::new(2, 8);
179    ///
180    /// selection.select_range(start, end);
181    ///
182    /// assert_eq!(selection.start, end);
183    /// assert_eq!(selection.end, start);
184    /// assert_eq!(selection.direction, Direction::Right);
185    ///
186    /// let new_end = SelectionEnd::new(2, 2);
187    ///
188    /// selection.change_selection(new_end);
189    ///
190    /// assert_eq!(selection.start, new_end);
191    /// assert_eq!(selection.end, end);
192    /// assert_eq!(selection.direction, Direction::Left);
193    /// ```
194    ///
195    /// [`Left`]: Direction::Left
196    /// [`Right`]: Direction::Right
197    pub fn change_selection(&mut self, new_end: SelectionEnd) {
198        let (start, end) = if self.direction == Direction::Right {
199            if new_end < self.start {
200                self.direction = Direction::Left;
201            }
202
203            (self.start, new_end)
204        } else {
205            if new_end > self.end {
206                self.direction = Direction::Right;
207            }
208
209            (new_end, self.end)
210        };
211
212        self.moving_line_index = None;
213        self.select_range(start, end);
214    }
215
216    /// Updates the current selection by setting a new end point, either to the start of the
217    /// previous word, or to the next one's end.
218    pub fn change_selection_by_word(
219        &mut self,
220        new_end: SelectionEnd,
221        paragraph: &Paragraph,
222    ) {
223        let (base_word_start, base_word_end) = {
224            if self.direction == Direction::Right {
225                let value = Value::new(
226                    paragraph.buffer().lines[self.start.line].text(),
227                );
228
229                let end = SelectionEnd::new(
230                    self.start.line,
231                    value.next_end_of_word(self.start.index),
232                );
233
234                (self.start, end)
235            } else {
236                let value =
237                    Value::new(paragraph.buffer().lines[self.end.line].text());
238
239                let start = SelectionEnd::new(
240                    self.end.line,
241                    value.previous_start_of_word(self.end.index),
242                );
243
244                (start, self.end)
245            }
246        };
247
248        let value = Value::new(paragraph.buffer().lines[new_end.line].text());
249
250        let (start, end) = if new_end < self.start {
251            self.direction = Direction::Left;
252
253            let start = SelectionEnd::new(
254                new_end.line,
255                value.previous_start_of_word(new_end.index),
256            );
257
258            (start, base_word_end)
259        } else if new_end > self.end {
260            self.direction = Direction::Right;
261
262            let end = SelectionEnd::new(
263                new_end.line,
264                value.next_end_of_word(new_end.index),
265            );
266
267            (base_word_start, end)
268        } else if self.direction == Direction::Right {
269            let end = SelectionEnd::new(
270                new_end.line,
271                value.next_end_of_word(new_end.index),
272            );
273
274            (base_word_start, end.max(base_word_end))
275        } else {
276            let start = SelectionEnd::new(
277                new_end.line,
278                value.previous_start_of_word(new_end.index),
279            );
280
281            (start.min(base_word_start), base_word_end)
282        };
283
284        self.moving_line_index = None;
285        self.select_range(start, end);
286    }
287
288    /// Updates the current selection by setting a new end point, either to the end of a following
289    /// line, or the beginning of a previous one.
290    pub fn change_selection_by_line(
291        &mut self,
292        new_line: usize,
293        paragraph: &Paragraph,
294    ) {
295        if self.active_end().line == new_line {
296            return;
297        }
298
299        let old_direction = self.direction;
300
301        if new_line < self.start.line {
302            self.direction = Direction::Left;
303        } else if new_line > self.end.line {
304            self.direction = Direction::Right;
305        }
306
307        let (start, end) = if self.direction == Direction::Right {
308            let value = Value::new(paragraph.buffer().lines[new_line].text());
309
310            let start = if self.direction == old_direction {
311                self.start
312            } else {
313                SelectionEnd::new(self.end.line, 0)
314            };
315
316            let end = SelectionEnd::new(new_line, value.len());
317
318            (start, end)
319        } else {
320            let start = SelectionEnd::new(new_line, 0);
321
322            let end = if self.direction == old_direction {
323                self.end
324            } else {
325                let value = Value::new(
326                    paragraph.buffer().lines[self.start.line].text(),
327                );
328
329                SelectionEnd::new(self.start.line, value.len())
330            };
331
332            (start, end)
333        };
334
335        self.moving_line_index = None;
336        self.select_range(start, end);
337    }
338
339    /// Selects the word around the given grapheme position.
340    pub fn select_word(
341        &mut self,
342        line: usize,
343        index: usize,
344        paragraph: &Paragraph,
345    ) {
346        let value = Value::new(paragraph.buffer().lines[line].text());
347
348        let start =
349            SelectionEnd::new(line, value.previous_start_of_word(index));
350        let end = SelectionEnd::new(line, value.next_end_of_word(index));
351
352        self.select_range(start, end);
353    }
354
355    /// Moves the active [`SelectionEnd`] to the left by one, wrapping to the previous line if
356    /// possible and required.
357    pub fn select_left(&mut self, paragraph: &Paragraph) {
358        let mut active_end = self.active_end();
359
360        if active_end.index > 0 {
361            active_end.index -= 1;
362
363            self.change_selection(active_end);
364        } else if active_end.line > 0 {
365            active_end.line -= 1;
366
367            let value =
368                Value::new(paragraph.buffer().lines[active_end.line].text());
369            active_end.index = value.len();
370
371            self.change_selection(active_end);
372        }
373    }
374
375    /// Moves the active [`SelectionEnd`] to the right by one, wrapping to the next line if
376    /// possible and required.
377    pub fn select_right(&mut self, paragraph: &Paragraph) {
378        let mut active_end = self.active_end();
379
380        let lines = &paragraph.buffer().lines;
381        let value = Value::new(lines[active_end.line].text());
382
383        if active_end.index < value.len() {
384            active_end.index += 1;
385
386            self.change_selection(active_end);
387        } else if active_end.line < lines.len() - 1 {
388            active_end.line += 1;
389            active_end.index = 0;
390
391            self.change_selection(active_end);
392        }
393    }
394
395    /// Moves the active [`SelectionEnd`] up by one, keeping track of the original grapheme index.
396    pub fn select_up(&mut self, paragraph: &Paragraph) {
397        let mut active_end = self.active_end();
398
399        if active_end.line == 0 {
400            active_end.index = 0;
401
402            self.change_selection(active_end);
403        } else {
404            active_end.line -= 1;
405
406            let mut moving_line_index = None;
407
408            if let Some(index) = self.moving_line_index.take() {
409                active_end.index = index;
410            }
411
412            let value =
413                Value::new(paragraph.buffer().lines[active_end.line].text());
414            if active_end.index > value.len() {
415                moving_line_index = Some(active_end.index);
416                active_end.index = value.len();
417            }
418
419            self.change_selection(active_end);
420            self.moving_line_index = moving_line_index;
421        }
422    }
423
424    /// Moves the active [`SelectionEnd`] down by one, keeping track of the original grapheme index.
425    pub fn select_down(&mut self, paragraph: &Paragraph) {
426        let mut active_end = self.active_end();
427
428        let lines = &paragraph.buffer().lines;
429        let value = Value::new(lines[active_end.line].text());
430
431        if active_end.line == lines.len() - 1 {
432            active_end.index = value.len();
433
434            self.change_selection(active_end);
435        } else {
436            active_end.line += 1;
437
438            let mut moving_line_index = None;
439
440            if let Some(index) = self.moving_line_index.take() {
441                active_end.index = index;
442            }
443
444            let value =
445                Value::new(paragraph.buffer().lines[active_end.line].text());
446            if active_end.index > value.len() {
447                moving_line_index = Some(active_end.index);
448                active_end.index = value.len();
449            }
450
451            self.change_selection(active_end);
452            self.moving_line_index = moving_line_index;
453        }
454    }
455
456    /// Moves the active [`SelectionEnd`] to the previous start of a word on its current line, or
457    /// the previous line if it exists and `index == 0`.
458    pub fn select_left_by_words(&mut self, paragraph: &Paragraph) {
459        let mut active_end = self.active_end();
460
461        if active_end.index == 1 {
462            active_end.index = 0;
463
464            self.change_selection(active_end);
465        } else if active_end.index > 1 {
466            let value =
467                Value::new(paragraph.buffer().lines[active_end.line].text());
468            active_end.index = value.previous_start_of_word(active_end.index);
469
470            self.change_selection(active_end);
471        } else if active_end.line > 0 {
472            active_end.line -= 1;
473
474            let value =
475                Value::new(paragraph.buffer().lines[active_end.line].text());
476            active_end.index = value.previous_start_of_word(value.len());
477
478            self.change_selection(active_end);
479        }
480    }
481
482    /// Moves the active [`SelectionEnd`] to the next end of a word on its current line, or
483    /// the next line if it exists and `index == line.len()`.
484    pub fn select_right_by_words(&mut self, paragraph: &Paragraph) {
485        let mut active_end = self.active_end();
486
487        let lines = &paragraph.buffer().lines;
488        let value = Value::new(lines[active_end.line].text());
489
490        if value.len() - active_end.index == 1 {
491            active_end.index = value.len();
492
493            self.change_selection(active_end);
494        } else if active_end.index < value.len() {
495            active_end.index = value.next_end_of_word(active_end.index);
496
497            self.change_selection(active_end);
498        } else if active_end.line < lines.len() - 1 {
499            active_end.line += 1;
500
501            let value = Value::new(lines[active_end.line].text());
502            active_end.index = value.next_end_of_word(0);
503
504            self.change_selection(active_end);
505        }
506    }
507
508    /// Moves the active [`SelectionEnd`] to the beginning of its current line.
509    pub fn select_line_beginning(&mut self) {
510        let mut active_end = self.active_end();
511
512        if active_end.index > 0 {
513            active_end.index = 0;
514
515            self.change_selection(active_end);
516        }
517    }
518
519    /// Moves the active [`SelectionEnd`] to the end of its current line.
520    pub fn select_line_end(&mut self, paragraph: &Paragraph) {
521        let mut active_end = self.active_end();
522
523        let value =
524            Value::new(paragraph.buffer().lines[active_end.line].text());
525
526        if active_end.index < value.len() {
527            active_end.index = value.len();
528
529            self.change_selection(active_end);
530        }
531    }
532
533    /// Moves the active [`SelectionEnd`] to the beginning of the [`Paragraph`].
534    ///
535    /// [`Paragraph`]: https://docs.iced.rs/iced_graphics/text/paragraph/struct.Paragraph.html
536    pub fn select_beginning(&mut self) {
537        self.change_selection(SelectionEnd::new(0, 0));
538    }
539
540    /// Moves the active [`SelectionEnd`] to the end of the [`Paragraph`].
541    ///
542    /// [`Paragraph`]: https://docs.iced.rs/iced_graphics/text/paragraph/struct.Paragraph.html
543    pub fn select_end(&mut self, paragraph: &Paragraph) {
544        let lines = &paragraph.buffer().lines;
545        let value = Value::new(lines[lines.len() - 1].text());
546
547        let new_end = SelectionEnd::new(lines.len() - 1, value.len());
548
549        self.change_selection(new_end);
550    }
551
552    /// Selects an entire line.
553    pub fn select_line(&mut self, line: usize, paragraph: &Paragraph) {
554        let value = Value::new(paragraph.buffer().lines[line].text());
555
556        let start = SelectionEnd::new(line, 0);
557        let end = SelectionEnd::new(line, value.len());
558
559        self.select_range(start, end);
560    }
561
562    /// Selects the entire [`Paragraph`].
563    ///
564    /// [`Paragraph`]: https://docs.iced.rs/iced_graphics/text/paragraph/struct.Paragraph.html
565    pub fn select_all(&mut self, paragraph: &Paragraph) {
566        let line = paragraph.buffer().lines.len() - 1;
567        let index = Value::new(paragraph.buffer().lines[line].text()).len();
568
569        let end = SelectionEnd::new(line, index);
570
571        self.select_range(SelectionEnd::new(0, 0), end);
572    }
573}