iced_graphics/text/
editor.rs

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