Skip to main content

iced_selection/
selection.rs

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