diff options
| author | Polesznyák Márk <contact@pml68.dev> | 2025-10-18 01:43:15 +0200 |
|---|---|---|
| committer | Polesznyák Márk <contact@pml68.dev> | 2025-10-18 02:48:55 +0200 |
| commit | d8b724701dde52a17daf9874e8bbcf2a64ac7d7a (patch) | |
| tree | 7f0b587613473376a3d88e3794ceb2391cff4b65 /src/selection.rs | |
| download | iced_selection-d8b724701dde52a17daf9874e8bbcf2a64ac7d7a.tar.gz | |
feat: initial commit
Diffstat (limited to '')
| -rw-r--r-- | src/selection.rs | 436 |
1 files changed, 436 insertions, 0 deletions
diff --git a/src/selection.rs b/src/selection.rs new file mode 100644 index 0000000..32c8807 --- /dev/null +++ b/src/selection.rs @@ -0,0 +1,436 @@ +//! Provides a [`Selection`] type for working with text selections in [`Paragraph`]. +//! +//! [`Paragraph`]: https://docs.iced.rs/iced_graphics/text/paragraph/struct.Paragraph.html + +use std::cmp::Ordering; + +use iced_widget::{graphics::text::Paragraph, text_input::Value}; + +/// The direction of a selection. +#[derive(Debug, Default, Clone, Copy, PartialEq)] +#[allow(missing_docs)] +pub enum Direction { + Left, + #[default] + Right, +} + +/// A text selection. +#[derive(Debug, Default, Clone, Copy, PartialEq)] +pub struct Selection { + /// The start of the selection. + pub start: SelectionEnd, + /// The end of the selection. + pub end: SelectionEnd, + /// The last direction of the selection. + pub direction: Direction, + moving_line_index: Option<usize>, +} + +/// One of the ends of a [`Selection`]. +/// +/// Note that the index refers to [`graphemes`], not glyphs or bytes. +/// +/// [`graphemes`]: https://docs.rs/unicode-segmentation/latest/unicode_segmentation/trait.UnicodeSegmentation.html#tymethod.graphemes +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +#[allow(missing_docs)] +pub struct SelectionEnd { + pub line: usize, + pub index: usize, +} + +impl SelectionEnd { + /// Creates a new [`SelectionEnd`]. + pub fn new(line: usize, index: usize) -> Self { + Self { line, index } + } +} + +impl PartialOrd for SelectionEnd { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + Some(self.cmp(other)) + } +} + +impl Ord for SelectionEnd { + fn cmp(&self, other: &Self) -> Ordering { + self.line + .cmp(&other.line) + .then(self.index.cmp(&other.index)) + } +} + +impl Selection { + /// Creates a new empty [`Selection`]. + pub fn new() -> Self { + Self::default() + } + + /// A selection is empty when the start and end are the same. + pub fn is_empty(&self) -> bool { + self.start == self.end + } + + /// Returns the selected text from the given [`Paragraph`]. + /// + /// [`Paragraph`]: https://docs.iced.rs/iced_graphics/text/paragraph/struct.Paragraph.html + pub fn text(&self, paragraph: &Paragraph) -> String { + let Selection { start, end, .. } = *self; + + let mut value = String::new(); + let buffer_lines = ¶graph.buffer().lines; + let lines_total = end.line - start.line + 1; + + for (idx, line) in buffer_lines.iter().enumerate().take(lines_total) { + let text = line.text(); + let length = text.len(); + + if idx == 0 { + if lines_total == 1 { + value.push_str( + &text[start.index.min(length)..end.index.min(length)], + ); + } else { + value.push_str(&dbg!(text)[start.index.min(length)..]); + value.push_str(line.ending().as_str()); + } + } else if idx == lines_total - 1 { + value.push_str(&text[..end.index.min(length)]); + } else { + value.push_str(text); + value.push_str(line.ending().as_str()); + } + } + + value + } + + /// Returns the currently active [`SelectionEnd`]. + /// + /// `self.end` if `self.direction` is [`Right`], `self.start` otherwise. + /// + /// [`Right`]: Direction::Right + pub fn active_end(&self) -> SelectionEnd { + if self.direction == Direction::Right { + self.end + } else { + self.start + } + } + + /// Select a new range. + /// + /// `self.start` will be set to the smaller value, `self.end` to the larger. + /// + /// # Example + /// + /// ``` + /// use iced_selection::selection::{Selection, SelectionEnd}; + /// + /// let mut selection = Selection::default(); + /// + /// let start = SelectionEnd::new(5, 17); + /// let end = SelectionEnd::new(2, 8); + /// + /// selection.select_range(start, end); + /// + /// assert_eq!(selection.start, end); + /// assert_eq!(selection.end, start); + /// ``` + pub fn select_range(&mut self, start: SelectionEnd, end: SelectionEnd) { + self.start = start.min(end); + self.end = end.max(start); + } + + /// Updates the current selection by setting a new end point. + /// + /// This method adjusts the selection range based on the provided `new_end` position. The + /// current [`Direction`] is used to determine the new values: + /// + /// - If the current direction is [`Right`] (i.e., the selection goes from `start` to `end`), the + /// range becomes `(start, new_end)`. If `new_end` is before `start`, the direction is flipped to [`Left`]. + /// + /// - If it's [`Left`], the range becomes `(new_end, end)`. If `new_end` is after `end`, the + /// direction is flipped to [`Right`]. + /// + /// # Example + /// + /// ``` + /// use iced_selection::selection::{Direction, Selection, SelectionEnd}; + /// + /// let mut selection = Selection::default(); + /// + /// let start = SelectionEnd::new(5, 17); + /// let end = SelectionEnd::new(2, 8); + /// + /// selection.select_range(start, end); + /// + /// assert_eq!(selection.start, end); + /// assert_eq!(selection.end, start); + /// assert_eq!(selection.direction, Direction::Right); + /// + /// let new_end = SelectionEnd::new(2, 2); + /// + /// selection.change_selection(new_end); + /// + /// assert_eq!(selection.start, new_end); + /// assert_eq!(selection.end, end); + /// assert_eq!(selection.direction, Direction::Left); + /// ``` + /// + /// [`Left`]: Direction::Left + /// [`Right`]: Direction::Right + pub fn change_selection(&mut self, new_end: SelectionEnd) { + let (start, end) = if self.direction == Direction::Right { + if new_end < self.start { + self.direction = Direction::Left; + } + + (self.start, new_end) + } else { + if new_end > self.end { + self.direction = Direction::Right; + } + + (new_end, self.end) + }; + + self.moving_line_index = None; + self.select_range(start, end); + } + + /// Selects the word around the given grapheme position. + pub fn select_word( + &mut self, + line: usize, + index: usize, + paragraph: &Paragraph, + ) { + let value = Value::new(paragraph.buffer().lines[line].text()); + + let start = + SelectionEnd::new(line, value.previous_start_of_word(index)); + let end = SelectionEnd::new(line, value.next_end_of_word(index)); + + self.select_range(start, end); + } + + /// Moves the active [`SelectionEnd`] to the left by one, wrapping to the previous line if + /// possible and required. + pub fn select_left(&mut self, paragraph: &Paragraph) { + let mut active_end = self.active_end(); + + if active_end.index > 0 { + active_end.index -= 1; + + self.change_selection(active_end); + } else if active_end.line > 0 { + active_end.line -= 1; + + let value = + Value::new(paragraph.buffer().lines[active_end.line].text()); + active_end.index = value.len(); + + self.change_selection(active_end); + } + } + + /// Moves the active [`SelectionEnd`] to the right by one, wrapping to the next line if + /// possible and required. + pub fn select_right(&mut self, paragraph: &Paragraph) { + let mut active_end = self.active_end(); + + let lines = ¶graph.buffer().lines; + let value = Value::new(lines[active_end.line].text()); + + if active_end.index < value.len() { + active_end.index += 1; + + self.change_selection(active_end); + } else if active_end.line < lines.len() - 1 { + active_end.line += 1; + active_end.index = 0; + + self.change_selection(active_end); + } + } + + /// Moves the active [`SelectionEnd`] up by one, keeping track of the original grapheme index. + pub fn select_up(&mut self, paragraph: &Paragraph) { + let mut active_end = self.active_end(); + + if active_end.line == 0 { + active_end.index = 0; + + self.change_selection(active_end); + } else { + active_end.line -= 1; + + let mut moving_line_index = None; + + if let Some(index) = self.moving_line_index.take() { + active_end.index = index; + } + + let value = + Value::new(paragraph.buffer().lines[active_end.line].text()); + if active_end.index > value.len() { + moving_line_index = Some(active_end.index); + active_end.index = value.len(); + } + + self.change_selection(active_end); + self.moving_line_index = moving_line_index; + } + } + + /// Moves the active [`SelectionEnd`] down by one, keeping track of the original grapheme index. + pub fn select_down(&mut self, paragraph: &Paragraph) { + let mut active_end = self.active_end(); + + let lines = ¶graph.buffer().lines; + let value = Value::new(lines[active_end.line].text()); + + if active_end.line == lines.len() - 1 { + active_end.index = value.len(); + + self.change_selection(active_end); + } else { + active_end.line += 1; + + let mut moving_line_index = None; + + if let Some(index) = self.moving_line_index.take() { + active_end.index = index; + } + + let value = + Value::new(paragraph.buffer().lines[active_end.line].text()); + if active_end.index > value.len() { + moving_line_index = Some(active_end.index); + active_end.index = value.len(); + } + + self.change_selection(active_end); + self.moving_line_index = moving_line_index; + } + } + + /// Moves the active [`SelectionEnd`] to the previous start of a word on its current line, or + /// the previous line if it exists and `index == 0`. + pub fn select_left_by_words(&mut self, paragraph: &Paragraph) { + let mut active_end = self.active_end(); + + if active_end.index == 1 { + active_end.index = 0; + + self.change_selection(active_end); + } else if active_end.index > 1 { + let value = + Value::new(paragraph.buffer().lines[active_end.line].text()); + active_end.index = value.previous_start_of_word(active_end.index); + + self.change_selection(active_end); + } else if active_end.line > 0 { + active_end.line -= 1; + + let value = + Value::new(paragraph.buffer().lines[active_end.line].text()); + active_end.index = value.previous_start_of_word(value.len()); + + self.change_selection(active_end); + } + } + + /// Moves the active [`SelectionEnd`] to the next end of a word on its current line, or + /// the next line if it exists and `index == line.len()`. + pub fn select_right_by_words(&mut self, paragraph: &Paragraph) { + let mut active_end = self.active_end(); + + let lines = ¶graph.buffer().lines; + let value = Value::new(lines[active_end.line].text()); + + if value.len() - active_end.index == 1 { + active_end.index = value.len(); + + self.change_selection(active_end); + } else if active_end.index < value.len() { + active_end.index = value.next_end_of_word(active_end.index); + + self.change_selection(active_end); + } else if active_end.line < lines.len() - 1 { + active_end.line += 1; + + let value = Value::new(lines[active_end.line].text()); + active_end.index = value.next_end_of_word(0); + + self.change_selection(active_end); + } + } + + /// Moves the active [`SelectionEnd`] to the beginning of its current line. + pub fn select_line_beginning(&mut self) { + let mut active_end = self.active_end(); + + if active_end.index > 0 { + active_end.index = 0; + + self.change_selection(active_end); + } + } + + /// Moves the active [`SelectionEnd`] to the end of its current line. + pub fn select_line_end(&mut self, paragraph: &Paragraph) { + let mut active_end = self.active_end(); + + let value = + Value::new(paragraph.buffer().lines[active_end.line].text()); + + if active_end.index < value.len() { + active_end.index = value.len(); + + self.change_selection(active_end); + } + } + + /// Moves the active [`SelectionEnd`] to the beginning of the [`Paragraph`]. + /// + /// [`Paragraph`]: https://docs.iced.rs/iced_graphics/text/paragraph/struct.Paragraph.html + pub fn select_beginning(&mut self) { + self.change_selection(SelectionEnd::new(0, 0)); + } + + /// Moves the active [`SelectionEnd`] to the end of the [`Paragraph`]. + /// + /// [`Paragraph`]: https://docs.iced.rs/iced_graphics/text/paragraph/struct.Paragraph.html + pub fn select_end(&mut self, paragraph: &Paragraph) { + let lines = ¶graph.buffer().lines; + let value = Value::new(lines[lines.len() - 1].text()); + + let new_end = SelectionEnd::new(lines.len() - 1, value.len()); + + self.change_selection(new_end); + } + + /// Selects an entire line. + pub fn select_line(&mut self, line: usize, paragraph: &Paragraph) { + let value = Value::new(paragraph.buffer().lines[line].text()); + + let start = SelectionEnd::new(line, 0); + let end = SelectionEnd::new(line, value.len()); + + self.select_range(start, end); + } + + /// Selects the entire [`Paragraph`]. + /// + /// [`Paragraph`]: https://docs.iced.rs/iced_graphics/text/paragraph/struct.Paragraph.html + pub fn select_all(&mut self, paragraph: &Paragraph) { + let line = paragraph.buffer().lines.len() - 1; + let index = Value::new(paragraph.buffer().lines[line].text()).len(); + + let end = SelectionEnd::new(line, index); + + self.select_range(SelectionEnd::new(0, 0), end); + } +} |
