Skip to main content

kas_core/text/
selection.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License in the LICENSE-APACHE file or at:
4//     https://www.apache.org/licenses/LICENSE-2.0
5
6//! Tools for text selection
7
8use crate::theme::Text;
9use kas_macros::autoimpl;
10use kas_text::format::FormattableText;
11use std::ops::Range;
12use unicode_segmentation::UnicodeSegmentation;
13
14/// Cursor index / selection range
15///
16/// This is essentially a pair of indices: the selection index and the edit
17/// index.
18#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
19pub struct CursorRange {
20    sel: usize,
21    edit: usize,
22}
23
24impl From<usize> for CursorRange {
25    #[inline]
26    fn from(index: usize) -> Self {
27        CursorRange {
28            sel: index,
29            edit: index,
30        }
31    }
32}
33
34impl From<Range<usize>> for CursorRange {
35    #[inline]
36    fn from(range: Range<usize>) -> Self {
37        CursorRange {
38            sel: range.start,
39            edit: range.end,
40        }
41    }
42}
43
44impl CursorRange {
45    /// Construct from `(selection, edit)` positions
46    ///
47    /// Constructs as a range, with the cursor at the `edit` position.
48    ///
49    /// See also:
50    ///
51    /// - `Default`: an empty cursor at index 0
52    /// - `From<usize>`: construct from an index (empty selection)
53    /// - `From<Range<usize>>`: construct from a range (potentially non-empty
54    ///   selection; edit position is set to the range's end)
55    #[inline]
56    pub fn new(sel: usize, edit: usize) -> Self {
57        CursorRange { sel, edit }
58    }
59
60    /// True if the selection index equals the cursor index
61    #[inline]
62    pub fn is_empty(&self) -> bool {
63        self.edit == self.sel
64    }
65
66    /// Clear selection without changing the edit index
67    #[inline]
68    pub fn set_empty(&mut self) {
69        self.sel = self.edit;
70    }
71
72    /// Get the selection index
73    pub fn sel_index(&self) -> usize {
74        self.sel
75    }
76
77    /// Get the edit cursor index
78    pub fn edit_index(&self) -> usize {
79        self.edit
80    }
81
82    /// Get the selection range
83    ///
84    /// This range is from the edit index to the selection index or reversed,
85    /// whichever is increasing.
86    pub fn range(&self) -> Range<usize> {
87        let mut range = self.edit..self.sel;
88        if range.start > range.end {
89            std::mem::swap(&mut range.start, &mut range.end);
90        }
91        range
92    }
93}
94
95/// Text-selection logic
96///
97/// This struct holds a [`CursorRange`]. There is no requirement on the order of these two
98/// positions. Each may be adjusted independently.
99///
100/// Additionally, this struct holds the selection anchor index. This usually
101/// equals the selection index, but when using double-click or triple-click
102/// selection, the anchor represents the initially-clicked position while the
103/// selection index represents the expanded position.
104#[derive(Clone, Debug, Default)]
105#[autoimpl(Deref, DerefMut using self.cursor)]
106pub struct SelectionHelper {
107    cursor: CursorRange,
108    anchor: usize,
109}
110
111impl<T: Into<CursorRange>> From<T> for SelectionHelper {
112    fn from(x: T) -> Self {
113        let cursor = x.into();
114        SelectionHelper {
115            cursor,
116            anchor: cursor.sel,
117        }
118    }
119}
120
121impl SelectionHelper {
122    /// Set the cursor position, clearing the selection
123    #[inline]
124    pub fn set_cursor(&mut self, index: usize) {
125        self.cursor.sel = index;
126        self.cursor.edit = index;
127        self.anchor = index;
128    }
129
130    /// Set the cursor index without adjusting the selection index
131    #[inline]
132    pub fn set_edit_index(&mut self, index: usize) {
133        self.edit = index;
134    }
135
136    /// Set the selection index without adjusting the edit index
137    ///
138    /// The anchor index is also set to the selection index.
139    #[inline]
140    pub fn set_sel_index(&mut self, index: usize) {
141        self.sel = index;
142        self.anchor = index;
143    }
144    /// Set the selection index only
145    ///
146    /// Prefer [`Self::set_sel_index`] unless you know you don't want to set the anchor.
147    #[inline]
148    pub fn set_sel_index_only(&mut self, index: usize) {
149        self.sel = index;
150    }
151
152    /// Apply new limit to the maximum length
153    ///
154    /// Call this method if the string changes under the selection to ensure
155    /// that the selection does not exceed the length of the new string.
156    #[inline]
157    pub fn set_max_len(&mut self, len: usize) {
158        self.edit = self.edit.min(len);
159        self.sel = self.sel.min(len);
160        self.anchor = self.anchor.min(len);
161    }
162
163    /// Set the anchor to the edit position
164    ///
165    /// This is used to start a drag-selection. If `clear`, then the selection
166    /// position is also set to the edit position.
167    ///
168    /// [`Self::expand`] may be used to expand the selection from this anchor.
169    #[inline]
170    pub fn set_anchor(&mut self, clear: bool) {
171        self.anchor = self.edit;
172        if clear {
173            self.sel = self.edit;
174        }
175    }
176
177    /// Expand the selection from the range between edit and anchor positions
178    ///
179    /// This moves the cursor range. To obtain repeatable
180    /// behaviour on drag-selection, set the anchor ([`Self::set_anchor`])
181    /// initially, then set the edit position and call this method each time
182    /// the cursor moves.
183    ///
184    /// The selection is expanded by words or lines (if `lines`). Line expansion
185    /// requires that text has been prepared ([`Text::prepare`]).
186    pub fn expand<T: FormattableText>(&mut self, text: &Text<T>, lines: bool) {
187        let string = text.as_str();
188        let mut range = self.edit..self.anchor;
189        if range.start > range.end {
190            std::mem::swap(&mut range.start, &mut range.end);
191        }
192        let (mut start, mut end);
193        if !lines {
194            end = string[range.start..]
195                .char_indices()
196                .nth(1)
197                .map(|(i, _)| range.start + i)
198                .unwrap_or(string.len());
199            start = string[0..end]
200                .split_word_bound_indices()
201                .next_back()
202                .map(|(index, _)| index)
203                .unwrap_or(0);
204            end = string[start..]
205                .split_word_bound_indices()
206                .find_map(|(index, _)| {
207                    let pos = start + index;
208                    (pos >= range.end).then_some(pos)
209                })
210                .unwrap_or(string.len());
211        } else {
212            start = match text.find_line(range.start) {
213                Ok(Some(r)) => r.1.start,
214                _ => 0,
215            };
216            end = match text.find_line(range.end) {
217                Ok(Some(r)) => r.1.end,
218                _ => string.len(),
219            };
220        }
221
222        if self.edit < self.sel {
223            std::mem::swap(&mut start, &mut end);
224        }
225        self.sel = start;
226        self.edit = end;
227    }
228
229    /// Adjust all indices for a deletion from the source text
230    pub fn delete_range(&mut self, range: Range<usize>) {
231        let len = range.len();
232        let adjust = |index: usize| -> usize {
233            if index >= range.end {
234                index - len
235            } else if index > range.start {
236                range.start
237            } else {
238                index
239            }
240        };
241        self.edit = adjust(self.edit);
242        self.sel = adjust(self.sel);
243        self.anchor = adjust(self.anchor);
244    }
245}