iced_graphics/text/
editor.rs

1//! Draw and edit text.
2use crate::core::text::editor::{
3    self, Action, Cursor, Direction, Edit, Motion,
4};
5use crate::core::text::highlighter::{self, Highlighter};
6use crate::core::text::{LineHeight, Wrapping};
7use crate::core::{Font, Pixels, Point, Rectangle, Size};
8use crate::text;
9
10use cosmic_text::Edit as _;
11
12use std::borrow::Cow;
13use std::fmt;
14use std::sync::{self, Arc, RwLock};
15
16/// A multi-line text editor.
17#[derive(Debug, PartialEq)]
18pub struct Editor(Option<Arc<Internal>>);
19
20struct Internal {
21    editor: cosmic_text::Editor<'static>,
22    cursor: RwLock<Option<Cursor>>,
23    font: Font,
24    bounds: Size,
25    topmost_line_changed: Option<usize>,
26    version: text::Version,
27}
28
29impl Editor {
30    /// Creates a new empty [`Editor`].
31    pub fn new() -> Self {
32        Self::default()
33    }
34
35    /// Returns the buffer of the [`Editor`].
36    pub fn buffer(&self) -> &cosmic_text::Buffer {
37        buffer_from_editor(&self.internal().editor)
38    }
39
40    /// Creates a [`Weak`] reference to the [`Editor`].
41    ///
42    /// This is useful to avoid cloning the [`Editor`] when
43    /// referential guarantees are unnecessary. For instance,
44    /// when creating a rendering tree.
45    pub fn downgrade(&self) -> Weak {
46        let editor = self.internal();
47
48        Weak {
49            raw: Arc::downgrade(editor),
50            bounds: editor.bounds,
51        }
52    }
53
54    fn internal(&self) -> &Arc<Internal> {
55        self.0
56            .as_ref()
57            .expect("Editor should always be initialized")
58    }
59}
60
61impl editor::Editor for Editor {
62    type Font = Font;
63
64    fn with_text(text: &str) -> Self {
65        let mut buffer = cosmic_text::Buffer::new_empty(cosmic_text::Metrics {
66            font_size: 1.0,
67            line_height: 1.0,
68        });
69
70        let mut font_system =
71            text::font_system().write().expect("Write font system");
72
73        buffer.set_text(
74            font_system.raw(),
75            text,
76            &cosmic_text::Attrs::new(),
77            cosmic_text::Shaping::Advanced,
78            None,
79        );
80
81        Editor(Some(Arc::new(Internal {
82            editor: cosmic_text::Editor::new(buffer),
83            version: font_system.version(),
84            ..Default::default()
85        })))
86    }
87
88    fn is_empty(&self) -> bool {
89        let buffer = self.buffer();
90
91        buffer.lines.is_empty()
92            || (buffer.lines.len() == 1 && buffer.lines[0].text().is_empty())
93    }
94
95    fn line(&self, index: usize) -> Option<editor::Line<'_>> {
96        self.buffer().lines.get(index).map(|line| editor::Line {
97            text: Cow::Borrowed(line.text()),
98            ending: match line.ending() {
99                cosmic_text::LineEnding::Lf => editor::LineEnding::Lf,
100                cosmic_text::LineEnding::CrLf => editor::LineEnding::CrLf,
101                cosmic_text::LineEnding::Cr => editor::LineEnding::Cr,
102                cosmic_text::LineEnding::LfCr => editor::LineEnding::LfCr,
103                cosmic_text::LineEnding::None => editor::LineEnding::None,
104            },
105        })
106    }
107
108    fn line_count(&self) -> usize {
109        self.buffer().lines.len()
110    }
111
112    fn selection(&self) -> Option<String> {
113        self.internal().editor.copy_selection()
114    }
115
116    fn cursor(&self) -> editor::Cursor {
117        let internal = self.internal();
118
119        if let Ok(Some(cursor)) = internal.cursor.read().as_deref() {
120            return cursor.clone();
121        }
122
123        let cursor = internal.editor.cursor();
124        let buffer = buffer_from_editor(&internal.editor);
125
126        let cursor = match internal.editor.selection_bounds() {
127            Some((start, end)) => {
128                let line_height = buffer.metrics().line_height;
129                let selected_lines = end.line - start.line + 1;
130
131                let visual_lines_offset =
132                    visual_lines_offset(start.line, buffer);
133
134                let regions = buffer
135                    .lines
136                    .iter()
137                    .skip(start.line)
138                    .take(selected_lines)
139                    .enumerate()
140                    .flat_map(|(i, line)| {
141                        highlight_line(
142                            line,
143                            if i == 0 { start.index } else { 0 },
144                            if i == selected_lines - 1 {
145                                end.index
146                            } else {
147                                line.text().len()
148                            },
149                        )
150                    })
151                    .enumerate()
152                    .filter_map(|(visual_line, (x, width))| {
153                        if width > 0.0 {
154                            Some(Rectangle {
155                                x,
156                                width,
157                                y: (visual_line as i32 + visual_lines_offset)
158                                    as f32
159                                    * line_height
160                                    - buffer.scroll().vertical,
161                                height: line_height,
162                            })
163                        } else {
164                            None
165                        }
166                    })
167                    .collect();
168
169                Cursor::Selection(regions)
170            }
171            _ => {
172                let line_height = buffer.metrics().line_height;
173
174                let visual_lines_offset =
175                    visual_lines_offset(cursor.line, buffer);
176
177                let line = buffer
178                    .lines
179                    .get(cursor.line)
180                    .expect("Cursor line should be present");
181
182                let layout =
183                    line.layout_opt().expect("Line layout should be cached");
184
185                let mut lines = layout.iter().enumerate();
186
187                let (visual_line, offset) = lines
188                    .find_map(|(i, line)| {
189                        let start = line
190                            .glyphs
191                            .first()
192                            .map(|glyph| glyph.start)
193                            .unwrap_or(0);
194                        let end = line
195                            .glyphs
196                            .last()
197                            .map(|glyph| glyph.end)
198                            .unwrap_or(0);
199
200                        let is_cursor_before_start = start > cursor.index;
201
202                        let is_cursor_before_end = match cursor.affinity {
203                            cosmic_text::Affinity::Before => {
204                                cursor.index <= end
205                            }
206                            cosmic_text::Affinity::After => cursor.index < end,
207                        };
208
209                        if is_cursor_before_start {
210                            // Sometimes, the glyph we are looking for is right
211                            // between lines. This can happen when a line wraps
212                            // on a space.
213                            // In that case, we can assume the cursor is at the
214                            // end of the previous line.
215                            // i is guaranteed to be > 0 because `start` is always
216                            // 0 for the first line, so there is no way for the
217                            // cursor to be before it.
218                            Some((i - 1, layout[i - 1].w))
219                        } else if is_cursor_before_end {
220                            let offset = line
221                                .glyphs
222                                .iter()
223                                .take_while(|glyph| cursor.index > glyph.start)
224                                .map(|glyph| glyph.w)
225                                .sum();
226
227                            Some((i, offset))
228                        } else {
229                            None
230                        }
231                    })
232                    .unwrap_or((
233                        layout.len().saturating_sub(1),
234                        layout.last().map(|line| line.w).unwrap_or(0.0),
235                    ));
236
237                Cursor::Caret(Point::new(
238                    offset,
239                    (visual_lines_offset + visual_line as i32) as f32
240                        * line_height
241                        - buffer.scroll().vertical,
242                ))
243            }
244        };
245
246        *internal.cursor.write().expect("Write to cursor cache") =
247            Some(cursor.clone());
248
249        cursor
250    }
251
252    fn cursor_position(&self) -> (usize, usize) {
253        let cursor = self.internal().editor.cursor();
254
255        (cursor.line, cursor.index)
256    }
257
258    fn perform(&mut self, action: Action) {
259        let mut font_system =
260            text::font_system().write().expect("Write font system");
261
262        let editor =
263            self.0.take().expect("Editor should always be initialized");
264
265        // TODO: Handle multiple strong references somehow
266        let mut internal = Arc::try_unwrap(editor)
267            .expect("Editor cannot have multiple strong references");
268
269        let editor = &mut internal.editor;
270
271        // Clear cursor cache
272        let _ = internal
273            .cursor
274            .write()
275            .expect("Write to cursor cache")
276            .take();
277
278        match action {
279            // Motion events
280            Action::Move(motion) => {
281                if let Some((start, end)) = editor.selection_bounds() {
282                    editor.set_selection(cosmic_text::Selection::None);
283
284                    match motion {
285                        // These motions are performed as-is even when a selection
286                        // is present
287                        Motion::Home
288                        | Motion::End
289                        | Motion::DocumentStart
290                        | Motion::DocumentEnd => {
291                            editor.action(
292                                font_system.raw(),
293                                cosmic_text::Action::Motion(to_motion(motion)),
294                            );
295                        }
296                        // Other motions simply move the cursor to one end of the selection
297                        _ => editor.set_cursor(match motion.direction() {
298                            Direction::Left => start,
299                            Direction::Right => end,
300                        }),
301                    }
302                } else {
303                    editor.action(
304                        font_system.raw(),
305                        cosmic_text::Action::Motion(to_motion(motion)),
306                    );
307                }
308            }
309
310            // Selection events
311            Action::Select(motion) => {
312                let cursor = editor.cursor();
313
314                if editor.selection_bounds().is_none() {
315                    editor
316                        .set_selection(cosmic_text::Selection::Normal(cursor));
317                }
318
319                editor.action(
320                    font_system.raw(),
321                    cosmic_text::Action::Motion(to_motion(motion)),
322                );
323
324                // Deselect if selection matches cursor position
325                if let Some((start, end)) = editor.selection_bounds()
326                    && start.line == end.line
327                    && start.index == end.index
328                {
329                    editor.set_selection(cosmic_text::Selection::None);
330                }
331            }
332            Action::SelectWord => {
333                let cursor = editor.cursor();
334
335                editor.set_selection(cosmic_text::Selection::Word(cursor));
336            }
337            Action::SelectLine => {
338                let cursor = editor.cursor();
339
340                editor.set_selection(cosmic_text::Selection::Line(cursor));
341            }
342            Action::SelectAll => {
343                let buffer = buffer_from_editor(editor);
344
345                if buffer.lines.len() > 1
346                    || buffer
347                        .lines
348                        .first()
349                        .is_some_and(|line| !line.text().is_empty())
350                {
351                    let cursor = editor.cursor();
352
353                    editor.set_selection(cosmic_text::Selection::Normal(
354                        cosmic_text::Cursor {
355                            line: 0,
356                            index: 0,
357                            ..cursor
358                        },
359                    ));
360
361                    editor.action(
362                        font_system.raw(),
363                        cosmic_text::Action::Motion(
364                            cosmic_text::Motion::BufferEnd,
365                        ),
366                    );
367                }
368            }
369
370            // Editing events
371            Action::Edit(edit) => {
372                match edit {
373                    Edit::Insert(c) => {
374                        editor.action(
375                            font_system.raw(),
376                            cosmic_text::Action::Insert(c),
377                        );
378                    }
379                    Edit::Paste(text) => {
380                        editor.insert_string(&text, None);
381                    }
382                    Edit::Indent => {
383                        editor.action(
384                            font_system.raw(),
385                            cosmic_text::Action::Indent,
386                        );
387                    }
388                    Edit::Unindent => {
389                        editor.action(
390                            font_system.raw(),
391                            cosmic_text::Action::Unindent,
392                        );
393                    }
394                    Edit::Enter => {
395                        editor.action(
396                            font_system.raw(),
397                            cosmic_text::Action::Enter,
398                        );
399                    }
400                    Edit::Backspace => {
401                        editor.action(
402                            font_system.raw(),
403                            cosmic_text::Action::Backspace,
404                        );
405                    }
406                    Edit::Delete => {
407                        editor.action(
408                            font_system.raw(),
409                            cosmic_text::Action::Delete,
410                        );
411                    }
412                }
413
414                let cursor = editor.cursor();
415                let selection_start = editor
416                    .selection_bounds()
417                    .map(|(start, _)| start)
418                    .unwrap_or(cursor);
419
420                internal.topmost_line_changed = Some(selection_start.line);
421            }
422
423            // Mouse events
424            Action::Click(position) => {
425                editor.action(
426                    font_system.raw(),
427                    cosmic_text::Action::Click {
428                        x: position.x as i32,
429                        y: position.y as i32,
430                    },
431                );
432            }
433            Action::Drag(position) => {
434                editor.action(
435                    font_system.raw(),
436                    cosmic_text::Action::Drag {
437                        x: position.x as i32,
438                        y: position.y as i32,
439                    },
440                );
441
442                // Deselect if selection matches cursor position
443                if let Some((start, end)) = editor.selection_bounds()
444                    && start.line == end.line
445                    && start.index == end.index
446                {
447                    editor.set_selection(cosmic_text::Selection::None);
448                }
449            }
450            Action::Scroll { lines } => {
451                editor.action(
452                    font_system.raw(),
453                    cosmic_text::Action::Scroll {
454                        pixels: lines as f32
455                            * buffer_from_editor(editor).metrics().line_height,
456                    },
457                );
458            }
459        }
460
461        self.0 = Some(Arc::new(internal));
462    }
463
464    fn bounds(&self) -> Size {
465        self.internal().bounds
466    }
467
468    fn min_bounds(&self) -> Size {
469        let internal = self.internal();
470
471        let (bounds, _has_rtl) =
472            text::measure(buffer_from_editor(&internal.editor));
473
474        bounds
475    }
476
477    fn update(
478        &mut self,
479        new_bounds: Size,
480        new_font: Font,
481        new_size: Pixels,
482        new_line_height: LineHeight,
483        new_wrapping: Wrapping,
484        new_highlighter: &mut impl Highlighter,
485    ) {
486        let editor =
487            self.0.take().expect("Editor should always be initialized");
488
489        let mut internal = Arc::try_unwrap(editor)
490            .expect("Editor cannot have multiple strong references");
491
492        let mut font_system =
493            text::font_system().write().expect("Write font system");
494
495        let buffer = buffer_mut_from_editor(&mut internal.editor);
496
497        if font_system.version() != internal.version {
498            log::trace!("Updating `FontSystem` of `Editor`...");
499
500            for line in buffer.lines.iter_mut() {
501                line.reset();
502            }
503
504            internal.version = font_system.version();
505            internal.topmost_line_changed = Some(0);
506        }
507
508        if new_font != internal.font {
509            log::trace!("Updating font of `Editor`...");
510
511            for line in buffer.lines.iter_mut() {
512                let _ = line.set_attrs_list(cosmic_text::AttrsList::new(
513                    &text::to_attributes(new_font),
514                ));
515            }
516
517            internal.font = new_font;
518            internal.topmost_line_changed = Some(0);
519        }
520
521        let metrics = buffer.metrics();
522        let new_line_height = new_line_height.to_absolute(new_size);
523
524        if new_size.0 != metrics.font_size
525            || new_line_height.0 != metrics.line_height
526        {
527            log::trace!("Updating `Metrics` of `Editor`...");
528
529            buffer.set_metrics(
530                font_system.raw(),
531                cosmic_text::Metrics::new(new_size.0, new_line_height.0),
532            );
533        }
534
535        let new_wrap = text::to_wrap(new_wrapping);
536
537        if new_wrap != buffer.wrap() {
538            log::trace!("Updating `Wrap` strategy of `Editor`...");
539
540            buffer.set_wrap(font_system.raw(), new_wrap);
541        }
542
543        if new_bounds != internal.bounds {
544            log::trace!("Updating size of `Editor`...");
545
546            buffer.set_size(
547                font_system.raw(),
548                Some(new_bounds.width),
549                Some(new_bounds.height),
550            );
551
552            internal.bounds = new_bounds;
553        }
554
555        if let Some(topmost_line_changed) = internal.topmost_line_changed.take()
556        {
557            log::trace!(
558                "Notifying highlighter of line change: {topmost_line_changed}"
559            );
560
561            new_highlighter.change_line(topmost_line_changed);
562        }
563
564        internal.editor.shape_as_needed(font_system.raw(), false);
565
566        // Clear cursor cache
567        let _ = internal
568            .cursor
569            .write()
570            .expect("Write to cursor cache")
571            .take();
572
573        self.0 = Some(Arc::new(internal));
574    }
575
576    fn highlight<H: Highlighter>(
577        &mut self,
578        font: Self::Font,
579        highlighter: &mut H,
580        format_highlight: impl Fn(&H::Highlight) -> highlighter::Format<Self::Font>,
581    ) {
582        let internal = self.internal();
583        let buffer = buffer_from_editor(&internal.editor);
584
585        let scroll = buffer.scroll();
586        let mut window = (internal.bounds.height / buffer.metrics().line_height)
587            .ceil() as i32;
588
589        let last_visible_line = buffer.lines[scroll.line..]
590            .iter()
591            .enumerate()
592            .find_map(|(i, line)| {
593                let visible_lines = line
594                    .layout_opt()
595                    .as_ref()
596                    .expect("Line layout should be cached")
597                    .len() as i32;
598
599                if window > visible_lines {
600                    window -= visible_lines;
601                    None
602                } else {
603                    Some(scroll.line + i)
604                }
605            })
606            .unwrap_or(buffer.lines.len().saturating_sub(1));
607
608        let current_line = highlighter.current_line();
609
610        if current_line > last_visible_line {
611            return;
612        }
613
614        let editor =
615            self.0.take().expect("Editor should always be initialized");
616
617        let mut internal = Arc::try_unwrap(editor)
618            .expect("Editor cannot have multiple strong references");
619
620        let mut font_system =
621            text::font_system().write().expect("Write font system");
622
623        let attributes = text::to_attributes(font);
624
625        for line in &mut buffer_mut_from_editor(&mut internal.editor).lines
626            [current_line..=last_visible_line]
627        {
628            let mut list = cosmic_text::AttrsList::new(&attributes);
629
630            for (range, highlight) in highlighter.highlight_line(line.text()) {
631                let format = format_highlight(&highlight);
632
633                if format.color.is_some() || format.font.is_some() {
634                    list.add_span(
635                        range,
636                        &cosmic_text::Attrs {
637                            color_opt: format.color.map(text::to_color),
638                            ..if let Some(font) = format.font {
639                                text::to_attributes(font)
640                            } else {
641                                attributes.clone()
642                            }
643                        },
644                    );
645                }
646            }
647
648            let _ = line.set_attrs_list(list);
649        }
650
651        internal.editor.shape_as_needed(font_system.raw(), false);
652
653        self.0 = Some(Arc::new(internal));
654    }
655}
656
657impl Default for Editor {
658    fn default() -> Self {
659        Self(Some(Arc::new(Internal::default())))
660    }
661}
662
663impl PartialEq for Internal {
664    fn eq(&self, other: &Self) -> bool {
665        self.font == other.font
666            && self.bounds == other.bounds
667            && buffer_from_editor(&self.editor).metrics()
668                == buffer_from_editor(&other.editor).metrics()
669    }
670}
671
672impl Default for Internal {
673    fn default() -> Self {
674        Self {
675            editor: cosmic_text::Editor::new(cosmic_text::Buffer::new_empty(
676                cosmic_text::Metrics {
677                    font_size: 1.0,
678                    line_height: 1.0,
679                },
680            )),
681            cursor: RwLock::new(None),
682            font: Font::default(),
683            bounds: Size::ZERO,
684            topmost_line_changed: None,
685            version: text::Version::default(),
686        }
687    }
688}
689
690impl fmt::Debug for Internal {
691    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
692        f.debug_struct("Internal")
693            .field("font", &self.font)
694            .field("bounds", &self.bounds)
695            .finish()
696    }
697}
698
699/// A weak reference to an [`Editor`].
700#[derive(Debug, Clone)]
701pub struct Weak {
702    raw: sync::Weak<Internal>,
703    /// The bounds of the [`Editor`].
704    pub bounds: Size,
705}
706
707impl Weak {
708    /// Tries to update the reference into an [`Editor`].
709    pub fn upgrade(&self) -> Option<Editor> {
710        self.raw.upgrade().map(Some).map(Editor)
711    }
712}
713
714impl PartialEq for Weak {
715    fn eq(&self, other: &Self) -> bool {
716        match (self.raw.upgrade(), other.raw.upgrade()) {
717            (Some(p1), Some(p2)) => p1 == p2,
718            _ => false,
719        }
720    }
721}
722
723fn highlight_line(
724    line: &cosmic_text::BufferLine,
725    from: usize,
726    to: usize,
727) -> impl Iterator<Item = (f32, f32)> + '_ {
728    let layout = line.layout_opt().map(Vec::as_slice).unwrap_or_default();
729
730    layout.iter().map(move |visual_line| {
731        let start = visual_line
732            .glyphs
733            .first()
734            .map(|glyph| glyph.start)
735            .unwrap_or(0);
736        let end = visual_line
737            .glyphs
738            .last()
739            .map(|glyph| glyph.end)
740            .unwrap_or(0);
741
742        let range = start.max(from)..end.min(to);
743
744        if range.is_empty() {
745            (0.0, 0.0)
746        } else if range.start == start && range.end == end {
747            (0.0, visual_line.w)
748        } else {
749            let first_glyph = visual_line
750                .glyphs
751                .iter()
752                .position(|glyph| range.start <= glyph.start)
753                .unwrap_or(0);
754
755            let mut glyphs = visual_line.glyphs.iter();
756
757            let x =
758                glyphs.by_ref().take(first_glyph).map(|glyph| glyph.w).sum();
759
760            let width: f32 = glyphs
761                .take_while(|glyph| range.end > glyph.start)
762                .map(|glyph| glyph.w)
763                .sum();
764
765            (x, width)
766        }
767    })
768}
769
770fn visual_lines_offset(line: usize, buffer: &cosmic_text::Buffer) -> i32 {
771    let scroll = buffer.scroll();
772
773    let start = scroll.line.min(line);
774    let end = scroll.line.max(line);
775
776    let visual_lines_offset: usize = buffer.lines[start..]
777        .iter()
778        .take(end - start)
779        .map(|line| line.layout_opt().map(Vec::len).unwrap_or_default())
780        .sum();
781
782    visual_lines_offset as i32 * if scroll.line < line { 1 } else { -1 }
783}
784
785fn to_motion(motion: Motion) -> cosmic_text::Motion {
786    match motion {
787        Motion::Left => cosmic_text::Motion::Left,
788        Motion::Right => cosmic_text::Motion::Right,
789        Motion::Up => cosmic_text::Motion::Up,
790        Motion::Down => cosmic_text::Motion::Down,
791        Motion::WordLeft => cosmic_text::Motion::LeftWord,
792        Motion::WordRight => cosmic_text::Motion::RightWord,
793        Motion::Home => cosmic_text::Motion::Home,
794        Motion::End => cosmic_text::Motion::End,
795        Motion::PageUp => cosmic_text::Motion::PageUp,
796        Motion::PageDown => cosmic_text::Motion::PageDown,
797        Motion::DocumentStart => cosmic_text::Motion::BufferStart,
798        Motion::DocumentEnd => cosmic_text::Motion::BufferEnd,
799    }
800}
801
802fn buffer_from_editor<'a, 'b>(
803    editor: &'a impl cosmic_text::Edit<'b>,
804) -> &'a cosmic_text::Buffer
805where
806    'b: 'a,
807{
808    match editor.buffer_ref() {
809        cosmic_text::BufferRef::Owned(buffer) => buffer,
810        cosmic_text::BufferRef::Borrowed(buffer) => buffer,
811        cosmic_text::BufferRef::Arc(buffer) => buffer,
812    }
813}
814
815fn buffer_mut_from_editor<'a, 'b>(
816    editor: &'a mut impl cosmic_text::Edit<'b>,
817) -> &'a mut cosmic_text::Buffer
818where
819    'b: 'a,
820{
821    match editor.buffer_ref_mut() {
822        cosmic_text::BufferRef::Owned(buffer) => buffer,
823        cosmic_text::BufferRef::Borrowed(buffer) => buffer,
824        cosmic_text::BufferRef::Arc(_buffer) => unreachable!(),
825    }
826}