1use epaint::text::{Galley, cursor::CCursor};
4use unicode_segmentation::UnicodeSegmentation as _;
5
6use crate::{NumExt as _, Rect, Response, Ui, epaint};
7
8use super::CCursorRange;
9
10#[derive(Clone, Copy, Debug, Default)]
14#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
15#[cfg_attr(feature = "serde", serde(default))]
16pub struct TextCursorState {
17 ccursor_range: Option<CCursorRange>,
18}
19
20impl From<CCursorRange> for TextCursorState {
21 fn from(ccursor_range: CCursorRange) -> Self {
22 Self {
23 ccursor_range: Some(ccursor_range),
24 }
25 }
26}
27
28impl TextCursorState {
29 pub fn is_empty(&self) -> bool {
30 self.ccursor_range.is_none()
31 }
32
33 pub fn char_range(&self) -> Option<CCursorRange> {
35 self.ccursor_range
36 }
37
38 pub fn range(&self, galley: &Galley) -> Option<CCursorRange> {
41 self.ccursor_range.map(|mut range| {
42 range.primary = galley.clamp_cursor(&range.primary);
43 range.secondary = galley.clamp_cursor(&range.secondary);
44 range
45 })
46 }
47
48 pub fn set_char_range(&mut self, ccursor_range: Option<CCursorRange>) {
50 self.ccursor_range = ccursor_range;
51 }
52}
53
54impl TextCursorState {
55 pub fn pointer_interaction(
59 &mut self,
60 ui: &Ui,
61 response: &Response,
62 cursor_at_pointer: CCursor,
63 galley: &Galley,
64 is_being_dragged: bool,
65 ) -> bool {
66 let text = galley.text();
67
68 if response.double_clicked() {
69 let ccursor_range = select_word_at(text, cursor_at_pointer);
71 self.set_char_range(Some(ccursor_range));
72 true
73 } else if response.triple_clicked() {
74 let ccursor_range = select_line_at(text, cursor_at_pointer);
76 self.set_char_range(Some(ccursor_range));
77 true
78 } else if response.sense.senses_drag() {
79 if response.hovered() && ui.input(|i| i.pointer.any_pressed()) {
80 if ui.input(|i| i.modifiers.shift) {
82 if let Some(mut cursor_range) = self.range(galley) {
83 cursor_range.primary = cursor_at_pointer;
84 self.set_char_range(Some(cursor_range));
85 } else {
86 self.set_char_range(Some(CCursorRange::one(cursor_at_pointer)));
87 }
88 } else {
89 self.set_char_range(Some(CCursorRange::one(cursor_at_pointer)));
90 }
91 true
92 } else if is_being_dragged {
93 if let Some(mut cursor_range) = self.range(galley) {
95 cursor_range.primary = cursor_at_pointer;
96 self.set_char_range(Some(cursor_range));
97 }
98 true
99 } else {
100 false
101 }
102 } else {
103 false
104 }
105 }
106}
107
108fn select_word_at(text: &str, ccursor: CCursor) -> CCursorRange {
109 if ccursor.index == 0 {
110 CCursorRange::two(ccursor, ccursor_next_word(text, ccursor))
111 } else {
112 let it = text.chars();
113 let mut it = it.skip(ccursor.index - 1);
114 if let Some(char_before_cursor) = it.next() {
115 if let Some(char_after_cursor) = it.next() {
116 if is_word_char(char_before_cursor) && is_word_char(char_after_cursor) {
117 let min = ccursor_previous_word(text, ccursor + 1);
118 let max = ccursor_next_word(text, min);
119 CCursorRange::two(min, max)
120 } else if is_word_char(char_before_cursor) {
121 let min = ccursor_previous_word(text, ccursor);
122 let max = ccursor_next_word(text, min);
123 CCursorRange::two(min, max)
124 } else if is_word_char(char_after_cursor) {
125 let max = ccursor_next_word(text, ccursor);
126 CCursorRange::two(ccursor, max)
127 } else {
128 let min = ccursor_previous_word(text, ccursor);
129 let max = ccursor_next_word(text, ccursor);
130 CCursorRange::two(min, max)
131 }
132 } else {
133 let min = ccursor_previous_word(text, ccursor);
134 CCursorRange::two(min, ccursor)
135 }
136 } else {
137 let max = ccursor_next_word(text, ccursor);
138 CCursorRange::two(ccursor, max)
139 }
140 }
141}
142
143fn select_line_at(text: &str, ccursor: CCursor) -> CCursorRange {
144 if ccursor.index == 0 {
145 CCursorRange::two(ccursor, ccursor_next_line(text, ccursor))
146 } else {
147 let it = text.chars();
148 let mut it = it.skip(ccursor.index - 1);
149 if let Some(char_before_cursor) = it.next() {
150 if let Some(char_after_cursor) = it.next() {
151 if (!is_linebreak(char_before_cursor)) && (!is_linebreak(char_after_cursor)) {
152 let min = ccursor_previous_line(text, ccursor + 1);
153 let max = ccursor_next_line(text, min);
154 CCursorRange::two(min, max)
155 } else if !is_linebreak(char_before_cursor) {
156 let min = ccursor_previous_line(text, ccursor);
157 let max = ccursor_next_line(text, min);
158 CCursorRange::two(min, max)
159 } else if !is_linebreak(char_after_cursor) {
160 let max = ccursor_next_line(text, ccursor);
161 CCursorRange::two(ccursor, max)
162 } else {
163 let min = ccursor_previous_line(text, ccursor);
164 let max = ccursor_next_line(text, ccursor);
165 CCursorRange::two(min, max)
166 }
167 } else {
168 let min = ccursor_previous_line(text, ccursor);
169 CCursorRange::two(min, ccursor)
170 }
171 } else {
172 let max = ccursor_next_line(text, ccursor);
173 CCursorRange::two(ccursor, max)
174 }
175 }
176}
177
178pub fn ccursor_next_word(text: &str, ccursor: CCursor) -> CCursor {
179 CCursor {
180 index: next_word_boundary_char_index(text, ccursor.index),
181 prefer_next_row: false,
182 }
183}
184
185fn ccursor_next_line(text: &str, ccursor: CCursor) -> CCursor {
186 CCursor {
187 index: next_line_boundary_char_index(text.chars(), ccursor.index),
188 prefer_next_row: false,
189 }
190}
191
192pub fn ccursor_previous_word(text: &str, ccursor: CCursor) -> CCursor {
193 let num_chars = text.chars().count();
194 let reversed: String = text.graphemes(true).rev().collect();
195 CCursor {
196 index: num_chars
197 - next_word_boundary_char_index(&reversed, num_chars - ccursor.index).min(num_chars),
198 prefer_next_row: true,
199 }
200}
201
202fn ccursor_previous_line(text: &str, ccursor: CCursor) -> CCursor {
203 let num_chars = text.chars().count();
204 CCursor {
205 index: num_chars
206 - next_line_boundary_char_index(text.chars().rev(), num_chars - ccursor.index),
207 prefer_next_row: true,
208 }
209}
210
211fn next_word_boundary_char_index(text: &str, index: usize) -> usize {
212 for word in text.split_word_bound_indices() {
213 let ci = char_index_from_byte_index(text, word.0);
218 if ci > index && !skip_word(word.1) {
219 return ci;
220 }
221 }
222
223 char_index_from_byte_index(text, text.len())
224}
225
226fn skip_word(text: &str) -> bool {
227 !text.chars().any(|c| !is_word_char(c))
230}
231
232fn next_line_boundary_char_index(it: impl Iterator<Item = char>, mut index: usize) -> usize {
233 let mut it = it.skip(index);
234 if let Some(_first) = it.next() {
235 index += 1;
236
237 if let Some(second) = it.next() {
238 index += 1;
239 for next in it {
240 if is_linebreak(next) != is_linebreak(second) {
241 break;
242 }
243 index += 1;
244 }
245 }
246 }
247 index
248}
249
250pub fn is_word_char(c: char) -> bool {
251 c.is_alphanumeric() || c == '_'
252}
253
254fn is_linebreak(c: char) -> bool {
255 c == '\r' || c == '\n'
256}
257
258pub fn find_line_start(text: &str, current_index: CCursor) -> CCursor {
260 let chars_count = text.chars().count();
266
267 let position = text
268 .chars()
269 .rev()
270 .skip(chars_count - current_index.index)
271 .position(|x| x == '\n');
272
273 match position {
274 Some(pos) => CCursor::new(current_index.index - pos),
275 None => CCursor::new(0),
276 }
277}
278
279pub fn byte_index_from_char_index(s: &str, char_index: usize) -> usize {
280 for (ci, (bi, _)) in s.char_indices().enumerate() {
281 if ci == char_index {
282 return bi;
283 }
284 }
285 s.len()
286}
287
288pub fn char_index_from_byte_index(input: &str, byte_index: usize) -> usize {
289 for (ci, (bi, _)) in input.char_indices().enumerate() {
290 if bi == byte_index {
291 return ci;
292 }
293 }
294
295 input.char_indices().last().map_or(0, |(i, _)| i + 1)
296}
297
298pub fn slice_char_range(s: &str, char_range: std::ops::Range<usize>) -> &str {
299 assert!(
300 char_range.start <= char_range.end,
301 "Invalid range, start must be less than end, but start = {}, end = {}",
302 char_range.start,
303 char_range.end
304 );
305 let start_byte = byte_index_from_char_index(s, char_range.start);
306 let end_byte = byte_index_from_char_index(s, char_range.end);
307 &s[start_byte..end_byte]
308}
309
310pub fn cursor_rect(galley: &Galley, cursor: &CCursor, row_height: f32) -> Rect {
312 let mut cursor_pos = galley.pos_from_cursor(*cursor);
313
314 cursor_pos.max.y = cursor_pos.max.y.at_least(cursor_pos.min.y + row_height);
316
317 cursor_pos = cursor_pos.expand(1.5); cursor_pos
320}
321
322#[cfg(test)]
323mod test {
324 use crate::text_selection::text_cursor_state::next_word_boundary_char_index;
325
326 #[test]
327 fn test_next_word_boundary_char_index() {
328 let text = "abc d3f g_h i-j";
330 assert_eq!(next_word_boundary_char_index(text, 1), 3);
331 assert_eq!(next_word_boundary_char_index(text, 3), 7);
332 assert_eq!(next_word_boundary_char_index(text, 9), 11);
333 assert_eq!(next_word_boundary_char_index(text, 12), 13);
334 assert_eq!(next_word_boundary_char_index(text, 13), 15);
335 assert_eq!(next_word_boundary_char_index(text, 15), 15);
336
337 assert_eq!(next_word_boundary_char_index("", 0), 0);
338 assert_eq!(next_word_boundary_char_index("", 1), 0);
339
340 let text = "❤️👍 skvělá knihovna 👍❤️";
346 assert_eq!(next_word_boundary_char_index(text, 0), 2);
347 assert_eq!(next_word_boundary_char_index(text, 2), 3); assert_eq!(next_word_boundary_char_index(text, 6), 10);
349 assert_eq!(next_word_boundary_char_index(text, 9), 10);
350 assert_eq!(next_word_boundary_char_index(text, 12), 19);
351 assert_eq!(next_word_boundary_char_index(text, 15), 19);
352 assert_eq!(next_word_boundary_char_index(text, 19), 20);
353 assert_eq!(next_word_boundary_char_index(text, 20), 21);
354 }
355}