1use std::{
2 borrow::Cow,
3 cmp::Ordering,
4 fmt::Display,
5 ops::Range,
6};
7
8use freya_clipboard::clipboard::Clipboard;
9use freya_core::events::modifiers::ModifiersExt;
10use keyboard_types::{
11 Key,
12 Modifiers,
13 NamedKey,
14};
15use unicode_segmentation::UnicodeSegmentation;
16
17use crate::editor_history::EditorHistory;
18
19#[derive(PartialEq, Clone, Debug, Copy, Hash)]
20pub enum EditorLine {
21 SingleParagraph,
23 Paragraph(usize),
25}
26
27#[derive(Clone, PartialEq, Debug)]
29pub enum TextSelection {
30 Cursor(usize),
31 Range { from: usize, to: usize },
32}
33
34impl TextSelection {
35 pub fn new_cursor(pos: usize) -> Self {
37 Self::Cursor(pos)
38 }
39
40 pub fn new_range((from, to): (usize, usize)) -> Self {
42 Self::Range { from, to }
43 }
44
45 pub fn pos(&self) -> usize {
47 self.end()
48 }
49
50 pub fn set_as_cursor(&mut self) {
52 *self = Self::Cursor(self.end())
53 }
54
55 pub fn set_as_range(&mut self) {
57 *self = Self::Range {
58 from: self.start(),
59 to: self.end(),
60 }
61 }
62
63 pub fn start(&self) -> usize {
65 match self {
66 Self::Cursor(pos) => *pos,
67 Self::Range { from, .. } => *from,
68 }
69 }
70
71 pub fn end(&self) -> usize {
73 match self {
74 Self::Cursor(pos) => *pos,
75 Self::Range { to, .. } => *to,
76 }
77 }
78
79 pub fn move_to(&mut self, position: usize) {
81 match self {
82 Self::Cursor(pos) => *pos = position,
83 Self::Range { to, .. } => {
84 *to = position;
85 }
86 }
87 }
88
89 pub fn is_range(&self) -> bool {
90 matches!(self, Self::Range { .. })
91 }
92}
93
94#[derive(Clone)]
96pub struct Line<'a> {
97 pub text: Cow<'a, str>,
98 pub utf16_len: usize,
99}
100
101impl Line<'_> {
102 pub fn utf16_len(&self) -> usize {
104 self.utf16_len
105 }
106}
107
108impl Display for Line<'_> {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 f.write_str(&self.text)
111 }
112}
113
114bitflags::bitflags! {
115 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
117 pub struct TextEvent: u8 {
118 const CURSOR_CHANGED = 0x01;
120 const TEXT_CHANGED = 0x02;
122 const SELECTION_CHANGED = 0x04;
124 }
125}
126
127pub trait TextEditor {
129 type LinesIterator<'a>: Iterator<Item = Line<'a>>
130 where
131 Self: 'a;
132
133 fn set(&mut self, text: &str);
134
135 fn lines(&self) -> Self::LinesIterator<'_>;
137
138 fn insert_char(&mut self, char: char, char_idx: usize) -> usize;
140
141 fn insert(&mut self, text: &str, char_idx: usize) -> usize;
143
144 fn remove(&mut self, range: Range<usize>) -> usize;
146
147 fn char_to_line(&self, char_idx: usize) -> usize;
149
150 fn line_to_char(&self, line_idx: usize) -> usize;
152
153 fn utf16_cu_to_char(&self, utf16_cu_idx: usize) -> usize;
154
155 fn char_to_utf16_cu(&self, idx: usize) -> usize;
156
157 fn line(&self, line_idx: usize) -> Option<Line<'_>>;
159
160 fn len_lines(&self) -> usize;
162
163 fn len_chars(&self) -> usize;
165
166 fn len_utf16_cu(&self) -> usize;
168
169 fn selection(&self) -> &TextSelection;
171
172 fn selection_mut(&mut self) -> &mut TextSelection;
174
175 fn grapheme_cluster_at(&self, pos_utf16: usize) -> Range<usize> {
177 let line_idx = self.char_to_line(self.utf16_cu_to_char(pos_utf16));
178 let line_start = self.char_to_utf16_cu(self.line_to_char(line_idx));
179 let Some(line) = self.line(line_idx) else {
180 return pos_utf16..pos_utf16;
181 };
182
183 let mut cluster = line_start..line_start;
184 for grapheme in line.text.graphemes(true) {
185 cluster = cluster.end..cluster.end + grapheme.encode_utf16().count();
186 if cluster.end > pos_utf16 {
187 return cluster;
188 }
189 }
190 pos_utf16..pos_utf16
191 }
192
193 fn cursor_row(&self) -> usize {
195 let pos = self.cursor_pos();
196 let pos_utf8 = self.utf16_cu_to_char(pos);
197 self.char_to_line(pos_utf8)
198 }
199
200 fn cursor_col(&self) -> usize {
202 let pos = self.cursor_pos();
203 let pos_utf8 = self.utf16_cu_to_char(pos);
204 let line = self.char_to_line(pos_utf8);
205 let line_char_utf8 = self.line_to_char(line);
206 let line_char = self.char_to_utf16_cu(line_char_utf8);
207 pos - line_char
208 }
209
210 fn line_end_position(&self, row: usize) -> Option<usize> {
212 let line = self.line(row)?;
213 let row_start = self.char_to_utf16_cu(self.line_to_char(row));
214 let row_end = row_start + line.utf16_len();
215 if row + 1 == self.len_lines() {
216 Some(row_end)
217 } else {
218 Some(self.grapheme_cluster_at(row_end - 1).start)
219 }
220 }
221
222 fn move_cursor_to_row(&mut self, row: usize, col: usize) {
225 let Some(last_position) = self.line_end_position(row) else {
226 return;
227 };
228 let row_start = self.char_to_utf16_cu(self.line_to_char(row));
229 let col = col.min(last_position - row_start);
230 let pos = self.grapheme_cluster_at(row_start + col).start;
231 self.selection_mut().move_to(pos);
232 }
233
234 fn cursor_down(&mut self) -> bool {
236 let old_row = self.cursor_row();
237 let old_col = self.cursor_col();
238
239 match old_row.cmp(&(self.len_lines() - 1)) {
240 Ordering::Less => {
241 self.move_cursor_to_row(old_row + 1, old_col);
242
243 true
244 }
245 Ordering::Equal => {
246 let end = self.len_utf16_cu();
247 self.selection_mut().move_to(end);
248
249 true
250 }
251 Ordering::Greater => false,
252 }
253 }
254
255 fn cursor_up(&mut self) -> bool {
257 let pos = self.cursor_pos();
258 let old_row = self.cursor_row();
259 let old_col = self.cursor_col();
260
261 if pos > 0 {
262 if old_row == 0 {
263 self.selection_mut().move_to(0);
264 } else {
265 self.move_cursor_to_row(old_row - 1, old_col);
266 }
267
268 true
269 } else {
270 false
271 }
272 }
273
274 fn cursor_right(&mut self) -> bool {
276 if self.cursor_pos() < self.len_utf16_cu() {
277 let to = self.grapheme_cluster_at(self.selection().end()).end;
278 self.selection_mut().move_to(to);
279
280 true
281 } else {
282 false
283 }
284 }
285
286 fn cursor_left(&mut self) -> bool {
288 if self.cursor_pos() > 0 {
289 let to = self.grapheme_cluster_at(self.selection().end() - 1).start;
290 self.selection_mut().move_to(to);
291
292 true
293 } else {
294 false
295 }
296 }
297
298 fn next_word_pos(&self, pos: usize) -> Option<usize> {
300 let len = self.len_utf16_cu();
301 if pos >= len {
302 return None;
303 }
304
305 let start_char = self.utf16_cu_to_char(pos);
307 let initial_line = self.char_to_line(start_char);
308 let initial_offset = start_char - self.line_to_char(initial_line);
309
310 for line_idx in initial_line..self.len_lines() {
311 let Some(line) = self.line(line_idx) else {
312 continue;
313 };
314 let line_char_offset = self.line_to_char(line_idx);
315 let from = if line_idx == initial_line {
316 initial_offset
317 } else {
318 0
319 };
320
321 let mut char_offset = 0;
323 for word in line.text.split_word_bounds() {
324 char_offset += word.chars().count();
325 if char_offset > from && !word.chars().all(char::is_whitespace) {
326 return Some(self.char_to_utf16_cu(line_char_offset + char_offset));
327 }
328 }
329 }
330
331 Some(len)
333 }
334
335 fn prev_word_pos(&self, pos: usize) -> Option<usize> {
337 if pos == 0 {
338 return None;
339 }
340
341 let start_char = self.utf16_cu_to_char(pos);
343 let initial_line = self.char_to_line(start_char);
344 let initial_offset = start_char - self.line_to_char(initial_line);
345
346 for line_idx in (0..=initial_line).rev() {
347 let Some(line) = self.line(line_idx) else {
348 continue;
349 };
350 let line_char_offset = self.line_to_char(line_idx);
351 let to = if line_idx == initial_line {
352 initial_offset
353 } else {
354 line.text.chars().count()
355 };
356
357 let mut char_offset = 0;
359 let mut last_word_start = None;
360 for word in line.text.split_word_bounds() {
361 if char_offset >= to {
362 break;
363 }
364 if !word.chars().all(char::is_whitespace) {
365 last_word_start = Some(char_offset);
366 }
367 char_offset += word.chars().count();
368 }
369
370 if let Some(start) = last_word_start {
371 return Some(self.char_to_utf16_cu(line_char_offset + start));
372 }
373 }
374
375 Some(0)
377 }
378
379 fn cursor_word_right(&mut self) -> bool {
381 if let Some(new_pos) = self.next_word_pos(self.cursor_pos()) {
382 self.selection_mut().move_to(new_pos);
383 true
384 } else {
385 false
386 }
387 }
388
389 fn cursor_word_left(&mut self) -> bool {
391 if let Some(new_pos) = self.prev_word_pos(self.cursor_pos()) {
392 self.selection_mut().move_to(new_pos);
393 true
394 } else {
395 false
396 }
397 }
398
399 fn cursor_pos(&self) -> usize {
401 self.selection().pos()
402 }
403
404 fn move_cursor_to(&mut self, pos: usize) {
406 self.selection_mut().move_to(pos);
407 }
408
409 fn has_any_selection(&self) -> bool;
411
412 fn get_selection(&self) -> Option<(usize, usize)>;
414
415 fn get_visible_selection(&self, editor_line: EditorLine) -> Option<(usize, usize)> {
417 let (selected_from, selected_to) = match self.selection() {
418 TextSelection::Cursor(_) => return None,
419 TextSelection::Range { from, to } => (*from, *to),
420 };
421
422 match editor_line {
423 EditorLine::Paragraph(line_index) => {
424 let selected_from_row = self.char_to_line(self.utf16_cu_to_char(selected_from));
425 let selected_to_row = self.char_to_line(self.utf16_cu_to_char(selected_to));
426
427 let editor_row_idx = self.char_to_utf16_cu(self.line_to_char(line_index));
428 let selected_from_row_idx =
429 self.char_to_utf16_cu(self.line_to_char(selected_from_row));
430 let selected_to_row_idx = self.char_to_utf16_cu(self.line_to_char(selected_to_row));
431
432 let selected_from_col_idx = selected_from - selected_from_row_idx;
433 let selected_to_col_idx = selected_to - selected_to_row_idx;
434
435 if (line_index > selected_from_row && line_index < selected_to_row)
437 || (line_index < selected_from_row && line_index > selected_to_row)
438 {
439 let len = self.line(line_index).unwrap().utf16_len();
440 return Some((0, len));
441 }
442
443 match selected_from_row.cmp(&selected_to_row) {
444 Ordering::Greater => {
446 if selected_from_row == line_index {
447 Some((0, selected_from_col_idx))
449 } else if selected_to_row == line_index {
450 let len = self.line(selected_to_row).unwrap().utf16_len();
452 Some((selected_to_col_idx, len))
453 } else {
454 None
455 }
456 }
457 Ordering::Less => {
459 if selected_from_row == line_index {
460 let len = self.line(selected_from_row).unwrap().utf16_len();
462 Some((selected_from_col_idx, len))
463 } else if selected_to_row == line_index {
464 Some((0, selected_to_col_idx))
466 } else {
467 None
468 }
469 }
470 Ordering::Equal if selected_from_row == line_index => {
471 Some((selected_from - editor_row_idx, selected_to - editor_row_idx))
473 }
474 _ => None,
475 }
476 }
477 EditorLine::SingleParagraph => Some((selected_from, selected_to)),
478 }
479 }
480
481 fn clear_selection(&mut self);
483
484 fn set_selection(&mut self, selected: (usize, usize));
486
487 fn measure_selection(&self, to: usize, line_index: EditorLine) -> TextSelection {
490 let mut selection = self.selection().clone();
491
492 match line_index {
493 EditorLine::Paragraph(line_index) => {
494 let row_char = self.line_to_char(line_index);
495 let pos = self.char_to_utf16_cu(row_char) + to;
496 selection.move_to(pos);
497 }
498 EditorLine::SingleParagraph => {
499 selection.move_to(to);
500 }
501 }
502
503 selection
504 }
505
506 fn process_key(
508 &mut self,
509 key: &Key,
510 modifiers: &Modifiers,
511 allow_tabs: bool,
512 allow_changes: bool,
513 allow_read_clipboard: bool,
514 allow_write_clipboard: bool,
515 ) -> TextEvent {
516 let mut event = TextEvent::empty();
517
518 let selection = self.get_selection();
519 let skip_arrows_movement = !modifiers.contains(Modifiers::SHIFT) && selection.is_some();
520 let word_jump = modifiers.contains(Modifiers::ctrl_or_alt());
521
522 match key {
523 Key::Named(NamedKey::Shift) => {}
524 Key::Named(NamedKey::Control) => {}
525 Key::Named(NamedKey::Alt) => {}
526 Key::Named(NamedKey::Escape) => {
527 self.clear_selection();
528 }
529 Key::Named(NamedKey::ArrowDown) => {
530 if modifiers.contains(Modifiers::SHIFT) {
531 self.selection_mut().set_as_range();
532 } else {
533 self.selection_mut().set_as_cursor();
534 }
535
536 if !skip_arrows_movement && self.cursor_down() {
537 event.insert(TextEvent::CURSOR_CHANGED);
538 }
539 }
540 Key::Named(NamedKey::ArrowLeft) => {
541 if modifiers.contains(Modifiers::SHIFT) {
542 self.selection_mut().set_as_range();
543 } else {
544 self.selection_mut().set_as_cursor();
545 }
546
547 let moved = !skip_arrows_movement
548 && if word_jump {
549 self.cursor_word_left()
550 } else {
551 self.cursor_left()
552 };
553
554 if moved {
555 event.insert(TextEvent::CURSOR_CHANGED);
556 }
557 }
558 Key::Named(NamedKey::ArrowRight) => {
559 if modifiers.contains(Modifiers::SHIFT) {
560 self.selection_mut().set_as_range();
561 } else {
562 self.selection_mut().set_as_cursor();
563 }
564
565 let moved = !skip_arrows_movement
566 && if word_jump {
567 self.cursor_word_right()
568 } else {
569 self.cursor_right()
570 };
571
572 if moved {
573 event.insert(TextEvent::CURSOR_CHANGED);
574 }
575 }
576 Key::Named(NamedKey::ArrowUp) => {
577 if modifiers.contains(Modifiers::SHIFT) {
578 self.selection_mut().set_as_range();
579 } else {
580 self.selection_mut().set_as_cursor();
581 }
582
583 if !skip_arrows_movement && self.cursor_up() {
584 event.insert(TextEvent::CURSOR_CHANGED);
585 }
586 }
587 Key::Named(named_key @ (NamedKey::Home | NamedKey::End)) => {
588 if modifiers.contains(Modifiers::SHIFT) {
589 self.selection_mut().set_as_range();
590 } else {
591 self.selection_mut().set_as_cursor();
592 }
593
594 let whole_text = modifiers.contains(Modifiers::ctrl_or_meta());
595 let pos = match (named_key, whole_text) {
596 (NamedKey::Home, true) => 0,
597 (NamedKey::Home, false) => {
598 self.char_to_utf16_cu(self.line_to_char(self.cursor_row()))
599 }
600 (_, true) => self.len_utf16_cu(),
601 (_, false) => self
602 .line_end_position(self.cursor_row())
603 .unwrap_or_else(|| self.len_utf16_cu()),
604 };
605
606 if pos != self.cursor_pos() {
607 self.selection_mut().move_to(pos);
608 event.insert(TextEvent::CURSOR_CHANGED);
609 }
610 }
611 Key::Named(NamedKey::Backspace) if allow_changes => {
612 let cursor_pos = self.cursor_pos();
613
614 let removal = if let Some((start, end)) = self.get_selection_range() {
615 Some(start..end)
616 } else if word_jump {
617 self.prev_word_pos(cursor_pos)
618 .map(|start| start..cursor_pos)
619 } else if cursor_pos > 0 {
620 Some(self.grapheme_cluster_at(cursor_pos - 1).start..cursor_pos)
621 } else {
622 None
623 };
624
625 if let Some(removal) = removal {
626 let end = removal.end;
627 let removed_text_len = self.remove(removal);
628 self.move_cursor_to(end - removed_text_len);
629 event.insert(TextEvent::TEXT_CHANGED);
630 }
631 }
632 Key::Named(NamedKey::Delete) if allow_changes => {
633 let cursor_pos = self.cursor_pos();
634
635 let removal = if let Some((start, end)) = self.get_selection_range() {
636 Some(start..end)
637 } else if word_jump {
638 self.next_word_pos(cursor_pos).map(|end| cursor_pos..end)
639 } else if cursor_pos < self.len_utf16_cu() {
640 Some(cursor_pos..self.grapheme_cluster_at(cursor_pos).end)
641 } else {
642 None
643 };
644
645 if let Some(removal) = removal {
646 let start = removal.start;
647 self.remove(removal);
648 self.move_cursor_to(start);
649 event.insert(TextEvent::TEXT_CHANGED);
650 }
651 }
652 Key::Named(NamedKey::Enter) if allow_changes => {
653 let cursor_pos = self.cursor_pos();
655 self.insert_char('\n', cursor_pos);
656 self.cursor_right();
657
658 event.insert(TextEvent::TEXT_CHANGED);
659 }
660 Key::Named(NamedKey::Tab) if allow_tabs && allow_changes => {
661 let text = " ".repeat(self.get_indentation().into());
663 let cursor_pos = self.cursor_pos();
664 self.insert(&text, cursor_pos);
665 self.move_cursor_to(cursor_pos + text.chars().count());
666
667 event.insert(TextEvent::TEXT_CHANGED);
668 }
669 Key::Character(character) => {
670 let meta_or_ctrl = modifiers.contains(Modifiers::ctrl_or_meta());
671
672 match character.as_str() {
673 " " if allow_changes => {
674 let selection = self.get_selection_range();
675 if let Some((start, end)) = selection {
676 self.remove(start..end);
677 self.move_cursor_to(start);
678 event.insert(TextEvent::TEXT_CHANGED);
679 }
680
681 let cursor_pos = self.cursor_pos();
683 self.insert_char(' ', cursor_pos);
684 self.cursor_right();
685
686 event.insert(TextEvent::TEXT_CHANGED);
687 }
688
689 "a" if meta_or_ctrl => {
691 let len = self.len_utf16_cu();
692 self.set_selection((0, len));
693 }
694
695 "c" if meta_or_ctrl && allow_write_clipboard => {
697 let selected = self.get_selected_text();
698 if let Some(selected) = selected {
699 Clipboard::set(selected).ok();
700 }
701 }
702
703 "x" if meta_or_ctrl && allow_changes && allow_write_clipboard => {
705 let selection = self.get_selection_range();
706 if let Some((start, end)) = selection {
707 let text = self.get_selected_text().unwrap();
708 self.remove(start..end);
709 Clipboard::set(text).ok();
710 self.move_cursor_to(start);
711 event.insert(TextEvent::TEXT_CHANGED);
712 }
713 }
714
715 "v" if meta_or_ctrl && allow_changes && allow_read_clipboard => {
717 if let Ok(copied_text) = Clipboard::get() {
718 let selection = self.get_selection_range();
719 if let Some((start, end)) = selection {
720 self.remove(start..end);
721 self.move_cursor_to(start);
722 }
723 let cursor_pos = self.cursor_pos();
724 self.insert(&copied_text, cursor_pos);
725 let last_idx = copied_text.encode_utf16().count() + cursor_pos;
726 self.move_cursor_to(last_idx);
727 event.insert(TextEvent::TEXT_CHANGED);
728 }
729 }
730
731 "z" if meta_or_ctrl && allow_changes => {
733 let undo_result = self.undo();
734
735 if let Some(selection) = undo_result {
736 *self.selection_mut() = selection;
737 event.insert(TextEvent::TEXT_CHANGED);
738 event.insert(TextEvent::SELECTION_CHANGED);
739 }
740 }
741
742 "y" if meta_or_ctrl && allow_changes => {
744 let redo_result = self.redo();
745
746 if let Some(selection) = redo_result {
747 *self.selection_mut() = selection;
748 event.insert(TextEvent::TEXT_CHANGED);
749 event.insert(TextEvent::SELECTION_CHANGED);
750 }
751 }
752
753 _ if allow_changes => {
754 let selection = self.get_selection_range();
756 if let Some((start, end)) = selection {
757 self.remove(start..end);
758 self.move_cursor_to(start);
759 event.insert(TextEvent::TEXT_CHANGED);
760 }
761
762 if let Ok(ch) = character.parse::<char>() {
763 let cursor_pos = self.cursor_pos();
765 let inserted_text_len = self.insert_char(ch, cursor_pos);
766 self.move_cursor_to(cursor_pos + inserted_text_len);
767 event.insert(TextEvent::TEXT_CHANGED);
768 } else {
769 let cursor_pos = self.cursor_pos();
771 let inserted_text_len = self.insert(character, cursor_pos);
772 self.move_cursor_to(cursor_pos + inserted_text_len);
773 event.insert(TextEvent::TEXT_CHANGED);
774 }
775 }
776 _ => {}
777 }
778 }
779 _ => {}
780 }
781
782 if event.contains(TextEvent::TEXT_CHANGED) && !event.contains(TextEvent::SELECTION_CHANGED)
783 {
784 self.clear_selection();
785 }
786
787 if self.get_selection() != selection {
788 event.insert(TextEvent::SELECTION_CHANGED);
789 }
790
791 event
792 }
793
794 fn get_selected_text(&self) -> Option<String>;
795
796 fn undo(&mut self) -> Option<TextSelection>;
797
798 fn redo(&mut self) -> Option<TextSelection>;
799
800 fn editor_history(&self) -> &EditorHistory;
801
802 fn editor_history_mut(&mut self) -> &mut EditorHistory;
803
804 fn get_selection_range(&self) -> Option<(usize, usize)>;
805
806 fn get_indentation(&self) -> u8;
807
808 fn find_word_boundaries(&self, pos: usize) -> (usize, usize) {
809 let pos_char = self.utf16_cu_to_char(pos);
810 let len_chars = self.len_chars();
811
812 if len_chars == 0 {
813 return (pos, pos);
814 }
815
816 let line_idx = self.char_to_line(pos_char);
818 let line_char = self.line_to_char(line_idx);
819 let line = self.line(line_idx).unwrap();
820
821 let line_str: std::borrow::Cow<str> = line.text;
822 let pos_in_line = pos_char - line_char;
823
824 let mut char_offset = 0;
826 for word in line_str.split_word_bounds() {
827 let word_char_len = word.chars().count();
828 let word_start = char_offset;
829 let word_end = char_offset + word_char_len;
830
831 if pos_in_line >= word_start && pos_in_line < word_end {
832 let start_char = line_char + word_start;
833 let end_char = line_char + word_end;
834 return (
835 self.char_to_utf16_cu(start_char),
836 self.char_to_utf16_cu(end_char),
837 );
838 }
839
840 char_offset = word_end;
841 }
842
843 (pos, pos)
844 }
845}