freya_edit/
text_editor.rs

1use std::{
2    borrow::Cow,
3    cmp::Ordering,
4    fmt::Display,
5    ops::Range,
6};
7
8use freya_clipboard::clipboard::Clipboard;
9use keyboard_types::{
10    Key,
11    Modifiers,
12    NamedKey,
13};
14use unicode_segmentation::UnicodeSegmentation;
15
16use crate::editor_history::EditorHistory;
17
18#[derive(PartialEq, Clone, Debug, Copy, Hash)]
19pub enum EditorLine {
20    /// Only one `paragraph` element exists in the whole editor.
21    SingleParagraph,
22    /// There are multiple `paragraph` elements in the editor, one per line.
23    Paragraph(usize),
24}
25
26/// Holds the position of a cursor in a text
27#[derive(Clone, PartialEq, Debug)]
28pub enum TextSelection {
29    Cursor(usize),
30    Range { from: usize, to: usize },
31}
32
33impl TextSelection {
34    /// Create a new [TextSelection::Cursor]
35    pub fn new_cursor(pos: usize) -> Self {
36        Self::Cursor(pos)
37    }
38
39    /// Create a new [TextSelection::Range]
40    pub fn new_range((from, to): (usize, usize)) -> Self {
41        Self::Range { from, to }
42    }
43
44    /// Get the position
45    pub fn pos(&self) -> usize {
46        self.end()
47    }
48
49    /// Set the selection as a cursor
50    pub fn set_as_cursor(&mut self) {
51        *self = Self::Cursor(self.end())
52    }
53
54    /// Set the selection as a range
55    pub fn set_as_range(&mut self) {
56        *self = Self::Range {
57            from: self.start(),
58            to: self.end(),
59        }
60    }
61
62    /// Get the start of the cursor position.
63    pub fn start(&self) -> usize {
64        match self {
65            Self::Cursor(pos) => *pos,
66            Self::Range { from, .. } => *from,
67        }
68    }
69
70    /// Get the end of the cursor position.
71    pub fn end(&self) -> usize {
72        match self {
73            Self::Cursor(pos) => *pos,
74            Self::Range { to, .. } => *to,
75        }
76    }
77
78    /// Move the end position of the cursor.
79    pub fn move_to(&mut self, position: usize) {
80        match self {
81            Self::Cursor(pos) => *pos = position,
82            Self::Range { to, .. } => {
83                *to = position;
84            }
85        }
86    }
87
88    pub fn is_range(&self) -> bool {
89        matches!(self, Self::Range { .. })
90    }
91}
92
93/// A text line from a [TextEditor]
94#[derive(Clone)]
95pub struct Line<'a> {
96    pub text: Cow<'a, str>,
97    pub utf16_len: usize,
98}
99
100impl Line<'_> {
101    /// Get the length of the line
102    pub fn utf16_len(&self) -> usize {
103        self.utf16_len
104    }
105}
106
107impl Display for Line<'_> {
108    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109        f.write_str(&self.text)
110    }
111}
112
113bitflags::bitflags! {
114    /// Events for [TextEditor]
115    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
116    pub struct TextEvent: u8 {
117         /// Cursor position has been moved
118        const CURSOR_CHANGED = 0x01;
119        /// Text has changed
120        const TEXT_CHANGED = 0x02;
121        /// Selected text has changed
122        const SELECTION_CHANGED = 0x04;
123    }
124}
125
126/// Common trait for editable texts
127pub trait TextEditor {
128    type LinesIterator<'a>: Iterator<Item = Line<'a>>
129    where
130        Self: 'a;
131
132    fn set(&mut self, text: &str);
133
134    /// Iterator over all the lines in the text.
135    fn lines(&self) -> Self::LinesIterator<'_>;
136
137    /// Insert a character in the text in the given position.
138    fn insert_char(&mut self, char: char, char_idx: usize) -> usize;
139
140    /// Insert a string in the text in the given position.
141    fn insert(&mut self, text: &str, char_idx: usize) -> usize;
142
143    /// Remove a part of the text.
144    fn remove(&mut self, range: Range<usize>) -> usize;
145
146    /// Get line from the given char
147    fn char_to_line(&self, char_idx: usize) -> usize;
148
149    /// Get the first char from the given line
150    fn line_to_char(&self, line_idx: usize) -> usize;
151
152    fn utf16_cu_to_char(&self, utf16_cu_idx: usize) -> usize;
153
154    fn char_to_utf16_cu(&self, idx: usize) -> usize;
155
156    /// Get a line from the text
157    fn line(&self, line_idx: usize) -> Option<Line<'_>>;
158
159    /// Total of lines
160    fn len_lines(&self) -> usize;
161
162    /// Total of chars
163    fn len_chars(&self) -> usize;
164
165    /// Total of utf16 code units
166    fn len_utf16_cu(&self) -> usize;
167
168    /// Get a readable text selection
169    fn selection(&self) -> &TextSelection;
170
171    /// Get a mutable reference to text selection
172    fn selection_mut(&mut self) -> &mut TextSelection;
173
174    /// Get the cursor row
175    fn cursor_row(&self) -> usize {
176        let pos = self.cursor_pos();
177        let pos_utf8 = self.utf16_cu_to_char(pos);
178        self.char_to_line(pos_utf8)
179    }
180
181    /// Get the cursor column
182    fn cursor_col(&self) -> usize {
183        let pos = self.cursor_pos();
184        let pos_utf8 = self.utf16_cu_to_char(pos);
185        let line = self.char_to_line(pos_utf8);
186        let line_char_utf8 = self.line_to_char(line);
187        let line_char = self.char_to_utf16_cu(line_char_utf8);
188        pos - line_char
189    }
190
191    /// Move the cursor 1 line down
192    fn cursor_down(&mut self) -> bool {
193        let old_row = self.cursor_row();
194        let old_col = self.cursor_col();
195
196        match old_row.cmp(&(self.len_lines() - 1)) {
197            Ordering::Less => {
198                // One line below
199                let new_row = old_row + 1;
200                let new_row_char = self.char_to_utf16_cu(self.line_to_char(new_row));
201                let new_row_len = self.line(new_row).unwrap().utf16_len();
202                let new_col = old_col.min(new_row_len.saturating_sub(1));
203                self.selection_mut().move_to(new_row_char + new_col);
204
205                true
206            }
207            Ordering::Equal => {
208                let end = self.len_utf16_cu();
209                // Reached max
210                self.selection_mut().move_to(end);
211
212                true
213            }
214            Ordering::Greater => {
215                // Can't go further
216
217                false
218            }
219        }
220    }
221
222    /// Move the cursor 1 line up
223    fn cursor_up(&mut self) -> bool {
224        let pos = self.cursor_pos();
225        let old_row = self.cursor_row();
226        let old_col = self.cursor_col();
227
228        if pos > 0 {
229            // Reached max
230            if old_row == 0 {
231                self.selection_mut().move_to(0);
232            } else {
233                let new_row = old_row - 1;
234                let new_row_char = self.char_to_utf16_cu(self.line_to_char(new_row));
235                let new_row_len = self.line(new_row).unwrap().utf16_len();
236                let new_col = old_col.min(new_row_len.saturating_sub(1));
237                self.selection_mut().move_to(new_row_char + new_col);
238            }
239
240            true
241        } else {
242            false
243        }
244    }
245
246    /// Move the cursor 1 char to the right
247    fn cursor_right(&mut self) -> bool {
248        if self.cursor_pos() < self.len_utf16_cu() {
249            let to = self.selection().end() + 1;
250            self.selection_mut().move_to(to);
251
252            true
253        } else {
254            false
255        }
256    }
257
258    /// Move the cursor 1 char to the left
259    fn cursor_left(&mut self) -> bool {
260        if self.cursor_pos() > 0 {
261            let to = self.selection().end() - 1;
262            self.selection_mut().move_to(to);
263
264            true
265        } else {
266            false
267        }
268    }
269
270    /// Get the cursor position
271    fn cursor_pos(&self) -> usize {
272        self.selection().pos()
273    }
274
275    /// Move the cursor position
276    fn move_cursor_to(&mut self, pos: usize) {
277        self.selection_mut().move_to(pos);
278    }
279
280    // Check if has any selection at all
281    fn has_any_selection(&self) -> bool;
282
283    // Return the selected text
284    fn get_selection(&self) -> Option<(usize, usize)>;
285
286    // Return the visible selected text for the given editor line
287    fn get_visible_selection(&self, editor_line: EditorLine) -> Option<(usize, usize)> {
288        let (selected_from, selected_to) = match self.selection() {
289            TextSelection::Cursor(_) => return None,
290            TextSelection::Range { from, to } => (*from, *to),
291        };
292
293        match editor_line {
294            EditorLine::Paragraph(line_index) => {
295                let selected_from_row = self.char_to_line(self.utf16_cu_to_char(selected_from));
296                let selected_to_row = self.char_to_line(self.utf16_cu_to_char(selected_to));
297
298                let editor_row_idx = self.char_to_utf16_cu(self.line_to_char(line_index));
299                let selected_from_row_idx =
300                    self.char_to_utf16_cu(self.line_to_char(selected_from_row));
301                let selected_to_row_idx = self.char_to_utf16_cu(self.line_to_char(selected_to_row));
302
303                let selected_from_col_idx = selected_from - selected_from_row_idx;
304                let selected_to_col_idx = selected_to - selected_to_row_idx;
305
306                // Between starting line and endling line
307                if (line_index > selected_from_row && line_index < selected_to_row)
308                    || (line_index < selected_from_row && line_index > selected_to_row)
309                {
310                    let len = self.line(line_index).unwrap().utf16_len();
311                    return Some((0, len));
312                }
313
314                match selected_from_row.cmp(&selected_to_row) {
315                    // Selection direction is from bottom -> top
316                    Ordering::Greater => {
317                        if selected_from_row == line_index {
318                            // Starting line
319                            Some((0, selected_from_col_idx))
320                        } else if selected_to_row == line_index {
321                            // Ending line
322                            let len = self.line(selected_to_row).unwrap().utf16_len();
323                            Some((selected_to_col_idx, len))
324                        } else {
325                            None
326                        }
327                    }
328                    // Selection direction is from top -> bottom
329                    Ordering::Less => {
330                        if selected_from_row == line_index {
331                            // Starting line
332                            let len = self.line(selected_from_row).unwrap().utf16_len();
333                            Some((selected_from_col_idx, len))
334                        } else if selected_to_row == line_index {
335                            // Ending line
336                            Some((0, selected_to_col_idx))
337                        } else {
338                            None
339                        }
340                    }
341                    Ordering::Equal if selected_from_row == line_index => {
342                        // Starting and endline line are the same
343                        Some((selected_from - editor_row_idx, selected_to - editor_row_idx))
344                    }
345                    _ => None,
346                }
347            }
348            EditorLine::SingleParagraph => Some((selected_from, selected_to)),
349        }
350    }
351
352    // Remove the selection
353    fn clear_selection(&mut self);
354
355    // Select some text
356    fn set_selection(&mut self, selected: (usize, usize));
357
358    // Measure a new text selection
359
360    fn measure_selection(&self, to: usize, line_index: EditorLine) -> TextSelection {
361        let mut selection = self.selection().clone();
362
363        match line_index {
364            EditorLine::Paragraph(line_index) => {
365                let row_char = self.line_to_char(line_index);
366                let pos = self.char_to_utf16_cu(row_char) + to;
367                selection.move_to(pos);
368            }
369            EditorLine::SingleParagraph => {
370                selection.move_to(to);
371            }
372        }
373
374        selection
375    }
376
377    // Process a Keyboard event
378    fn process_key(
379        &mut self,
380        key: &Key,
381        modifiers: &Modifiers,
382        allow_tabs: bool,
383        allow_changes: bool,
384        allow_clipboard: bool,
385    ) -> TextEvent {
386        let mut event = TextEvent::empty();
387
388        let selection = self.get_selection();
389        let skip_arrows_movement = !modifiers.contains(Modifiers::SHIFT) && selection.is_some();
390
391        match key {
392            Key::Named(NamedKey::Shift) => {}
393            Key::Named(NamedKey::Control) => {}
394            Key::Named(NamedKey::Alt) => {}
395            Key::Named(NamedKey::Escape) => {
396                self.clear_selection();
397            }
398            Key::Named(NamedKey::ArrowDown) => {
399                if modifiers.contains(Modifiers::SHIFT) {
400                    self.selection_mut().set_as_range();
401                } else {
402                    self.selection_mut().set_as_cursor();
403                }
404
405                if !skip_arrows_movement && self.cursor_down() {
406                    event.insert(TextEvent::CURSOR_CHANGED);
407                }
408            }
409            Key::Named(NamedKey::ArrowLeft) => {
410                if modifiers.contains(Modifiers::SHIFT) {
411                    self.selection_mut().set_as_range();
412                } else {
413                    self.selection_mut().set_as_cursor();
414                }
415
416                if !skip_arrows_movement && self.cursor_left() {
417                    event.insert(TextEvent::CURSOR_CHANGED);
418                }
419            }
420            Key::Named(NamedKey::ArrowRight) => {
421                if modifiers.contains(Modifiers::SHIFT) {
422                    self.selection_mut().set_as_range();
423                } else {
424                    self.selection_mut().set_as_cursor();
425                }
426
427                if !skip_arrows_movement && self.cursor_right() {
428                    event.insert(TextEvent::CURSOR_CHANGED);
429                }
430            }
431            Key::Named(NamedKey::ArrowUp) => {
432                if modifiers.contains(Modifiers::SHIFT) {
433                    self.selection_mut().set_as_range();
434                } else {
435                    self.selection_mut().set_as_cursor();
436                }
437
438                if !skip_arrows_movement && self.cursor_up() {
439                    event.insert(TextEvent::CURSOR_CHANGED);
440                }
441            }
442            Key::Named(NamedKey::Backspace) if allow_changes => {
443                let cursor_pos = self.cursor_pos();
444                let selection = self.get_selection_range();
445
446                if let Some((start, end)) = selection {
447                    self.remove(start..end);
448                    self.move_cursor_to(start);
449                    event.insert(TextEvent::TEXT_CHANGED);
450                } else if cursor_pos > 0 {
451                    // Remove the character to the left if there is any
452                    let removed_text_len = self.remove(cursor_pos - 1..cursor_pos);
453                    self.move_cursor_to(cursor_pos - removed_text_len);
454                    event.insert(TextEvent::TEXT_CHANGED);
455                }
456            }
457            Key::Named(NamedKey::Delete) if allow_changes => {
458                let cursor_pos = self.cursor_pos();
459                let selection = self.get_selection_range();
460
461                if let Some((start, end)) = selection {
462                    self.remove(start..end);
463                    self.move_cursor_to(start);
464                    event.insert(TextEvent::TEXT_CHANGED);
465                } else if cursor_pos < self.len_utf16_cu() {
466                    // Remove the character to the right if there is any
467                    self.remove(cursor_pos..cursor_pos + 1);
468                    event.insert(TextEvent::TEXT_CHANGED);
469                }
470            }
471            Key::Named(NamedKey::Enter) if allow_changes => {
472                // Breaks the line
473                let cursor_pos = self.cursor_pos();
474                self.insert_char('\n', cursor_pos);
475                self.cursor_right();
476
477                event.insert(TextEvent::TEXT_CHANGED);
478            }
479            Key::Named(NamedKey::Tab) if allow_tabs && allow_changes => {
480                // Inserts a tab
481                let text = " ".repeat(self.get_indentation().into());
482                let cursor_pos = self.cursor_pos();
483                self.insert(&text, cursor_pos);
484                self.move_cursor_to(cursor_pos + text.chars().count());
485
486                event.insert(TextEvent::TEXT_CHANGED);
487            }
488            Key::Character(character) => {
489                let meta_or_ctrl = if cfg!(target_os = "macos") {
490                    modifiers.meta()
491                } else {
492                    modifiers.ctrl()
493                };
494
495                match character.as_str() {
496                    " " if allow_changes => {
497                        let selection = self.get_selection_range();
498                        if let Some((start, end)) = selection {
499                            self.remove(start..end);
500                            self.move_cursor_to(start);
501                            event.insert(TextEvent::TEXT_CHANGED);
502                        }
503
504                        // Simply adds an space
505                        let cursor_pos = self.cursor_pos();
506                        self.insert_char(' ', cursor_pos);
507                        self.cursor_right();
508
509                        event.insert(TextEvent::TEXT_CHANGED);
510                    }
511
512                    // Select all text
513                    "a" if meta_or_ctrl => {
514                        let len = self.len_utf16_cu();
515                        self.set_selection((0, len));
516                    }
517
518                    // Copy selected text
519                    "c" if meta_or_ctrl && allow_clipboard => {
520                        let selected = self.get_selected_text();
521                        if let Some(selected) = selected {
522                            Clipboard::set(selected).ok();
523                        }
524                    }
525
526                    // Cut selected text
527                    "x" if meta_or_ctrl && allow_changes && allow_clipboard => {
528                        let selection = self.get_selection_range();
529                        if let Some((start, end)) = selection {
530                            let text = self.get_selected_text().unwrap();
531                            self.remove(start..end);
532                            Clipboard::set(text).ok();
533                            self.move_cursor_to(start);
534                            event.insert(TextEvent::TEXT_CHANGED);
535                        }
536                    }
537
538                    // Paste copied text
539                    "v" if meta_or_ctrl && allow_changes && allow_clipboard => {
540                        if let Ok(copied_text) = Clipboard::get() {
541                            let selection = self.get_selection_range();
542                            if let Some((start, end)) = selection {
543                                self.remove(start..end);
544                                self.move_cursor_to(start);
545                            }
546                            let cursor_pos = self.cursor_pos();
547                            self.insert(&copied_text, cursor_pos);
548                            let last_idx = copied_text.encode_utf16().count() + cursor_pos;
549                            self.move_cursor_to(last_idx);
550                            event.insert(TextEvent::TEXT_CHANGED);
551                        }
552                    }
553
554                    // Undo last change
555                    "z" if meta_or_ctrl && allow_changes => {
556                        let undo_result = self.undo();
557
558                        if let Some(selection) = undo_result {
559                            *self.selection_mut() = selection;
560                            event.insert(TextEvent::TEXT_CHANGED);
561                            event.insert(TextEvent::SELECTION_CHANGED);
562                        }
563                    }
564
565                    // Redo last change
566                    "y" if meta_or_ctrl && allow_changes => {
567                        let redo_result = self.redo();
568
569                        if let Some(selection) = redo_result {
570                            *self.selection_mut() = selection;
571                            event.insert(TextEvent::TEXT_CHANGED);
572                            event.insert(TextEvent::SELECTION_CHANGED);
573                        }
574                    }
575
576                    _ if allow_changes => {
577                        // Remove selected text
578                        let selection = self.get_selection_range();
579                        if let Some((start, end)) = selection {
580                            self.remove(start..end);
581                            self.move_cursor_to(start);
582                            event.insert(TextEvent::TEXT_CHANGED);
583                        }
584
585                        if let Ok(ch) = character.parse::<char>() {
586                            // Inserts a character
587                            let cursor_pos = self.cursor_pos();
588                            let inserted_text_len = self.insert_char(ch, cursor_pos);
589                            self.move_cursor_to(cursor_pos + inserted_text_len);
590                            event.insert(TextEvent::TEXT_CHANGED);
591                        } else {
592                            // Inserts a text
593                            let cursor_pos = self.cursor_pos();
594                            let inserted_text_len = self.insert(character, cursor_pos);
595                            self.move_cursor_to(cursor_pos + inserted_text_len);
596                            event.insert(TextEvent::TEXT_CHANGED);
597                        }
598                    }
599                    _ => {}
600                }
601            }
602            _ => {}
603        }
604
605        if event.contains(TextEvent::TEXT_CHANGED) && !event.contains(TextEvent::SELECTION_CHANGED)
606        {
607            self.clear_selection();
608        }
609
610        if self.get_selection() != selection {
611            event.insert(TextEvent::SELECTION_CHANGED);
612        }
613
614        event
615    }
616
617    fn get_selected_text(&self) -> Option<String>;
618
619    fn undo(&mut self) -> Option<TextSelection>;
620
621    fn redo(&mut self) -> Option<TextSelection>;
622
623    fn editor_history(&mut self) -> &mut EditorHistory;
624
625    fn get_selection_range(&self) -> Option<(usize, usize)>;
626
627    fn get_indentation(&self) -> u8;
628
629    fn find_word_boundaries(&self, pos: usize) -> (usize, usize) {
630        let pos_char = self.utf16_cu_to_char(pos);
631        let len_chars = self.len_chars();
632
633        if len_chars == 0 {
634            return (pos, pos);
635        }
636
637        // Get the line containing the cursor
638        let line_idx = self.char_to_line(pos_char);
639        let line_char = self.line_to_char(line_idx);
640        let line = self.line(line_idx).unwrap();
641
642        let line_str: std::borrow::Cow<str> = line.text;
643        let pos_in_line = pos_char - line_char;
644
645        // Find word boundaries within the line
646        let mut char_offset = 0;
647        for word in line_str.split_word_bounds() {
648            let word_char_len = word.chars().count();
649            let word_start = char_offset;
650            let word_end = char_offset + word_char_len;
651
652            if pos_in_line >= word_start && pos_in_line < word_end {
653                let start_char = line_char + word_start;
654                let end_char = line_char + word_end;
655                return (
656                    self.char_to_utf16_cu(start_char),
657                    self.char_to_utf16_cu(end_char),
658                );
659            }
660
661            char_offset = word_end;
662        }
663
664        (pos, pos)
665    }
666}