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 text.is_empty() {
110 return CCursorRange::one(ccursor);
111 }
112
113 let line_start = find_line_start(text, ccursor);
114 let line_end = ccursor_next_line(text, line_start);
115
116 let line_range = line_start.index..line_end.index;
117 let current_line_text = slice_char_range(text, line_range.clone());
118
119 let relative_idx = ccursor.index - line_start.index;
120 let relative_ccursor = CCursor::new(relative_idx);
121
122 let min = ccursor_previous_word(current_line_text, relative_ccursor);
123 let max = ccursor_next_word(current_line_text, relative_ccursor);
124
125 CCursorRange::two(
126 CCursor::new(line_start.index + min.index),
127 CCursor::new(line_start.index + max.index),
128 )
129}
130
131fn select_line_at(text: &str, ccursor: CCursor) -> CCursorRange {
132 if ccursor.index == 0 {
133 CCursorRange::two(ccursor, ccursor_next_line(text, ccursor))
134 } else {
135 let it = text.chars();
136 let mut it = it.skip(ccursor.index - 1);
137 if let Some(char_before_cursor) = it.next() {
138 if let Some(char_after_cursor) = it.next() {
139 if (!is_linebreak(char_before_cursor)) && (!is_linebreak(char_after_cursor)) {
140 let min = ccursor_previous_line(text, ccursor + 1);
141 let max = ccursor_next_line(text, min);
142 CCursorRange::two(min, max)
143 } else if !is_linebreak(char_before_cursor) {
144 let min = ccursor_previous_line(text, ccursor);
145 let max = ccursor_next_line(text, min);
146 CCursorRange::two(min, max)
147 } else if !is_linebreak(char_after_cursor) {
148 let max = ccursor_next_line(text, ccursor);
149 CCursorRange::two(ccursor, max)
150 } else {
151 let min = ccursor_previous_line(text, ccursor);
152 let max = ccursor_next_line(text, ccursor);
153 CCursorRange::two(min, max)
154 }
155 } else {
156 let min = ccursor_previous_line(text, ccursor);
157 CCursorRange::two(min, ccursor)
158 }
159 } else {
160 let max = ccursor_next_line(text, ccursor);
161 CCursorRange::two(ccursor, max)
162 }
163 }
164}
165
166pub fn ccursor_next_word(text: &str, ccursor: CCursor) -> CCursor {
167 CCursor {
168 index: next_word_boundary_char_index(text, ccursor.index),
169 prefer_next_row: false,
170 }
171}
172
173fn ccursor_next_line(text: &str, ccursor: CCursor) -> CCursor {
174 CCursor {
175 index: next_line_boundary_char_index(text.chars(), ccursor.index),
176 prefer_next_row: false,
177 }
178}
179
180pub fn ccursor_previous_word(text: &str, ccursor: CCursor) -> CCursor {
181 let num_chars = text.chars().count();
182 let reversed: String = text.graphemes(true).rev().collect();
183 CCursor {
184 index: num_chars
185 - next_word_boundary_char_index(&reversed, num_chars - ccursor.index).min(num_chars),
186 prefer_next_row: true,
187 }
188}
189
190fn ccursor_previous_line(text: &str, ccursor: CCursor) -> CCursor {
191 let num_chars = text.chars().count();
192 CCursor {
193 index: num_chars
194 - next_line_boundary_char_index(text.chars().rev(), num_chars - ccursor.index),
195 prefer_next_row: true,
196 }
197}
198
199fn next_word_boundary_char_index(text: &str, cursor_ci: usize) -> usize {
200 let mut current_char_idx = 0;
201
202 for (_word_byte_index, word) in text.split_word_bound_indices() {
203 let word_ci = current_char_idx;
204
205 let mut word_char_count = 0;
208 for chr in word.chars() {
209 let dot_ci = word_ci + word_char_count;
210 if chr == '.' && cursor_ci < dot_ci {
211 return dot_ci;
212 }
213 word_char_count += 1;
214 }
215
216 if cursor_ci < word_ci && !all_word_chars(word) {
221 return word_ci;
222 }
223
224 current_char_idx += word_char_count;
225 }
226
227 current_char_idx
228}
229
230fn all_word_chars(text: &str) -> bool {
231 text.chars().all(is_word_char)
232}
233
234fn next_line_boundary_char_index(it: impl Iterator<Item = char>, mut index: usize) -> usize {
235 let mut it = it.skip(index);
236 if let Some(_first) = it.next() {
237 index += 1;
238
239 if let Some(second) = it.next() {
240 index += 1;
241 for next in it {
242 if is_linebreak(next) != is_linebreak(second) {
243 break;
244 }
245 index += 1;
246 }
247 }
248 }
249 index
250}
251
252pub fn is_word_char(c: char) -> bool {
253 c.is_alphanumeric() || c == '_'
254}
255
256fn is_linebreak(c: char) -> bool {
257 c == '\r' || c == '\n'
258}
259
260pub fn find_line_start(text: &str, current_index: CCursor) -> CCursor {
262 let byte_idx = byte_index_from_char_index(text, current_index.index);
263 let text_before = &text[..byte_idx];
264
265 if let Some(last_newline_byte) = text_before.rfind('\n') {
266 let char_idx = char_index_from_byte_index(text, last_newline_byte + 1);
267 CCursor::new(char_idx)
268 } else {
269 CCursor::new(0)
270 }
271}
272
273pub fn byte_index_from_char_index(s: &str, char_index: usize) -> usize {
274 for (ci, (bi, _)) in s.char_indices().enumerate() {
275 if ci == char_index {
276 return bi;
277 }
278 }
279 s.len()
280}
281
282pub fn char_index_from_byte_index(input: &str, byte_index: usize) -> usize {
283 for (ci, (bi, _)) in input.char_indices().enumerate() {
284 if bi == byte_index {
285 return ci;
286 }
287 }
288
289 input.char_indices().last().map_or(0, |(i, _)| i + 1)
290}
291
292pub fn slice_char_range(s: &str, char_range: std::ops::Range<usize>) -> &str {
293 assert!(
294 char_range.start <= char_range.end,
295 "Invalid range, start must be less than end, but start = {}, end = {}",
296 char_range.start,
297 char_range.end
298 );
299 let start_byte = byte_index_from_char_index(s, char_range.start);
300 let end_byte = byte_index_from_char_index(s, char_range.end);
301 &s[start_byte..end_byte]
302}
303
304pub fn cursor_rect(galley: &Galley, cursor: &CCursor, row_height: f32) -> Rect {
306 let mut cursor_pos = galley.pos_from_cursor(*cursor);
307
308 cursor_pos.max.y = cursor_pos.max.y.at_least(cursor_pos.min.y + row_height);
310
311 cursor_pos = cursor_pos.expand(1.5); cursor_pos
314}
315
316#[cfg(test)]
317mod test {
318 use super::*;
319
320 #[test]
321 fn test_next_word_boundary_char_index() {
322 let text = "abc d3f g_h i-j";
324 assert_eq!(next_word_boundary_char_index(text, 1), 3);
325 assert_eq!(next_word_boundary_char_index(text, 3), 7);
326 assert_eq!(next_word_boundary_char_index(text, 9), 11);
327 assert_eq!(next_word_boundary_char_index(text, 12), 13);
328 assert_eq!(next_word_boundary_char_index(text, 13), 15);
329 assert_eq!(next_word_boundary_char_index(text, 15), 15);
330
331 assert_eq!(next_word_boundary_char_index("", 0), 0);
332 assert_eq!(next_word_boundary_char_index("", 1), 0);
333
334 let text = "abc.def.ghi";
336 assert_eq!(next_word_boundary_char_index(text, 1), 3);
337 assert_eq!(next_word_boundary_char_index(text, 3), 7);
338 assert_eq!(next_word_boundary_char_index(text, 7), 11);
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
356 #[test]
357 fn test_previous_word() {
358 let text = "abc def ghi";
359 assert_eq!(ccursor_previous_word(text, CCursor::new(7)).index, 4);
360 assert_eq!(ccursor_previous_word(text, CCursor::new(5)).index, 4);
361 assert_eq!(ccursor_previous_word(text, CCursor::new(4)).index, 0);
362 assert_eq!(ccursor_previous_word(text, CCursor::new(0)).index, 0);
363 }
364
365 #[test]
366 fn test_next_word() {
367 let text = "abc def ghi";
368 assert_eq!(ccursor_next_word(text, CCursor::new(0)).index, 3);
369 assert_eq!(ccursor_next_word(text, CCursor::new(3)).index, 7);
370 assert_eq!(ccursor_next_word(text, CCursor::new(7)).index, 11);
371 assert_eq!(ccursor_next_word(text, CCursor::new(11)).index, 11);
372 }
373
374 #[test]
375 fn test_select_word_at() {
376 let text = "hello world";
378 let range = select_word_at(text, CCursor::new(2));
379 let (lo, hi) = (
380 range.primary.index.min(range.secondary.index),
381 range.primary.index.max(range.secondary.index),
382 );
383 assert_eq!(lo, 0);
384 assert_eq!(hi, 5);
385
386 let range = select_word_at(text, CCursor::new(8));
387 let (lo, hi) = (
388 range.primary.index.min(range.secondary.index),
389 range.primary.index.max(range.secondary.index),
390 );
391 assert_eq!(lo, 6);
392 assert_eq!(hi, 11);
393 }
394
395 #[test]
396 fn test_word_boundary_large_text_performance() {
397 let large_text = "word ".repeat(200_000); let len = large_text.chars().count();
400
401 let start = std::time::Instant::now();
402
403 let next = ccursor_next_word(&large_text, CCursor::new(len - 10));
404 assert!(next.index <= len);
405
406 let prev = ccursor_previous_word(&large_text, CCursor::new(len - 10));
407 assert!(prev.index < len);
408
409 let range = select_word_at(&large_text, CCursor::new(len - 3));
410 let lo = range.primary.index.min(range.secondary.index);
411 let hi = range.primary.index.max(range.secondary.index);
412 assert!(lo < hi, "Expected a non-empty word selection");
413
414 let elapsed = start.elapsed();
415 assert!(
416 elapsed.as_secs() < 5,
417 "Word boundary operations on 1MB text took {elapsed:?}, expected < 5s"
418 );
419 }
420}
421
422#[cfg(test)]
423mod tests {
424 use super::*;
425
426 #[test]
427 fn test_previous_word_graphemes() {
428 let cases = [
429 ("", 0, 0),
430 ("hello", 0, 0),
431 ("hello", "hello".chars().count(), 0),
432 ("hello world", 6, 0),
433 ("hello world", 8, 6),
434 ("hello world", "hello world".chars().count(), 6),
435 ("hello world ", "hello world ".chars().count(), 6),
436 ("hello world", "hello world".chars().count(), 8),
437 (" ", " ".chars().count(), 0),
438 ("hello, world", "hello, world".chars().count(), 7),
439 ("www.example.com", "www.example.com".chars().count(), 12),
440 ("μλ
! π μΈμ", 8, 6),
441 ("β€οΈπ skvΔlΓ‘ knihovna πβ€οΈ", 18, 11),
442 (
443 "a e\u{301} b",
444 "a e\u{301} b".chars().count(),
445 "a e\u{301} ".chars().count(),
446 ),
447 (
448 "hi π world",
449 "hi π world".chars().count(),
450 "hi π ".chars().count(),
451 ),
452 (
453 "hi π¨βπ©βπ§βπ¦ world",
454 "hi π¨βπ©βπ§βπ¦ world".chars().count(),
455 "hi π¨βπ©βπ§βπ¦ ".chars().count(),
456 ),
457 ];
458
459 for (text, cursor, expected) in cases {
460 let result = ccursor_previous_word(text, CCursor::new(cursor));
461 assert_eq!(
462 result.index, expected,
463 "text={text:?}, cursor={cursor}, got={}, expected={expected}",
464 result.index
465 );
466 }
467 }
468}