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}