1use epaint::text::{ByteIndex, ByteRangeExt as _, CharIndex, 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 == CharIndex::ZERO {
133 CCursorRange::two(ccursor, ccursor_next_line(text, ccursor))
134 } else {
135 let it = text.chars();
136 let mut it = it.skip(ccursor.index.0 - 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 min = ccursor_previous_line(text, ccursor);
149 let max = ccursor_next_line(text, ccursor);
150 CCursorRange::two(min, max)
151 } else {
152 let max = ccursor_next_line(text, ccursor);
153 CCursorRange::two(ccursor, 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 = CharIndex(text.chars().count());
182 let reversed: String = text.graphemes(true).rev().collect();
183 let boundary = next_word_boundary_char_index(&reversed, num_chars - ccursor.index);
184 CCursor {
185 index: num_chars - boundary.min(num_chars),
186 prefer_next_row: true,
187 }
188}
189
190fn ccursor_previous_line(text: &str, ccursor: CCursor) -> CCursor {
191 let num_chars = CharIndex(text.chars().count());
192 let boundary = next_line_boundary_char_index(text.chars().rev(), num_chars - ccursor.index);
193 CCursor {
194 index: num_chars - boundary,
195 prefer_next_row: true,
196 }
197}
198
199fn next_word_boundary_char_index(text: &str, cursor_ci: CharIndex) -> CharIndex {
200 let mut current_char_idx = CharIndex::ZERO;
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(
235 it: impl Iterator<Item = char>,
236 mut index: CharIndex,
237) -> CharIndex {
238 let mut it = it.skip(index.0);
239 if let Some(_first) = it.next() {
240 index += 1;
241
242 if let Some(second) = it.next() {
243 index += 1;
244 for next in it {
245 if is_linebreak(next) != is_linebreak(second) {
246 break;
247 }
248 index += 1;
249 }
250 }
251 }
252 index
253}
254
255pub fn is_word_char(c: char) -> bool {
256 c.is_alphanumeric() || c == '_'
257}
258
259fn is_linebreak(c: char) -> bool {
260 c == '\r' || c == '\n'
261}
262
263pub fn find_line_start(text: &str, current_index: CCursor) -> CCursor {
265 let byte_idx = byte_index_from_char_index(text, current_index.index);
266 let text_before = (ByteIndex::ZERO..byte_idx).slice(text);
267
268 if let Some(last_newline_byte) = text_before.rfind('\n') {
269 let char_idx = char_index_from_byte_index(text, ByteIndex(last_newline_byte + 1));
270 CCursor::new(char_idx)
271 } else {
272 CCursor::new(0)
273 }
274}
275
276pub fn byte_index_from_char_index(s: &str, char_index: CharIndex) -> ByteIndex {
277 for (ci, (bi, _)) in s.char_indices().enumerate() {
278 if ci == char_index.0 {
279 return ByteIndex(bi);
280 }
281 }
282 ByteIndex(s.len())
283}
284
285pub fn char_index_from_byte_index(input: &str, byte_index: ByteIndex) -> CharIndex {
286 for (ci, (bi, _)) in input.char_indices().enumerate() {
287 if bi == byte_index.0 {
288 return CharIndex(ci);
289 }
290 }
291
292 CharIndex(input.chars().count())
295}
296
297pub fn slice_char_range(s: &str, char_range: std::ops::Range<CharIndex>) -> &str {
298 assert!(
299 char_range.start <= char_range.end,
300 "Invalid range, start must be less than end, but start = {}, end = {}",
301 char_range.start,
302 char_range.end
303 );
304 let start_byte = byte_index_from_char_index(s, char_range.start);
305 let end_byte = byte_index_from_char_index(s, char_range.end);
306 (start_byte..end_byte).slice(s)
307}
308
309pub fn cursor_rect(galley: &Galley, cursor: &CCursor, row_height: f32) -> Rect {
311 let mut cursor_pos = galley.pos_from_cursor(*cursor);
312
313 cursor_pos.max.y = cursor_pos.max.y.at_least(cursor_pos.min.y + row_height);
315
316 cursor_pos = cursor_pos.expand(1.5); cursor_pos
319}
320
321#[cfg(test)]
322mod test {
323 use super::*;
324
325 #[test]
326 fn test_next_word_boundary_char_index() {
327 let text = "abc d3f g_h i-j";
329 assert_eq!(next_word_boundary_char_index(text, CharIndex(1)).0, 3);
330 assert_eq!(next_word_boundary_char_index(text, CharIndex(3)).0, 7);
331 assert_eq!(next_word_boundary_char_index(text, CharIndex(9)).0, 11);
332 assert_eq!(next_word_boundary_char_index(text, CharIndex(12)).0, 13);
333 assert_eq!(next_word_boundary_char_index(text, CharIndex(13)).0, 15);
334 assert_eq!(next_word_boundary_char_index(text, CharIndex(15)).0, 15);
335
336 assert_eq!(next_word_boundary_char_index("", CharIndex(0)).0, 0);
337 assert_eq!(next_word_boundary_char_index("", CharIndex(1)).0, 0);
338
339 let text = "abc.def.ghi";
341 assert_eq!(next_word_boundary_char_index(text, CharIndex(1)).0, 3);
342 assert_eq!(next_word_boundary_char_index(text, CharIndex(3)).0, 7);
343 assert_eq!(next_word_boundary_char_index(text, CharIndex(7)).0, 11);
344
345 let text = "β€οΈπ skvΔlΓ‘ knihovna πβ€οΈ";
351 assert_eq!(next_word_boundary_char_index(text, CharIndex(0)).0, 2);
352 assert_eq!(next_word_boundary_char_index(text, CharIndex(2)).0, 3); assert_eq!(next_word_boundary_char_index(text, CharIndex(6)).0, 10);
354 assert_eq!(next_word_boundary_char_index(text, CharIndex(9)).0, 10);
355 assert_eq!(next_word_boundary_char_index(text, CharIndex(12)).0, 19);
356 assert_eq!(next_word_boundary_char_index(text, CharIndex(15)).0, 19);
357 assert_eq!(next_word_boundary_char_index(text, CharIndex(19)).0, 20);
358 assert_eq!(next_word_boundary_char_index(text, CharIndex(20)).0, 21);
359 }
360
361 #[test]
362 fn test_previous_word() {
363 let text = "abc def ghi";
364 assert_eq!(ccursor_previous_word(text, CCursor::new(7)).index.0, 4);
365 assert_eq!(ccursor_previous_word(text, CCursor::new(5)).index.0, 4);
366 assert_eq!(ccursor_previous_word(text, CCursor::new(4)).index.0, 0);
367 assert_eq!(ccursor_previous_word(text, CCursor::new(0)).index.0, 0);
368 }
369
370 #[test]
371 fn test_next_word() {
372 let text = "abc def ghi";
373 assert_eq!(ccursor_next_word(text, CCursor::new(0)).index.0, 3);
374 assert_eq!(ccursor_next_word(text, CCursor::new(3)).index.0, 7);
375 assert_eq!(ccursor_next_word(text, CCursor::new(7)).index.0, 11);
376 assert_eq!(ccursor_next_word(text, CCursor::new(11)).index.0, 11);
377 }
378
379 #[test]
380 fn test_index_conversion_roundtrip() {
381 let text = "aΓ©πb";
383 let char_count = text.chars().count(); assert_eq!(char_count, 4);
385
386 assert_eq!(byte_index_from_char_index(text, CharIndex(0)).0, 0);
388 assert_eq!(byte_index_from_char_index(text, CharIndex(1)).0, 1);
389 assert_eq!(byte_index_from_char_index(text, CharIndex(2)).0, 3);
390 assert_eq!(byte_index_from_char_index(text, CharIndex(3)).0, 7);
391 assert_eq!(byte_index_from_char_index(text, CharIndex(4)).0, 8);
392 assert_eq!(
394 byte_index_from_char_index(text, CharIndex(99)).0,
395 text.len()
396 );
397
398 assert_eq!(char_index_from_byte_index(text, ByteIndex(0)).0, 0);
400 assert_eq!(char_index_from_byte_index(text, ByteIndex(1)).0, 1);
401 assert_eq!(char_index_from_byte_index(text, ByteIndex(3)).0, 2);
402 assert_eq!(char_index_from_byte_index(text, ByteIndex(7)).0, 3);
403 assert_eq!(char_index_from_byte_index(text, ByteIndex(text.len())).0, 4);
405 assert_eq!(char_index_from_byte_index(text, ByteIndex(99)).0, 4);
407
408 assert_eq!(byte_index_from_char_index("", CharIndex(0)).0, 0);
410 assert_eq!(char_index_from_byte_index("", ByteIndex(0)).0, 0);
411 }
412
413 #[test]
414 fn test_select_word_at() {
415 let text = "hello world";
417 let range = select_word_at(text, CCursor::new(2));
418 let (lo, hi) = (
419 range.primary.index.min(range.secondary.index),
420 range.primary.index.max(range.secondary.index),
421 );
422 assert_eq!(lo.0, 0);
423 assert_eq!(hi.0, 5);
424
425 let range = select_word_at(text, CCursor::new(8));
426 let (lo, hi) = (
427 range.primary.index.min(range.secondary.index),
428 range.primary.index.max(range.secondary.index),
429 );
430 assert_eq!(lo.0, 6);
431 assert_eq!(hi.0, 11);
432 }
433
434 #[test]
435 fn test_word_boundary_large_text_performance() {
436 let large_text = "word ".repeat(200_000); let len = large_text.chars().count();
439
440 let start = std::time::Instant::now();
441
442 let next = ccursor_next_word(&large_text, CCursor::new(len - 10));
443 assert!(next.index.0 <= len);
444
445 let prev = ccursor_previous_word(&large_text, CCursor::new(len - 10));
446 assert!(prev.index.0 < len);
447
448 let range = select_word_at(&large_text, CCursor::new(len - 3));
449 let lo = range.primary.index.min(range.secondary.index);
450 let hi = range.primary.index.max(range.secondary.index);
451 assert!(lo < hi, "Expected a non-empty word selection");
452
453 let elapsed = start.elapsed();
454 assert!(
455 elapsed.as_secs() < 5,
456 "Word boundary operations on 1MB text took {elapsed:?}, expected < 5s"
457 );
458 }
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464
465 #[test]
466 fn test_previous_word_graphemes() {
467 let cases = [
468 ("", 0, 0),
469 ("hello", 0, 0),
470 ("hello", "hello".chars().count(), 0),
471 ("hello world", 6, 0),
472 ("hello world", 8, 6),
473 ("hello world", "hello world".chars().count(), 6),
474 ("hello world ", "hello world ".chars().count(), 6),
475 ("hello world", "hello world".chars().count(), 8),
476 (" ", " ".chars().count(), 0),
477 ("hello, world", "hello, world".chars().count(), 7),
478 ("www.example.com", "www.example.com".chars().count(), 12),
479 ("μλ
! π μΈμ", 8, 6),
480 ("β€οΈπ skvΔlΓ‘ knihovna πβ€οΈ", 18, 11),
481 (
482 "a e\u{301} b",
483 "a e\u{301} b".chars().count(),
484 "a e\u{301} ".chars().count(),
485 ),
486 (
487 "hi π world",
488 "hi π world".chars().count(),
489 "hi π ".chars().count(),
490 ),
491 (
492 "hi π¨βπ©βπ§βπ¦ world",
493 "hi π¨βπ©βπ§βπ¦ world".chars().count(),
494 "hi π¨βπ©βπ§βπ¦ ".chars().count(),
495 ),
496 ];
497
498 for (text, cursor, expected) in cases {
499 let result = ccursor_previous_word(text, CCursor::new(cursor));
500 assert_eq!(
501 result.index.0, expected,
502 "text={text:?}, cursor={cursor}, got={}, expected={expected}",
503 result.index.0
504 );
505 }
506 }
507}