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