Skip to main content

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