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