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
289    // Remove the selection
290    fn clear_selection(&mut self);
291
292    // Select some text
293    fn set_selection(&mut self, selected: (usize, usize));
294
295    // Measure a new text selection
296
297    fn measure_selection(&self, to: usize, line_index: EditorLine) -> TextSelection {
298        let mut selection = self.selection().clone();
299
300        match line_index {
301            EditorLine::Paragraph(line_index) => {
302                let row_char = self.line_to_char(line_index);
303                let pos = self.char_to_utf16_cu(row_char) + to;
304                selection.move_to(pos);
305            }
306            EditorLine::SingleParagraph => {
307                selection.move_to(to);
308            }
309        }
310
311        selection
312    }
313
314    // Process a Keyboard event
315    fn process_key(
316        &mut self,
317        key: &Key,
318        modifiers: &Modifiers,
319        allow_tabs: bool,
320        allow_changes: bool,
321        allow_clipboard: bool,
322    ) -> TextEvent {
323        let mut event = TextEvent::empty();
324
325        let selection = self.get_selection();
326        let skip_arrows_movement = !modifiers.contains(Modifiers::SHIFT) && selection.is_some();
327
328        match key {
329            Key::Named(NamedKey::Shift) => {}
330            Key::Named(NamedKey::Control) => {}
331            Key::Named(NamedKey::Alt) => {}
332            Key::Named(NamedKey::Escape) => {
333                self.clear_selection();
334            }
335            Key::Named(NamedKey::ArrowDown) => {
336                if modifiers.contains(Modifiers::SHIFT) {
337                    self.selection_mut().set_as_range();
338                } else {
339                    self.selection_mut().set_as_cursor();
340                }
341
342                if !skip_arrows_movement && self.cursor_down() {
343                    event.insert(TextEvent::CURSOR_CHANGED);
344                }
345            }
346            Key::Named(NamedKey::ArrowLeft) => {
347                if modifiers.contains(Modifiers::SHIFT) {
348                    self.selection_mut().set_as_range();
349                } else {
350                    self.selection_mut().set_as_cursor();
351                }
352
353                if !skip_arrows_movement && self.cursor_left() {
354                    event.insert(TextEvent::CURSOR_CHANGED);
355                }
356            }
357            Key::Named(NamedKey::ArrowRight) => {
358                if modifiers.contains(Modifiers::SHIFT) {
359                    self.selection_mut().set_as_range();
360                } else {
361                    self.selection_mut().set_as_cursor();
362                }
363
364                if !skip_arrows_movement && self.cursor_right() {
365                    event.insert(TextEvent::CURSOR_CHANGED);
366                }
367            }
368            Key::Named(NamedKey::ArrowUp) => {
369                if modifiers.contains(Modifiers::SHIFT) {
370                    self.selection_mut().set_as_range();
371                } else {
372                    self.selection_mut().set_as_cursor();
373                }
374
375                if !skip_arrows_movement && self.cursor_up() {
376                    event.insert(TextEvent::CURSOR_CHANGED);
377                }
378            }
379            Key::Named(NamedKey::Backspace) if allow_changes => {
380                let cursor_pos = self.cursor_pos();
381                let selection = self.get_selection_range();
382
383                if let Some((start, end)) = selection {
384                    self.remove(start..end);
385                    self.move_cursor_to(start);
386                    event.insert(TextEvent::TEXT_CHANGED);
387                } else if cursor_pos > 0 {
388                    // Remove the character to the left if there is any
389                    let removed_text_len = self.remove(cursor_pos - 1..cursor_pos);
390                    self.move_cursor_to(cursor_pos - removed_text_len);
391                    event.insert(TextEvent::TEXT_CHANGED);
392                }
393            }
394            Key::Named(NamedKey::Delete) if allow_changes => {
395                let cursor_pos = self.cursor_pos();
396                let selection = self.get_selection_range();
397
398                if let Some((start, end)) = selection {
399                    self.remove(start..end);
400                    self.move_cursor_to(start);
401                    event.insert(TextEvent::TEXT_CHANGED);
402                } else if cursor_pos < self.len_utf16_cu() {
403                    // Remove the character to the right if there is any
404                    self.remove(cursor_pos..cursor_pos + 1);
405                    event.insert(TextEvent::TEXT_CHANGED);
406                }
407            }
408            Key::Named(NamedKey::Enter) if allow_changes => {
409                // Breaks the line
410                let cursor_pos = self.cursor_pos();
411                self.insert_char('\n', cursor_pos);
412                self.cursor_right();
413
414                event.insert(TextEvent::TEXT_CHANGED);
415            }
416            Key::Named(NamedKey::Tab) if allow_tabs && allow_changes => {
417                // Inserts a tab
418                let text = " ".repeat(self.get_indentation().into());
419                let cursor_pos = self.cursor_pos();
420                self.insert(&text, cursor_pos);
421                self.move_cursor_to(cursor_pos + text.chars().count());
422
423                event.insert(TextEvent::TEXT_CHANGED);
424            }
425            Key::Character(character) => {
426                let meta_or_ctrl = if cfg!(target_os = "macos") {
427                    modifiers.meta()
428                } else {
429                    modifiers.ctrl()
430                };
431
432                match character.as_str() {
433                    " " if allow_changes => {
434                        let selection = self.get_selection_range();
435                        if let Some((start, end)) = selection {
436                            self.remove(start..end);
437                            self.move_cursor_to(start);
438                            event.insert(TextEvent::TEXT_CHANGED);
439                        }
440
441                        // Simply adds an space
442                        let cursor_pos = self.cursor_pos();
443                        self.insert_char(' ', cursor_pos);
444                        self.cursor_right();
445
446                        event.insert(TextEvent::TEXT_CHANGED);
447                    }
448
449                    // Select all text
450                    "a" if meta_or_ctrl => {
451                        let len = self.len_utf16_cu();
452                        self.set_selection((0, len));
453                    }
454
455                    // Copy selected text
456                    "c" if meta_or_ctrl && allow_clipboard => {
457                        let selected = self.get_selected_text();
458                        if let Some(selected) = selected {
459                            Clipboard::set(selected).ok();
460                        }
461                    }
462
463                    // Cut selected text
464                    "x" if meta_or_ctrl && allow_changes && allow_clipboard => {
465                        let selection = self.get_selection_range();
466                        if let Some((start, end)) = selection {
467                            let text = self.get_selected_text().unwrap();
468                            self.remove(start..end);
469                            Clipboard::set(text).ok();
470                            self.move_cursor_to(start);
471                            event.insert(TextEvent::TEXT_CHANGED);
472                        }
473                    }
474
475                    // Paste copied text
476                    "v" if meta_or_ctrl && allow_changes && allow_clipboard => {
477                        if let Ok(copied_text) = Clipboard::get() {
478                            let selection = self.get_selection_range();
479                            if let Some((start, end)) = selection {
480                                self.remove(start..end);
481                                self.move_cursor_to(start);
482                            }
483                            let cursor_pos = self.cursor_pos();
484                            self.insert(&copied_text, cursor_pos);
485                            let last_idx = copied_text.encode_utf16().count() + cursor_pos;
486                            self.move_cursor_to(last_idx);
487                            event.insert(TextEvent::TEXT_CHANGED);
488                        }
489                    }
490
491                    // Undo last change
492                    "z" if meta_or_ctrl && allow_changes => {
493                        let undo_result = self.undo();
494
495                        if let Some(selection) = undo_result {
496                            *self.selection_mut() = selection;
497                            event.insert(TextEvent::TEXT_CHANGED);
498                            event.insert(TextEvent::SELECTION_CHANGED);
499                        }
500                    }
501
502                    // Redo last change
503                    "y" if meta_or_ctrl && allow_changes => {
504                        let redo_result = self.redo();
505
506                        if let Some(selection) = redo_result {
507                            *self.selection_mut() = selection;
508                            event.insert(TextEvent::TEXT_CHANGED);
509                            event.insert(TextEvent::SELECTION_CHANGED);
510                        }
511                    }
512
513                    _ if allow_changes => {
514                        // Remove selected text
515                        let selection = self.get_selection_range();
516                        if let Some((start, end)) = selection {
517                            self.remove(start..end);
518                            self.move_cursor_to(start);
519                            event.insert(TextEvent::TEXT_CHANGED);
520                        }
521
522                        if let Ok(ch) = character.parse::<char>() {
523                            // Inserts a character
524                            let cursor_pos = self.cursor_pos();
525                            let inserted_text_len = self.insert_char(ch, cursor_pos);
526                            self.move_cursor_to(cursor_pos + inserted_text_len);
527                            event.insert(TextEvent::TEXT_CHANGED);
528                        } else {
529                            // Inserts a text
530                            let cursor_pos = self.cursor_pos();
531                            let inserted_text_len = self.insert(character, cursor_pos);
532                            self.move_cursor_to(cursor_pos + inserted_text_len);
533                            event.insert(TextEvent::TEXT_CHANGED);
534                        }
535                    }
536                    _ => {}
537                }
538            }
539            _ => {}
540        }
541
542        if event.contains(TextEvent::TEXT_CHANGED) && !event.contains(TextEvent::SELECTION_CHANGED)
543        {
544            self.clear_selection();
545        }
546
547        if self.get_selection() != selection {
548            event.insert(TextEvent::SELECTION_CHANGED);
549        }
550
551        event
552    }
553
554    fn get_selected_text(&self) -> Option<String>;
555
556    fn undo(&mut self) -> Option<TextSelection>;
557
558    fn redo(&mut self) -> Option<TextSelection>;
559
560    fn editor_history(&mut self) -> &mut EditorHistory;
561
562    fn get_selection_range(&self) -> Option<(usize, usize)>;
563
564    fn get_indentation(&self) -> u8;
565
566    fn find_word_boundaries(&self, pos: usize) -> (usize, usize) {
567        let pos_char = self.utf16_cu_to_char(pos);
568        let len_chars = self.len_chars();
569
570        if len_chars == 0 {
571            return (pos, pos);
572        }
573
574        // Get the line containing the cursor
575        let line_idx = self.char_to_line(pos_char);
576        let line_char = self.line_to_char(line_idx);
577        let line = self.line(line_idx).unwrap();
578
579        let line_str: std::borrow::Cow<str> = line.text;
580        let pos_in_line = pos_char - line_char;
581
582        // Find word boundaries within the line
583        let mut char_offset = 0;
584        for word in line_str.split_word_bounds() {
585            let word_char_len = word.chars().count();
586            let word_start = char_offset;
587            let word_end = char_offset + word_char_len;
588
589            if pos_in_line >= word_start && pos_in_line < word_end {
590                let start_char = line_char + word_start;
591                let end_char = line_char + word_end;
592                return (
593                    self.char_to_utf16_cu(start_char),
594                    self.char_to_utf16_cu(end_char),
595                );
596            }
597
598            char_offset = word_end;
599        }
600
601        (pos, pos)
602    }
603}