Skip to main content

cosmic_text/
buffer.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3#[cfg(not(feature = "std"))]
4use alloc::{string::String, vec::Vec};
5
6use core::{cmp, fmt};
7
8#[cfg(not(feature = "std"))]
9use core_maths::CoreFloat;
10use unicode_segmentation::UnicodeSegmentation;
11
12use crate::{
13    render_decoration, Affinity, Align, Attrs, AttrsList, BidiParagraphs, BorrowedWithFontSystem,
14    BufferLine, Color, Cursor, DecorationSpan, Direction, Ellipsize, FontSystem, Hinting,
15    LayoutCursor, LayoutGlyph, LayoutLine, LineEnding, LineIter, Motion, Renderer, Scroll,
16    ShapeLine, Shaping, Wrap,
17};
18
19bitflags::bitflags! {
20    /// Tracks which buffer-wide properties have changed since the last layout.
21    #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
22    struct DirtyFlags: u8 {
23        /// Layout caches are stale (wrap, size, metrics, hinting, ellipsize, monospace_width changed)
24        const RELAYOUT  = 0b0001;
25        /// tab_width changed — lines containing tabs need reshape
26        const TAB_SHAPE = 0b0010;
27        /// Text was replaced via set_text/set_rich_text — lines are fresh, just need shape_until_scroll
28        const TEXT_SET  = 0b0100;
29        /// Scroll position changed — visible region may have shifted to unshaped lines
30        const SCROLL    = 0b1000;
31        /// Base direction changed, reshape every line (some characters like '(' are shaped differently based on direction)
32        const DIRECTION = 0b1_0000;
33    }
34}
35
36/// A line of visible text for rendering
37#[derive(Debug)]
38pub struct LayoutRun<'a> {
39    /// The index of the original text line
40    pub line_i: usize,
41    /// The original text line
42    pub text: &'a str,
43    /// True if the original paragraph direction is RTL
44    pub rtl: bool,
45    /// The array of layout glyphs to draw
46    pub glyphs: &'a [LayoutGlyph],
47    /// Text decoration spans covering ranges of glyphs
48    pub decorations: &'a [DecorationSpan],
49    /// Y offset to baseline of line
50    pub line_y: f32,
51    /// Y offset to top of line
52    pub line_top: f32,
53    /// Y offset to next line
54    pub line_height: f32,
55    /// Width of line
56    pub line_w: f32,
57}
58
59impl LayoutRun<'_> {
60    /// Return an iterator of `(x_left, x_width)` pixel spans for the highlighted areas
61    /// between `cursor_start` and `cursor_end` within this run.
62    ///
63    /// For pure LTR or pure RTL runs this yields at most one span. For mixed BiDi runs
64    /// (where selected and unselected glyphs interleave visually) it yields multiple
65    /// disjoint spans.
66    ///
67    /// Returns an empty iterator if the cursor range does not intersect this run.
68    pub fn highlight(
69        &self,
70        cursor_start: Cursor,
71        cursor_end: Cursor,
72    ) -> impl Iterator<Item = (f32, f32)> {
73        let line_i = self.line_i;
74        let mut results = Vec::new();
75        let mut range_opt: Option<(f32, f32)> = None;
76
77        for glyph in self.glyphs {
78            let cluster = &self.text[glyph.start..glyph.end];
79            let total = cluster.grapheme_indices(true).count().max(1);
80            let c_w = glyph.w / total as f32;
81            let mut c_x = glyph.x;
82
83            for (i, c) in cluster.grapheme_indices(true) {
84                let c_start = glyph.start + i;
85                let c_end = glyph.start + i + c.len();
86
87                let is_selected = (cursor_start.line != line_i || c_end > cursor_start.index)
88                    && (cursor_end.line != line_i || c_start < cursor_end.index);
89
90                if is_selected {
91                    range_opt = Some(match range_opt {
92                        Some((min, max)) => (min.min(c_x), max.max(c_x + c_w)),
93                        None => (c_x, c_x + c_w),
94                    });
95                } else if let Some((min_x, max_x)) = range_opt.take() {
96                    let width = max_x - min_x;
97                    if width > 0.0 {
98                        results.push((min_x, width));
99                    }
100                }
101
102                c_x += c_w;
103            }
104        }
105
106        // Flush remaining highlighted region
107        if let Some((min_x, max_x)) = range_opt {
108            let width = max_x - min_x;
109            if width > 0.0 {
110                results.push((min_x, width));
111            }
112        }
113
114        results.into_iter()
115    }
116
117    /// Returns the visual x position (in pixels) of `cursor` within this run,
118    /// or `None` if the cursor does not belong to this run.
119    ///
120    /// For RTL glyphs the cursor is placed at the right edge minus the offset;
121    /// for LTR glyphs it is placed at the left edge plus the offset.
122    pub fn cursor_position(&self, cursor: &Cursor) -> Option<f32> {
123        let (glyph_idx, glyph_offset) = self.cursor_glyph(cursor)?;
124        let x = self.glyphs.get(glyph_idx).map_or_else(
125            || {
126                // Past-the-end: position after the last glyph
127                self.glyphs.last().map_or(0.0, |glyph| {
128                    if glyph.level.is_rtl() {
129                        glyph.x
130                    } else {
131                        glyph.x + glyph.w
132                    }
133                })
134            },
135            |glyph| {
136                if glyph.level.is_rtl() {
137                    glyph.x + glyph.w - glyph_offset
138                } else {
139                    glyph.x + glyph_offset
140                }
141            },
142        );
143        Some(x)
144    }
145
146    /// Find which glyph in this run contains `cursor`, returning
147    /// `(glyph_index, pixel_offset_within_glyph)`, or `None` if the cursor
148    /// is not on this run.
149    pub fn cursor_glyph(&self, cursor: &Cursor) -> Option<(usize, f32)> {
150        if cursor.line != self.line_i {
151            return None;
152        }
153        for (glyph_i, glyph) in self.glyphs.iter().enumerate() {
154            if cursor.index == glyph.start {
155                return Some((glyph_i, 0.0));
156            } else if cursor.index > glyph.start && cursor.index < glyph.end {
157                // Guess x offset based on graphemes within the cluster
158                let cluster = &self.text[glyph.start..glyph.end];
159                let mut before = 0;
160                let mut total = 0;
161                for (i, _) in cluster.grapheme_indices(true) {
162                    if glyph.start + i < cursor.index {
163                        before += 1;
164                    }
165                    total += 1;
166                }
167                let offset = glyph.w * (before as f32) / (total as f32);
168                return Some((glyph_i, offset));
169            }
170        }
171        // in mixed BiDi the last logical glyph may not be the last visual glyph.
172        for (glyph_i, glyph) in self.glyphs.iter().enumerate() {
173            if cursor.index == glyph.end {
174                return Some((glyph_i, glyph.w));
175            }
176        }
177        if self.glyphs.is_empty() {
178            return Some((0, 0.0));
179        }
180        None
181    }
182
183    /// Get the left-edge cursor position of a glyph, accounting for paragraph direction.
184    pub const fn cursor_from_glyph_left(&self, glyph: &LayoutGlyph) -> Cursor {
185        if self.rtl {
186            Cursor::new_with_affinity(self.line_i, glyph.end, Affinity::Before)
187        } else {
188            Cursor::new_with_affinity(self.line_i, glyph.start, Affinity::After)
189        }
190    }
191
192    /// Get the right-edge cursor position of a glyph, accounting for paragraph direction.
193    pub const fn cursor_from_glyph_right(&self, glyph: &LayoutGlyph) -> Cursor {
194        if self.rtl {
195            Cursor::new_with_affinity(self.line_i, glyph.start, Affinity::After)
196        } else {
197            Cursor::new_with_affinity(self.line_i, glyph.end, Affinity::Before)
198        }
199    }
200}
201
202/// An iterator of visible text lines, see [`LayoutRun`]
203#[derive(Debug)]
204pub struct LayoutRunIter<'b> {
205    lines: &'b [BufferLine],
206    height_opt: Option<f32>,
207    line_height: f32,
208    scroll: f32,
209    line_i: usize,
210    layout_i: usize,
211    total_height: f32,
212    line_top: f32,
213}
214
215impl<'b> LayoutRunIter<'b> {
216    pub const fn new(buffer: &'b Buffer) -> Self {
217        Self::from_lines(
218            buffer.lines.as_slice(),
219            buffer.height_opt,
220            buffer.metrics.line_height,
221            buffer.scroll.vertical,
222            buffer.scroll.line,
223        )
224    }
225
226    pub const fn from_lines(
227        lines: &'b [BufferLine],
228        height_opt: Option<f32>,
229        line_height: f32,
230        scroll: f32,
231        start: usize,
232    ) -> Self {
233        Self {
234            lines,
235            height_opt,
236            line_height,
237            scroll,
238            line_i: start,
239            layout_i: 0,
240            total_height: 0.0,
241            line_top: 0.0,
242        }
243    }
244}
245
246impl<'b> Iterator for LayoutRunIter<'b> {
247    type Item = LayoutRun<'b>;
248
249    fn next(&mut self) -> Option<Self::Item> {
250        while let Some(line) = self.lines.get(self.line_i) {
251            let shape = line.shape_opt()?;
252            let layout = line.layout_opt()?;
253            while let Some(layout_line) = layout.get(self.layout_i) {
254                self.layout_i += 1;
255
256                let line_height = layout_line.line_height_opt.unwrap_or(self.line_height);
257                self.total_height += line_height;
258
259                let line_top = self.line_top - self.scroll;
260                let glyph_height = layout_line.max_ascent + layout_line.max_descent;
261                let centering_offset = (line_height - glyph_height) / 2.0;
262                let line_y = line_top + centering_offset + layout_line.max_ascent;
263                if let Some(height) = self.height_opt {
264                    if line_y - layout_line.max_ascent > height {
265                        return None;
266                    }
267                }
268                self.line_top += line_height;
269                if line_y + layout_line.max_descent < 0.0 {
270                    continue;
271                }
272
273                return Some(LayoutRun {
274                    line_i: self.line_i,
275                    text: line.text(),
276                    rtl: shape.rtl,
277                    glyphs: &layout_line.glyphs,
278                    decorations: &layout_line.decorations,
279                    line_y,
280                    line_top,
281                    line_height,
282                    line_w: layout_line.w,
283                });
284            }
285            self.line_i += 1;
286            self.layout_i = 0;
287        }
288
289        None
290    }
291}
292
293/// Metrics of text
294#[derive(Clone, Copy, Debug, Default, PartialEq)]
295pub struct Metrics {
296    /// Font size in pixels
297    pub font_size: f32,
298    /// Line height in pixels
299    pub line_height: f32,
300}
301
302impl Metrics {
303    /// Create metrics with given font size and line height
304    pub const fn new(font_size: f32, line_height: f32) -> Self {
305        Self {
306            font_size,
307            line_height,
308        }
309    }
310
311    /// Create metrics with given font size and calculate line height using relative scale
312    pub fn relative(font_size: f32, line_height_scale: f32) -> Self {
313        Self {
314            font_size,
315            line_height: font_size * line_height_scale,
316        }
317    }
318
319    /// Scale font size and line height
320    pub fn scale(self, scale: f32) -> Self {
321        Self {
322            font_size: self.font_size * scale,
323            line_height: self.line_height * scale,
324        }
325    }
326}
327
328impl fmt::Display for Metrics {
329    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
330        write!(f, "{}px / {}px", self.font_size, self.line_height)
331    }
332}
333
334/// A buffer of text that is shaped and laid out
335#[derive(Debug)]
336pub struct Buffer {
337    /// [`BufferLine`]s (or paragraphs) of text in the buffer
338    pub lines: Vec<BufferLine>,
339    metrics: Metrics,
340    width_opt: Option<f32>,
341    height_opt: Option<f32>,
342    scroll: Scroll,
343    /// True if a redraw is requires. Set to false after processing
344    redraw: bool,
345    wrap: Wrap,
346    ellipsize: Ellipsize,
347    monospace_width: Option<f32>,
348    tab_width: u16,
349    hinting: Hinting,
350    direction: Direction,
351    /// Dirty flags tracking which properties changed since last layout
352    dirty: DirtyFlags,
353}
354
355impl Clone for Buffer {
356    fn clone(&self) -> Self {
357        Self {
358            lines: self.lines.clone(),
359            metrics: self.metrics,
360            width_opt: self.width_opt,
361            height_opt: self.height_opt,
362            scroll: self.scroll,
363            redraw: self.redraw,
364            wrap: self.wrap,
365            ellipsize: self.ellipsize,
366            monospace_width: self.monospace_width,
367            tab_width: self.tab_width,
368            hinting: self.hinting,
369            direction: self.direction,
370            dirty: self.dirty,
371        }
372    }
373}
374
375impl Buffer {
376    /// Create an empty [`Buffer`] with the provided [`Metrics`].
377    /// This is useful for initializing a [`Buffer`] without a [`FontSystem`].
378    ///
379    /// You must populate the [`Buffer`] with at least one [`BufferLine`] before shaping and layout,
380    /// for example by calling [`Buffer::set_text`].
381    ///
382    /// If you have a [`FontSystem`] in scope, you should use [`Buffer::new`] instead.
383    ///
384    /// # Panics
385    ///
386    /// Will panic if `metrics.line_height` is zero.
387    pub fn new_empty(metrics: Metrics) -> Self {
388        assert_ne!(metrics.line_height, 0.0, "line height cannot be 0");
389        Self {
390            lines: Vec::new(),
391            metrics,
392            width_opt: None,
393            height_opt: None,
394            scroll: Scroll::default(),
395            redraw: false,
396            wrap: Wrap::WordOrGlyph,
397            ellipsize: Ellipsize::None,
398            monospace_width: None,
399            tab_width: 8,
400            hinting: Hinting::default(),
401            direction: Direction::default(),
402            dirty: DirtyFlags::empty(),
403        }
404    }
405
406    /// Create a new [`Buffer`] with the provided [`FontSystem`] and [`Metrics`]
407    ///
408    /// # Panics
409    ///
410    /// Will panic if `metrics.line_height` is zero.
411    pub fn new(font_system: &mut FontSystem, metrics: Metrics) -> Self {
412        let mut buffer = Self::new_empty(metrics);
413        buffer.set_text("", &Attrs::new(), Shaping::Advanced, None);
414        buffer.shape_until_scroll(font_system, false);
415        buffer
416    }
417
418    /// Mutably borrows the buffer together with an [`FontSystem`] for more convenient methods
419    pub fn borrow_with<'a>(
420        &'a mut self,
421        font_system: &'a mut FontSystem,
422    ) -> BorrowedWithFontSystem<'a, Self> {
423        BorrowedWithFontSystem {
424            inner: self,
425            font_system,
426        }
427    }
428
429    /// Process dirty flags: invalidate shape/layout caches as needed, then clear flags.
430    /// Returns `true` if any flags were set (i.e., work may be needed).
431    fn resolve_dirty(&mut self) -> bool {
432        let dirty = self.dirty;
433        if dirty.is_empty() {
434            // individual lines may have been externally invalidated
435            if self.lines.iter().any(|line| line.needs_reshaping()) {
436                self.redraw = true;
437                return true;
438            }
439            return false;
440        }
441
442        if dirty.contains(DirtyFlags::TEXT_SET) {
443            // Lines were replaced — already fresh, no cache to invalidate.
444        } else {
445            if dirty.contains(DirtyFlags::DIRECTION) {
446                for line in &mut self.lines {
447                    if line.shape_opt().is_some() {
448                        line.reset_shaping();
449                    }
450                }
451            } else if dirty.contains(DirtyFlags::TAB_SHAPE) {
452                for line in &mut self.lines {
453                    if line.shape_opt().is_some() && line.text().contains('\t') {
454                        line.reset_shaping();
455                    }
456                }
457            }
458            if dirty.contains(DirtyFlags::RELAYOUT) {
459                for line in &mut self.lines {
460                    if line.shape_opt().is_some() {
461                        line.reset_layout();
462                    }
463                }
464            }
465        }
466
467        self.redraw = true;
468        self.dirty = DirtyFlags::empty();
469        true
470    }
471
472    /// Shape lines until cursor, also scrolling to include cursor in view
473    #[allow(clippy::missing_panics_doc)]
474    pub fn shape_until_cursor(
475        &mut self,
476        font_system: &mut FontSystem,
477        cursor: Cursor,
478        prune: bool,
479    ) {
480        self.shape_until_scroll(font_system, prune);
481        let metrics = self.metrics;
482        let old_scroll = self.scroll;
483
484        let layout_cursor = self
485            .layout_cursor(font_system, cursor)
486            .expect("shape_until_cursor invalid cursor");
487
488        let mut layout_y = 0.0;
489        let mut total_height = {
490            let layout = self
491                .line_layout(font_system, layout_cursor.line)
492                .expect("shape_until_cursor failed to scroll forwards");
493            (0..layout_cursor.layout).for_each(|layout_i| {
494                layout_y += layout[layout_i]
495                    .line_height_opt
496                    .unwrap_or(metrics.line_height);
497            });
498            layout_y
499                + layout[layout_cursor.layout]
500                    .line_height_opt
501                    .unwrap_or(metrics.line_height)
502        };
503
504        if self.scroll.line > layout_cursor.line
505            || (self.scroll.line == layout_cursor.line && self.scroll.vertical > layout_y)
506        {
507            // Adjust scroll backwards if cursor is before it
508            self.scroll.line = layout_cursor.line;
509            self.scroll.vertical = layout_y;
510        } else if let Some(height) = self.height_opt {
511            // Adjust scroll forwards if cursor is after it
512            let mut line_i = layout_cursor.line;
513            if line_i <= self.scroll.line {
514                // This is a single line that may wrap
515                if total_height > height + self.scroll.vertical {
516                    self.scroll.vertical = total_height - height;
517                }
518            } else {
519                while line_i > self.scroll.line {
520                    line_i -= 1;
521                    let layout = self
522                        .line_layout(font_system, line_i)
523                        .expect("shape_until_cursor failed to scroll forwards");
524                    for layout_line in layout {
525                        total_height += layout_line.line_height_opt.unwrap_or(metrics.line_height);
526                    }
527                    if total_height > height + self.scroll.vertical {
528                        self.scroll.line = line_i;
529                        self.scroll.vertical = total_height - height;
530                    }
531                }
532            }
533        }
534
535        if old_scroll != self.scroll {
536            self.dirty |= DirtyFlags::SCROLL;
537        }
538
539        self.shape_until_scroll(font_system, prune);
540
541        // Adjust horizontal scroll to include cursor
542        if let Some(layout_cursor) = self.layout_cursor(font_system, cursor) {
543            if let Some(layout_lines) = self.line_layout(font_system, layout_cursor.line) {
544                if let Some(layout_line) = layout_lines.get(layout_cursor.layout) {
545                    let (x_min, x_max) = layout_line
546                        .glyphs
547                        .get(layout_cursor.glyph)
548                        .or_else(|| layout_line.glyphs.last())
549                        .map_or((0.0, 0.0), |glyph| {
550                            //TODO: use code from cursor_glyph_opt?
551                            let x_a = glyph.x;
552                            let x_b = glyph.x + glyph.w;
553                            (x_a.min(x_b), x_a.max(x_b))
554                        });
555                    if x_min < self.scroll.horizontal {
556                        self.scroll.horizontal = x_min;
557                        self.redraw = true;
558                    }
559                    if let Some(width) = self.width_opt {
560                        if x_max > self.scroll.horizontal + width {
561                            self.scroll.horizontal = x_max - width;
562                            self.redraw = true;
563                        }
564                    }
565                }
566            }
567        }
568    }
569
570    /// Shape lines until scroll, resolving any pending dirty state first.
571    ///
572    /// This processes dirty flags (invalidating caches for lines that need
573    /// reshaping or relayout) and then shapes/layouts visible lines.
574    ///
575    /// Call this before reading layout results via [`layout_runs`] or [`hit`]
576    /// when working with the `Buffer` directly. The [`BorrowedWithFontSystem`]
577    /// wrapper calls this automatically.
578    ///
579    /// [`layout_runs`]: Self::layout_runs
580    /// [`hit`]: Self::hit
581    #[allow(clippy::missing_panics_doc)]
582    pub fn shape_until_scroll(&mut self, font_system: &mut FontSystem, prune: bool) {
583        if !self.resolve_dirty() {
584            return;
585        }
586        let metrics = self.metrics;
587
588        // Clamp scroll.line to valid range (lines may have been removed by editing)
589        if self.scroll.line >= self.lines.len() {
590            self.scroll.line = self.lines.len().saturating_sub(1);
591            self.scroll.vertical = 0.0;
592        }
593
594        let old_scroll = self.scroll;
595
596        loop {
597            // Adjust scroll.layout to be positive by moving scroll.line backwards
598            while self.scroll.vertical < 0.0 {
599                if self.scroll.line > 0 {
600                    let line_i = self.scroll.line - 1;
601                    if let Some(layout) = self.line_layout(font_system, line_i) {
602                        let mut layout_height = 0.0;
603                        for layout_line in layout {
604                            layout_height +=
605                                layout_line.line_height_opt.unwrap_or(metrics.line_height);
606                        }
607                        self.scroll.line = line_i;
608                        self.scroll.vertical += layout_height;
609                    } else {
610                        // If layout is missing, just assume line height
611                        self.scroll.line = line_i;
612                        self.scroll.vertical += metrics.line_height;
613                    }
614                } else {
615                    self.scroll.vertical = 0.0;
616                    break;
617                }
618            }
619
620            let scroll_start = self.scroll.vertical;
621            let scroll_end = scroll_start + self.height_opt.unwrap_or(f32::INFINITY);
622
623            if prune {
624                for line_i in 0..self.scroll.line {
625                    self.lines[line_i].reset_shaping();
626                }
627            }
628            let mut total_height = 0.0;
629            for line_i in self.scroll.line..self.lines.len() {
630                if total_height > scroll_end {
631                    if prune {
632                        self.lines[line_i].reset_shaping();
633                        continue;
634                    }
635                    break;
636                }
637
638                let mut layout_height = 0.0;
639                let layout = self
640                    .line_layout(font_system, line_i)
641                    .expect("shape_until_scroll invalid line");
642                for layout_line in layout {
643                    let line_height = layout_line.line_height_opt.unwrap_or(metrics.line_height);
644                    layout_height += line_height;
645                    total_height += line_height;
646                }
647
648                // Adjust scroll.vertical to be smaller by moving scroll.line forwards
649                if line_i == self.scroll.line && layout_height <= self.scroll.vertical {
650                    self.scroll.line += 1;
651                    self.scroll.vertical -= layout_height;
652                }
653            }
654
655            if total_height < scroll_end && self.scroll.line > 0 {
656                // Need to scroll up to stay inside of buffer
657                self.scroll.vertical -= scroll_end - total_height;
658            } else {
659                // Done adjusting scroll
660                break;
661            }
662        }
663
664        if old_scroll != self.scroll {
665            self.redraw = true;
666        }
667    }
668
669    /// Convert a [`Cursor`] to a [`LayoutCursor`]
670    pub fn layout_cursor(
671        &mut self,
672        font_system: &mut FontSystem,
673        cursor: Cursor,
674    ) -> Option<LayoutCursor> {
675        let layout = self.line_layout(font_system, cursor.line)?;
676        for (layout_i, layout_line) in layout.iter().enumerate() {
677            for (glyph_i, glyph) in layout_line.glyphs.iter().enumerate() {
678                let cursor_end =
679                    Cursor::new_with_affinity(cursor.line, glyph.end, Affinity::Before);
680                let cursor_start =
681                    Cursor::new_with_affinity(cursor.line, glyph.start, Affinity::After);
682                let (cursor_left, cursor_right) = if glyph.level.is_ltr() {
683                    (cursor_start, cursor_end)
684                } else {
685                    (cursor_end, cursor_start)
686                };
687                if cursor == cursor_left {
688                    return Some(LayoutCursor::new(cursor.line, layout_i, glyph_i));
689                }
690                if cursor == cursor_right {
691                    return Some(LayoutCursor::new(cursor.line, layout_i, glyph_i + 1));
692                }
693            }
694        }
695
696        // Fall back to start of line
697        //TODO: should this be the end of the line?
698        Some(LayoutCursor::new(cursor.line, 0, 0))
699    }
700
701    /// Shape the provided line index and return the result
702    pub fn line_shape(
703        &mut self,
704        font_system: &mut FontSystem,
705        line_i: usize,
706    ) -> Option<&ShapeLine> {
707        let line = self.lines.get_mut(line_i)?;
708        Some(line.shape(font_system, self.tab_width, self.direction))
709    }
710
711    /// Lay out the provided line index and return the result
712    pub fn line_layout(
713        &mut self,
714        font_system: &mut FontSystem,
715        line_i: usize,
716    ) -> Option<&[LayoutLine]> {
717        let line = self.lines.get_mut(line_i)?;
718        Some(line.layout(
719            font_system,
720            self.metrics.font_size,
721            self.width_opt,
722            self.wrap,
723            self.ellipsize,
724            self.monospace_width,
725            self.tab_width,
726            self.hinting,
727            self.direction,
728        ))
729    }
730
731    /// Get the current [`Metrics`]
732    pub const fn metrics(&self) -> Metrics {
733        self.metrics
734    }
735
736    /// Set the current [`Metrics`].
737    ///
738    /// # Panics
739    ///
740    /// Will panic if `metrics.font_size` is zero.
741    pub fn set_metrics(&mut self, metrics: Metrics) {
742        if metrics != self.metrics {
743            assert_ne!(metrics.font_size, 0.0, "font size cannot be 0");
744            assert_ne!(metrics.line_height, 0.0, "line height cannot be 0");
745            self.metrics = metrics;
746            self.dirty |= DirtyFlags::RELAYOUT;
747            self.redraw = true;
748        }
749    }
750
751    /// Get the current [`Hinting`] strategy.
752    pub const fn hinting(&self) -> Hinting {
753        self.hinting
754    }
755
756    /// Set the current [`Hinting`] strategy.
757    pub fn set_hinting(&mut self, hinting: Hinting) {
758        if hinting != self.hinting {
759            self.hinting = hinting;
760            self.dirty |= DirtyFlags::RELAYOUT;
761            self.redraw = true;
762        }
763    }
764
765    /// Get the current [`Wrap`]
766    pub const fn wrap(&self) -> Wrap {
767        self.wrap
768    }
769
770    /// Set the current [`Wrap`].
771    pub fn set_wrap(&mut self, wrap: Wrap) {
772        if wrap != self.wrap {
773            self.wrap = wrap;
774            self.dirty |= DirtyFlags::RELAYOUT;
775            self.redraw = true;
776        }
777    }
778
779    /// Get the current [`Ellipsize`]
780    pub const fn ellipsize(&self) -> Ellipsize {
781        self.ellipsize
782    }
783
784    /// Set the current [`Ellipsize`].
785    pub fn set_ellipsize(&mut self, ellipsize: Ellipsize) {
786        if ellipsize != self.ellipsize {
787            self.ellipsize = ellipsize;
788            self.dirty |= DirtyFlags::RELAYOUT;
789            self.redraw = true;
790        }
791    }
792
793    /// Get the current `monospace_width`
794    pub const fn monospace_width(&self) -> Option<f32> {
795        self.monospace_width
796    }
797
798    /// Set monospace width monospace glyphs should be resized to match. `None` means don't resize.
799    pub fn set_monospace_width(&mut self, monospace_width: Option<f32>) {
800        if monospace_width != self.monospace_width {
801            self.monospace_width = monospace_width;
802            self.dirty |= DirtyFlags::RELAYOUT;
803            self.redraw = true;
804        }
805    }
806
807    /// Get the current `tab_width`
808    pub const fn tab_width(&self) -> u16 {
809        self.tab_width
810    }
811
812    /// Set tab width (number of spaces between tab stops).
813    pub fn set_tab_width(&mut self, tab_width: u16) {
814        if tab_width == 0 {
815            return;
816        }
817        if tab_width != self.tab_width {
818            self.tab_width = tab_width;
819            self.dirty |= DirtyFlags::TAB_SHAPE | DirtyFlags::RELAYOUT;
820            self.redraw = true;
821        }
822    }
823
824    /// Get the current base [`Direction`].
825    pub const fn direction(&self) -> Direction {
826        self.direction
827    }
828
829    /// Set the base [`Direction`] used when shaping text.
830    ///
831    /// [`Direction::Auto`] (the default) detects each paragraph's base direction
832    /// from its content. [`Direction::LeftToRight`] and [`Direction::RightToLeft`]
833    /// force it for the whole buffer; use them when you know the direction from
834    /// context such as the UI locale rather than from the text.
835    pub fn set_direction(&mut self, direction: Direction) {
836        if direction != self.direction {
837            self.direction = direction;
838            // DIRECTION reshapes every line, which resets layout as a side effect.
839            self.dirty |= DirtyFlags::DIRECTION;
840            self.redraw = true;
841        }
842    }
843
844    /// Get the current buffer dimensions (width, height)
845    pub const fn size(&self) -> (Option<f32>, Option<f32>) {
846        (self.width_opt, self.height_opt)
847    }
848
849    /// Set the current buffer dimensions.
850    pub fn set_size(&mut self, width_opt: Option<f32>, height_opt: Option<f32>) {
851        let width_clamped = width_opt.map(|v| v.max(0.0));
852        let height_clamped = height_opt.map(|v| v.max(0.0));
853        if width_clamped != self.width_opt {
854            self.width_opt = width_clamped;
855            self.dirty |= DirtyFlags::RELAYOUT;
856            self.redraw = true;
857        }
858        if height_clamped != self.height_opt {
859            self.height_opt = height_clamped;
860            self.dirty |= DirtyFlags::RELAYOUT;
861            self.redraw = true;
862        }
863    }
864
865    /// Set the current [`Metrics`] and buffer dimensions at the same time.
866    ///
867    /// # Panics
868    ///
869    /// Will panic if `metrics.font_size` is zero.
870    pub fn set_metrics_and_size(
871        &mut self,
872        metrics: Metrics,
873        width_opt: Option<f32>,
874        height_opt: Option<f32>,
875    ) {
876        self.set_metrics(metrics);
877        self.set_size(width_opt, height_opt);
878    }
879
880    /// Get the current scroll location
881    pub const fn scroll(&self) -> Scroll {
882        self.scroll
883    }
884
885    /// Set the current scroll location
886    pub fn set_scroll(&mut self, scroll: Scroll) {
887        if scroll != self.scroll {
888            self.scroll = scroll;
889            self.dirty |= DirtyFlags::SCROLL;
890            self.redraw = true;
891        }
892    }
893
894    /// Internal: set text of buffer, reusing existing line allocations.
895    ///
896    /// Does NOT call `shape_until_scroll` — the caller is responsible for that.
897    fn set_text_impl(
898        &mut self,
899        text: &str,
900        attrs: &Attrs,
901        shaping: Shaping,
902        alignment: Option<Align>,
903    ) {
904        let mut line_count = 0;
905        for (range, ending) in LineIter::new(text) {
906            let line_text = &text[range];
907            if line_count < self.lines.len() {
908                // Reuse existing line: reclaim String/AttrsList allocations
909                let mut reused_text = self.lines[line_count].reclaim_text();
910                reused_text.push_str(line_text);
911                let reused_attrs = self.lines[line_count].reclaim_attrs().reset(attrs);
912                self.lines[line_count].reset_new(reused_text, ending, reused_attrs, shaping);
913            } else {
914                self.lines.push(BufferLine::new(
915                    line_text,
916                    ending,
917                    AttrsList::new(attrs),
918                    shaping,
919                ));
920            }
921            line_count += 1;
922        }
923
924        // Ensure there is an ending line with no line ending.
925        // When no lines were produced (empty text), unwrap_or_default() returns
926        // LineEnding::Lf (the Default), which is != None, so we add an empty line.
927        let last_ending = if line_count > 0 {
928            self.lines[line_count - 1].ending()
929        } else {
930            LineEnding::default()
931        };
932        if last_ending != LineEnding::None {
933            if line_count < self.lines.len() {
934                let reused_text = self.lines[line_count].reclaim_text();
935                let reused_attrs = self.lines[line_count].reclaim_attrs().reset(attrs);
936                self.lines[line_count].reset_new(
937                    reused_text,
938                    LineEnding::None,
939                    reused_attrs,
940                    shaping,
941                );
942            } else {
943                self.lines.push(BufferLine::new(
944                    "",
945                    LineEnding::None,
946                    AttrsList::new(attrs),
947                    shaping,
948                ));
949            }
950            line_count += 1;
951        }
952
953        // Discard excess lines now that we have reused as much of the existing allocations as possible.
954        self.lines.truncate(line_count);
955
956        if alignment.is_some() {
957            self.lines.iter_mut().for_each(|line| {
958                line.set_align(alignment);
959            });
960        }
961
962        self.scroll = Scroll::default();
963    }
964
965    /// Set text of buffer, using provided attributes for each line by default.
966    pub fn set_text(
967        &mut self,
968        text: &str,
969        attrs: &Attrs,
970        shaping: Shaping,
971        alignment: Option<Align>,
972    ) {
973        self.set_text_impl(text, attrs, shaping, alignment);
974        self.dirty |= DirtyFlags::TEXT_SET;
975        self.redraw = true;
976    }
977
978    /// Internal: set rich text of buffer, reusing existing line allocations.
979    ///
980    /// Does NOT call `shape_until_scroll` — the caller is responsible for that.
981    fn set_rich_text_impl<'r, 's, I>(
982        &mut self,
983        spans: I,
984        default_attrs: &Attrs,
985        shaping: Shaping,
986        alignment: Option<Align>,
987    ) where
988        I: IntoIterator<Item = (&'s str, Attrs<'r>)>,
989    {
990        let mut end = 0;
991        // TODO: find a way to cache this string and vec for reuse
992        let (string, spans_data): (String, Vec<_>) = spans
993            .into_iter()
994            .map(|(s, attrs)| {
995                let start = end;
996                end += s.len();
997                (s, (attrs, start..end))
998            })
999            .unzip();
1000
1001        let mut spans_iter = spans_data.into_iter();
1002        let mut maybe_span = spans_iter.next();
1003
1004        // split the string into lines, as ranges
1005        let string_start = string.as_ptr() as usize;
1006        let mut lines_iter = BidiParagraphs::new(&string).map(|line: &str| {
1007            let start = line.as_ptr() as usize - string_start;
1008            let end = start + line.len();
1009            start..end
1010        });
1011        let mut maybe_line = lines_iter.next();
1012        //TODO: set this based on information from spans
1013        let line_ending = LineEnding::default();
1014
1015        let mut line_count = 0;
1016        let mut attrs_list = self
1017            .lines
1018            .get_mut(line_count)
1019            .map_or_else(|| AttrsList::new(&Attrs::new()), BufferLine::reclaim_attrs)
1020            .reset(default_attrs);
1021        let mut line_string = self
1022            .lines
1023            .get_mut(line_count)
1024            .map(BufferLine::reclaim_text)
1025            .unwrap_or_default();
1026
1027        loop {
1028            let (Some(line_range), Some((attrs, span_range))) = (&maybe_line, &maybe_span) else {
1029                // this is reached only if this text is empty
1030                if self.lines.len() == line_count {
1031                    self.lines.push(BufferLine::empty());
1032                }
1033                self.lines[line_count].reset_new(
1034                    String::new(),
1035                    line_ending,
1036                    AttrsList::new(default_attrs),
1037                    shaping,
1038                );
1039                line_count += 1;
1040                break;
1041            };
1042
1043            // start..end is the intersection of this line and this span
1044            let start = line_range.start.max(span_range.start);
1045            let end = line_range.end.min(span_range.end);
1046            if start < end {
1047                let text = &string[start..end];
1048                let text_start = line_string.len();
1049                line_string.push_str(text);
1050                let text_end = line_string.len();
1051                // Only add attrs if they don't match the defaults
1052                if *attrs != attrs_list.defaults() {
1053                    attrs_list.add_span(text_start..text_end, attrs);
1054                }
1055            } else if line_string.is_empty() && attrs.metrics_opt.is_some() {
1056                // reset the attrs list with the span's attrs so the line height
1057                // matches the span's font size rather than falling back to
1058                // the buffer default
1059                attrs_list = attrs_list.reset(attrs);
1060            }
1061
1062            // we know that at the end of a line,
1063            // span text's end index is always >= line text's end index
1064            // so if this span ends before this line ends,
1065            // there is another span in this line.
1066            // otherwise, we move on to the next line.
1067            if span_range.end < line_range.end {
1068                maybe_span = spans_iter.next();
1069            } else {
1070                maybe_line = lines_iter.next();
1071                if maybe_line.is_some() {
1072                    // finalize this line and start a new line
1073                    let next_attrs_list = self
1074                        .lines
1075                        .get_mut(line_count + 1)
1076                        .map_or_else(|| AttrsList::new(&Attrs::new()), BufferLine::reclaim_attrs)
1077                        .reset(default_attrs);
1078                    let next_line_string = self
1079                        .lines
1080                        .get_mut(line_count + 1)
1081                        .map(BufferLine::reclaim_text)
1082                        .unwrap_or_default();
1083                    let prev_attrs_list = core::mem::replace(&mut attrs_list, next_attrs_list);
1084                    let prev_line_string = core::mem::replace(&mut line_string, next_line_string);
1085                    if self.lines.len() == line_count {
1086                        self.lines.push(BufferLine::empty());
1087                    }
1088                    self.lines[line_count].reset_new(
1089                        prev_line_string,
1090                        line_ending,
1091                        prev_attrs_list,
1092                        shaping,
1093                    );
1094                    line_count += 1;
1095                } else {
1096                    // finalize the final line
1097                    if self.lines.len() == line_count {
1098                        self.lines.push(BufferLine::empty());
1099                    }
1100                    self.lines[line_count].reset_new(line_string, line_ending, attrs_list, shaping);
1101                    line_count += 1;
1102                    break;
1103                }
1104            }
1105        }
1106
1107        // Discard excess lines now that we have reused as much of the existing allocations as possible.
1108        self.lines.truncate(line_count);
1109
1110        self.lines.iter_mut().for_each(|line| {
1111            line.set_align(alignment);
1112        });
1113
1114        self.scroll = Scroll::default();
1115    }
1116
1117    /// Set text of buffer, using an iterator of styled spans (pairs of text and attributes).
1118    ///
1119    /// ```
1120    /// # use cosmic_text::{Attrs, Buffer, Family, FontSystem, Metrics, Shaping};
1121    /// # let mut font_system = FontSystem::new();
1122    /// let mut buffer = Buffer::new_empty(Metrics::new(32.0, 44.0));
1123    /// let attrs = Attrs::new().family(Family::Serif);
1124    /// buffer.set_rich_text(
1125    ///     [
1126    ///         ("hello, ", attrs.clone()),
1127    ///         ("cosmic\ntext", attrs.clone().family(Family::Monospace)),
1128    ///     ],
1129    ///     &attrs,
1130    ///     Shaping::Advanced,
1131    ///     None,
1132    /// );
1133    /// ```
1134    pub fn set_rich_text<'r, 's, I>(
1135        &mut self,
1136        spans: I,
1137        default_attrs: &Attrs,
1138        shaping: Shaping,
1139        alignment: Option<Align>,
1140    ) where
1141        I: IntoIterator<Item = (&'s str, Attrs<'r>)>,
1142    {
1143        self.set_rich_text_impl(spans, default_attrs, shaping, alignment);
1144        self.dirty |= DirtyFlags::TEXT_SET;
1145        self.redraw = true;
1146    }
1147
1148    /// True if a redraw is needed
1149    pub const fn redraw(&self) -> bool {
1150        self.redraw
1151    }
1152
1153    /// Set redraw needed flag
1154    pub fn set_redraw(&mut self, redraw: bool) {
1155        self.redraw = redraw;
1156    }
1157
1158    /// Get the visible layout runs for rendering and other tasks.
1159    ///
1160    /// This returns an iterator over the laid-out runs that are visible in the
1161    /// current scroll region. Call [`shape_until_scroll`] first to ensure the buffer
1162    /// is up to date, or use [`BorrowedWithFontSystem`] which calls it
1163    /// automatically.
1164    ///
1165    /// [`shape_until_scroll`]: Self::shape_until_scroll
1166    pub fn layout_runs(&self) -> LayoutRunIter<'_> {
1167        LayoutRunIter::new(self)
1168    }
1169
1170    /// Convert x, y position to Cursor (hit detection).
1171    ///
1172    /// Call [`shape_until_scroll`] first to ensure the buffer is up to date,
1173    /// or use [`BorrowedWithFontSystem`] which calls it automatically.
1174    ///
1175    /// [`shape_until_scroll`]: Self::shape_until_scroll
1176    pub fn hit(&self, x: f32, y: f32) -> Option<Cursor> {
1177        #[cfg(all(feature = "std", not(target_arch = "wasm32")))]
1178        let instant = std::time::Instant::now();
1179
1180        let mut new_cursor_opt = None;
1181
1182        let mut runs = self.layout_runs().peekable();
1183        let mut first_run = true;
1184        while let Some(run) = runs.next() {
1185            let line_top = run.line_top;
1186            let line_height = run.line_height;
1187
1188            if first_run && y < line_top {
1189                first_run = false;
1190                let new_cursor = Cursor::new(run.line_i, 0);
1191                new_cursor_opt = Some(new_cursor);
1192            } else if y >= line_top && y < line_top + line_height {
1193                let mut new_cursor_glyph = run.glyphs.len();
1194                let mut new_cursor_char = 0;
1195                let mut new_cursor_affinity = Affinity::After;
1196
1197                let mut first_glyph = true;
1198
1199                'hit: for (glyph_i, glyph) in run.glyphs.iter().enumerate() {
1200                    if first_glyph {
1201                        first_glyph = false;
1202                        if (run.rtl && x > glyph.x) || (!run.rtl && x < 0.0) {
1203                            new_cursor_glyph = 0;
1204                            new_cursor_char = 0;
1205                        }
1206                    }
1207                    if x >= glyph.x && x <= glyph.x + glyph.w {
1208                        new_cursor_glyph = glyph_i;
1209
1210                        let cluster = &run.text[glyph.start..glyph.end];
1211                        let total = cluster.grapheme_indices(true).count();
1212                        let mut egc_x = glyph.x;
1213                        let egc_w = glyph.w / (total as f32);
1214                        for (egc_i, egc) in cluster.grapheme_indices(true) {
1215                            if x >= egc_x && x <= egc_x + egc_w {
1216                                new_cursor_char = egc_i;
1217
1218                                let right_half = x >= egc_x + egc_w / 2.0;
1219                                if right_half != glyph.level.is_rtl() {
1220                                    // If clicking on last half of glyph, move cursor past glyph
1221                                    new_cursor_char += egc.len();
1222                                    new_cursor_affinity = Affinity::Before;
1223                                }
1224                                break 'hit;
1225                            }
1226                            egc_x += egc_w;
1227                        }
1228
1229                        let right_half = x >= glyph.x + glyph.w / 2.0;
1230                        if right_half != glyph.level.is_rtl() {
1231                            // If clicking on last half of glyph, move cursor past glyph
1232                            new_cursor_char = cluster.len();
1233                            new_cursor_affinity = Affinity::Before;
1234                        }
1235                        break 'hit;
1236                    }
1237                }
1238
1239                let mut new_cursor = Cursor::new(run.line_i, 0);
1240
1241                match run.glyphs.get(new_cursor_glyph) {
1242                    Some(glyph) => {
1243                        // Position at glyph
1244                        new_cursor.index = glyph.start + new_cursor_char;
1245                        new_cursor.affinity = new_cursor_affinity;
1246                    }
1247                    None => {
1248                        // Click was past all glyphs in this visual run.
1249                        // Use the maximum glyph.end across all glyphs.
1250                        // this is the logical end of this visual line's byte coverage,
1251                        // correct for LTR, RTL, mixed-BiDi, and wrapped paragraphs.
1252                        let run_end = run.glyphs.iter().map(|g| g.end).max().unwrap_or(0);
1253                        new_cursor.index = run_end;
1254                        new_cursor.affinity = Affinity::Before;
1255                    }
1256                }
1257
1258                new_cursor_opt = Some(new_cursor);
1259
1260                break;
1261            } else if runs.peek().is_none() && y > run.line_y {
1262                // Click below the last run: place cursor at the logical end of the
1263                // line, regardless of paragraph direction or BiDi mixing.
1264                let new_cursor =
1265                    Cursor::new_with_affinity(run.line_i, run.text.len(), Affinity::Before);
1266                new_cursor_opt = Some(new_cursor);
1267            }
1268        }
1269
1270        #[cfg(all(feature = "std", not(target_arch = "wasm32")))]
1271        log::trace!("click({}, {}): {:?}", x, y, instant.elapsed());
1272
1273        new_cursor_opt
1274    }
1275
1276    /// Returns the visual (x, y) position of a cursor within the buffer.
1277    /// y is the top of the line containing the cursor.
1278    /// This is a convenience wrapper around [`LayoutRun::cursor_position`].
1279    pub fn cursor_position(&self, cursor: &Cursor) -> Option<(f32, f32)> {
1280        self.layout_runs()
1281            .filter(|run| run.line_i == cursor.line)
1282            .find_map(|run| run.cursor_position(cursor).map(|x| (x, run.line_top)))
1283    }
1284
1285    /// Returns if the text direction for a given line is RTL
1286    /// Returns `None` if the line doesn't exist or hasn't been shaped yet.
1287    pub fn is_rtl(&self, line: usize) -> Option<bool> {
1288        self.lines.get(line)?.shape_opt().map(|shape| shape.rtl)
1289    }
1290
1291    /// Apply a [`Motion`] to a [`Cursor`]
1292    pub fn cursor_motion(
1293        &mut self,
1294        font_system: &mut FontSystem,
1295        mut cursor: Cursor,
1296        mut cursor_x_opt: Option<i32>,
1297        motion: Motion,
1298    ) -> Option<(Cursor, Option<i32>)> {
1299        match motion {
1300            Motion::LayoutCursor(layout_cursor) => {
1301                let layout = self.line_layout(font_system, layout_cursor.line)?;
1302
1303                let layout_line = match layout.get(layout_cursor.layout) {
1304                    Some(some) => some,
1305                    None => match layout.last() {
1306                        Some(some) => some,
1307                        None => {
1308                            return None;
1309                        }
1310                    },
1311                };
1312
1313                let (new_index, new_affinity) =
1314                    layout_line.glyphs.get(layout_cursor.glyph).map_or_else(
1315                        || {
1316                            layout_line
1317                                .glyphs
1318                                .last()
1319                                .map_or((0, Affinity::After), |glyph| (glyph.end, Affinity::Before))
1320                        },
1321                        |glyph| (glyph.start, Affinity::After),
1322                    );
1323
1324                if cursor.line != layout_cursor.line
1325                    || cursor.index != new_index
1326                    || cursor.affinity != new_affinity
1327                {
1328                    cursor.line = layout_cursor.line;
1329                    cursor.index = new_index;
1330                    cursor.affinity = new_affinity;
1331                }
1332            }
1333            Motion::Previous => {
1334                let line = self.lines.get(cursor.line)?;
1335                if cursor.index > 0 {
1336                    // Find previous character index
1337                    let mut prev_index = 0;
1338                    for (i, _) in line.text().grapheme_indices(true) {
1339                        if i < cursor.index {
1340                            prev_index = i;
1341                        } else {
1342                            break;
1343                        }
1344                    }
1345
1346                    cursor.index = prev_index;
1347                    cursor.affinity = Affinity::After;
1348                } else if cursor.line > 0 {
1349                    cursor.line -= 1;
1350                    cursor.index = self.lines.get(cursor.line)?.text().len();
1351                    cursor.affinity = Affinity::After;
1352                }
1353                cursor_x_opt = None;
1354            }
1355            Motion::Next => {
1356                let line = self.lines.get(cursor.line)?;
1357                if cursor.index < line.text().len() {
1358                    for (i, c) in line.text().grapheme_indices(true) {
1359                        if i == cursor.index {
1360                            cursor.index += c.len();
1361                            cursor.affinity = Affinity::Before;
1362                            break;
1363                        }
1364                    }
1365                } else if cursor.line + 1 < self.lines.len() {
1366                    cursor.line += 1;
1367                    cursor.index = 0;
1368                    cursor.affinity = Affinity::Before;
1369                }
1370                cursor_x_opt = None;
1371            }
1372            Motion::Left => {
1373                let rtl_opt = self
1374                    .line_shape(font_system, cursor.line)
1375                    .map(|shape| shape.rtl);
1376                if let Some(rtl) = rtl_opt {
1377                    if rtl {
1378                        (cursor, cursor_x_opt) =
1379                            self.cursor_motion(font_system, cursor, cursor_x_opt, Motion::Next)?;
1380                    } else {
1381                        (cursor, cursor_x_opt) = self.cursor_motion(
1382                            font_system,
1383                            cursor,
1384                            cursor_x_opt,
1385                            Motion::Previous,
1386                        )?;
1387                    }
1388                }
1389            }
1390            Motion::Right => {
1391                let rtl_opt = self
1392                    .line_shape(font_system, cursor.line)
1393                    .map(|shape| shape.rtl);
1394                if let Some(rtl) = rtl_opt {
1395                    if rtl {
1396                        (cursor, cursor_x_opt) = self.cursor_motion(
1397                            font_system,
1398                            cursor,
1399                            cursor_x_opt,
1400                            Motion::Previous,
1401                        )?;
1402                    } else {
1403                        (cursor, cursor_x_opt) =
1404                            self.cursor_motion(font_system, cursor, cursor_x_opt, Motion::Next)?;
1405                    }
1406                }
1407            }
1408            Motion::Up => {
1409                let mut layout_cursor = self.layout_cursor(font_system, cursor)?;
1410
1411                if cursor_x_opt.is_none() {
1412                    cursor_x_opt = Some(
1413                        layout_cursor.glyph as i32, //TODO: glyph x position
1414                    );
1415                }
1416
1417                if layout_cursor.layout > 0 {
1418                    layout_cursor.layout -= 1;
1419                } else if layout_cursor.line > 0 {
1420                    layout_cursor.line -= 1;
1421                    layout_cursor.layout = usize::MAX;
1422                }
1423
1424                if let Some(cursor_x) = cursor_x_opt {
1425                    layout_cursor.glyph = cursor_x as usize; //TODO: glyph x position
1426                }
1427
1428                (cursor, cursor_x_opt) = self.cursor_motion(
1429                    font_system,
1430                    cursor,
1431                    cursor_x_opt,
1432                    Motion::LayoutCursor(layout_cursor),
1433                )?;
1434            }
1435            Motion::Down => {
1436                let mut layout_cursor = self.layout_cursor(font_system, cursor)?;
1437
1438                let layout_len = self.line_layout(font_system, layout_cursor.line)?.len();
1439
1440                if cursor_x_opt.is_none() {
1441                    cursor_x_opt = Some(
1442                        layout_cursor.glyph as i32, //TODO: glyph x position
1443                    );
1444                }
1445
1446                if layout_cursor.layout + 1 < layout_len {
1447                    layout_cursor.layout += 1;
1448                } else if layout_cursor.line + 1 < self.lines.len() {
1449                    layout_cursor.line += 1;
1450                    layout_cursor.layout = 0;
1451                }
1452
1453                if let Some(cursor_x) = cursor_x_opt {
1454                    layout_cursor.glyph = cursor_x as usize; //TODO: glyph x position
1455                }
1456
1457                (cursor, cursor_x_opt) = self.cursor_motion(
1458                    font_system,
1459                    cursor,
1460                    cursor_x_opt,
1461                    Motion::LayoutCursor(layout_cursor),
1462                )?;
1463            }
1464            Motion::Home => {
1465                cursor.index = 0;
1466                cursor_x_opt = None;
1467            }
1468            Motion::SoftHome => {
1469                let line = self.lines.get(cursor.line)?;
1470                cursor.index = line
1471                    .text()
1472                    .char_indices()
1473                    .find_map(|(i, c)| if c.is_whitespace() { None } else { Some(i) })
1474                    .unwrap_or(0);
1475                cursor_x_opt = None;
1476            }
1477            Motion::End => {
1478                let line = self.lines.get(cursor.line)?;
1479                cursor.index = line.text().len();
1480                cursor_x_opt = None;
1481            }
1482            Motion::ParagraphStart => {
1483                cursor.index = 0;
1484                cursor_x_opt = None;
1485            }
1486            Motion::ParagraphEnd => {
1487                cursor.index = self.lines.get(cursor.line)?.text().len();
1488                cursor_x_opt = None;
1489            }
1490            Motion::PageUp => {
1491                if let Some(height) = self.height_opt {
1492                    (cursor, cursor_x_opt) = self.cursor_motion(
1493                        font_system,
1494                        cursor,
1495                        cursor_x_opt,
1496                        Motion::Vertical(-height as i32),
1497                    )?;
1498                }
1499            }
1500            Motion::PageDown => {
1501                if let Some(height) = self.height_opt {
1502                    (cursor, cursor_x_opt) = self.cursor_motion(
1503                        font_system,
1504                        cursor,
1505                        cursor_x_opt,
1506                        Motion::Vertical(height as i32),
1507                    )?;
1508                }
1509            }
1510            Motion::Vertical(px) => {
1511                // TODO more efficient, use layout run line height
1512                let lines = px / self.metrics().line_height as i32;
1513                match lines.cmp(&0) {
1514                    cmp::Ordering::Less => {
1515                        for _ in 0..-lines {
1516                            (cursor, cursor_x_opt) =
1517                                self.cursor_motion(font_system, cursor, cursor_x_opt, Motion::Up)?;
1518                        }
1519                    }
1520                    cmp::Ordering::Greater => {
1521                        for _ in 0..lines {
1522                            (cursor, cursor_x_opt) = self.cursor_motion(
1523                                font_system,
1524                                cursor,
1525                                cursor_x_opt,
1526                                Motion::Down,
1527                            )?;
1528                        }
1529                    }
1530                    cmp::Ordering::Equal => {}
1531                }
1532            }
1533            Motion::PreviousWord => {
1534                let line = self.lines.get(cursor.line)?;
1535                if cursor.index > 0 {
1536                    cursor.index = line
1537                        .text()
1538                        .unicode_word_indices()
1539                        .rev()
1540                        .map(|(i, _)| i)
1541                        .find(|&i| i < cursor.index)
1542                        .unwrap_or(0);
1543                } else if cursor.line > 0 {
1544                    cursor.line -= 1;
1545                    cursor.index = self.lines.get(cursor.line)?.text().len();
1546                }
1547                cursor_x_opt = None;
1548            }
1549            Motion::NextWord => {
1550                let line = self.lines.get(cursor.line)?;
1551                if cursor.index < line.text().len() {
1552                    cursor.index = line
1553                        .text()
1554                        .unicode_word_indices()
1555                        .map(|(i, word)| i + word.len())
1556                        .find(|&i| i > cursor.index)
1557                        .unwrap_or_else(|| line.text().len());
1558                } else if cursor.line + 1 < self.lines.len() {
1559                    cursor.line += 1;
1560                    cursor.index = 0;
1561                }
1562                cursor_x_opt = None;
1563            }
1564            Motion::LeftWord => {
1565                let rtl_opt = self
1566                    .line_shape(font_system, cursor.line)
1567                    .map(|shape| shape.rtl);
1568                if let Some(rtl) = rtl_opt {
1569                    if rtl {
1570                        (cursor, cursor_x_opt) = self.cursor_motion(
1571                            font_system,
1572                            cursor,
1573                            cursor_x_opt,
1574                            Motion::NextWord,
1575                        )?;
1576                    } else {
1577                        (cursor, cursor_x_opt) = self.cursor_motion(
1578                            font_system,
1579                            cursor,
1580                            cursor_x_opt,
1581                            Motion::PreviousWord,
1582                        )?;
1583                    }
1584                }
1585            }
1586            Motion::RightWord => {
1587                let rtl_opt = self
1588                    .line_shape(font_system, cursor.line)
1589                    .map(|shape| shape.rtl);
1590                if let Some(rtl) = rtl_opt {
1591                    if rtl {
1592                        (cursor, cursor_x_opt) = self.cursor_motion(
1593                            font_system,
1594                            cursor,
1595                            cursor_x_opt,
1596                            Motion::PreviousWord,
1597                        )?;
1598                    } else {
1599                        (cursor, cursor_x_opt) = self.cursor_motion(
1600                            font_system,
1601                            cursor,
1602                            cursor_x_opt,
1603                            Motion::NextWord,
1604                        )?;
1605                    }
1606                }
1607            }
1608            Motion::BufferStart => {
1609                cursor.line = 0;
1610                cursor.index = 0;
1611                cursor_x_opt = None;
1612            }
1613            Motion::BufferEnd => {
1614                cursor.line = self.lines.len().saturating_sub(1);
1615                cursor.index = self.lines.get(cursor.line)?.text().len();
1616                cursor_x_opt = None;
1617            }
1618            Motion::GotoLine(line) => {
1619                let mut layout_cursor = self.layout_cursor(font_system, cursor)?;
1620                layout_cursor.line = line;
1621                (cursor, cursor_x_opt) = self.cursor_motion(
1622                    font_system,
1623                    cursor,
1624                    cursor_x_opt,
1625                    Motion::LayoutCursor(layout_cursor),
1626                )?;
1627            }
1628        }
1629        Some((cursor, cursor_x_opt))
1630    }
1631
1632    /// Draw the buffer.
1633    ///
1634    /// Automatically resolves any pending dirty state before drawing.
1635    #[cfg(feature = "swash")]
1636    pub fn draw<F>(
1637        &mut self,
1638        font_system: &mut FontSystem,
1639        cache: &mut crate::SwashCache,
1640        color: Color,
1641        callback: F,
1642    ) where
1643        F: FnMut(i32, i32, u32, u32, Color),
1644    {
1645        self.shape_until_scroll(font_system, false);
1646        let mut renderer = crate::LegacyRenderer {
1647            font_system,
1648            cache,
1649            callback,
1650        };
1651        for run in self.layout_runs() {
1652            for glyph in run.glyphs {
1653                let physical_glyph = glyph.physical((0., run.line_y), 1.0);
1654                let glyph_color = glyph.color_opt.map_or(color, |some| some);
1655                renderer.glyph(physical_glyph, glyph_color);
1656            }
1657            render_decoration(&mut renderer, &run, color);
1658        }
1659    }
1660
1661    /// Render the buffer using the provided renderer.
1662    ///
1663    /// Automatically resolves any pending dirty state before rendering.
1664    pub fn render<R: Renderer>(
1665        &mut self,
1666        font_system: &mut FontSystem,
1667        renderer: &mut R,
1668        color: Color,
1669    ) {
1670        self.shape_until_scroll(font_system, false);
1671        for run in self.layout_runs() {
1672            for glyph in run.glyphs {
1673                let physical_glyph = glyph.physical((0., run.line_y), 1.0);
1674                let glyph_color = glyph.color_opt.map_or(color, |some| some);
1675                renderer.glyph(physical_glyph, glyph_color);
1676            }
1677            // draw decorations after glyphs so strikethrough is over the glyphs
1678            render_decoration(renderer, &run, color);
1679        }
1680    }
1681}
1682
1683impl BorrowedWithFontSystem<'_, Buffer> {
1684    /// Shape lines until cursor, also scrolling to include cursor in view
1685    pub fn shape_until_cursor(&mut self, cursor: Cursor, prune: bool) {
1686        self.inner
1687            .shape_until_cursor(self.font_system, cursor, prune);
1688    }
1689
1690    /// Shape the provided line index and return the result
1691    pub fn line_shape(&mut self, line_i: usize) -> Option<&ShapeLine> {
1692        self.inner.line_shape(self.font_system, line_i)
1693    }
1694
1695    /// Lay out the provided line index and return the result
1696    pub fn line_layout(&mut self, line_i: usize) -> Option<&[LayoutLine]> {
1697        self.inner.line_layout(self.font_system, line_i)
1698    }
1699
1700    /// Set the current [`Metrics`].
1701    ///
1702    /// # Panics
1703    ///
1704    /// Will panic if `metrics.font_size` is zero.
1705    pub fn set_metrics(&mut self, metrics: Metrics) {
1706        self.inner.set_metrics(metrics);
1707    }
1708
1709    /// Set the current [`Hinting`] strategy.
1710    pub fn set_hinting(&mut self, hinting: Hinting) {
1711        self.inner.set_hinting(hinting);
1712    }
1713
1714    /// Set the current [`Wrap`].
1715    pub fn set_wrap(&mut self, wrap: Wrap) {
1716        self.inner.set_wrap(wrap);
1717    }
1718
1719    /// Set the base [`Direction`] used when shaping text.
1720    pub fn set_direction(&mut self, direction: Direction) {
1721        self.inner.set_direction(direction);
1722    }
1723
1724    /// Set the current [`Ellipsize`].
1725    pub fn set_ellipsize(&mut self, ellipsize: Ellipsize) {
1726        self.inner.set_ellipsize(ellipsize);
1727    }
1728
1729    /// Set the current buffer dimensions.
1730    pub fn set_size(&mut self, width_opt: Option<f32>, height_opt: Option<f32>) {
1731        self.inner.set_size(width_opt, height_opt);
1732    }
1733
1734    /// Set the current [`Metrics`] and buffer dimensions at the same time.
1735    ///
1736    /// # Panics
1737    ///
1738    /// Will panic if `metrics.font_size` is zero.
1739    pub fn set_metrics_and_size(
1740        &mut self,
1741        metrics: Metrics,
1742        width_opt: Option<f32>,
1743        height_opt: Option<f32>,
1744    ) {
1745        self.inner
1746            .set_metrics_and_size(metrics, width_opt, height_opt);
1747    }
1748
1749    /// Set tab width (number of spaces between tab stops).
1750    ///
1751    /// A `tab_width` of 0 is ignored.
1752    pub fn set_tab_width(&mut self, tab_width: u16) {
1753        self.inner.set_tab_width(tab_width);
1754    }
1755
1756    /// Set monospace width monospace glyphs should be resized to match. `None` means don't resize.
1757    pub fn set_monospace_width(&mut self, monospace_width: Option<f32>) {
1758        self.inner.set_monospace_width(monospace_width);
1759    }
1760
1761    /// Set text of buffer, using provided attributes for each line by default.
1762    pub fn set_text(
1763        &mut self,
1764        text: &str,
1765        attrs: &Attrs,
1766        shaping: Shaping,
1767        alignment: Option<Align>,
1768    ) {
1769        self.inner.set_text(text, attrs, shaping, alignment);
1770    }
1771
1772    /// Set text of buffer, using an iterator of styled spans (pairs of text and attributes).
1773    pub fn set_rich_text<'r, 's, I>(
1774        &mut self,
1775        spans: I,
1776        default_attrs: &Attrs,
1777        shaping: Shaping,
1778        alignment: Option<Align>,
1779    ) where
1780        I: IntoIterator<Item = (&'s str, Attrs<'r>)>,
1781    {
1782        self.inner
1783            .set_rich_text(spans, default_attrs, shaping, alignment);
1784    }
1785
1786    /// Shape lines until scroll, resolving any pending dirty state first.
1787    ///
1788    /// See [`Buffer::shape_until_scroll`].
1789    pub fn shape_until_scroll(&mut self, prune: bool) {
1790        self.inner.shape_until_scroll(self.font_system, prune);
1791    }
1792
1793    /// Get the visible layout runs for rendering and other tasks.
1794    ///
1795    /// Automatically resolves any pending dirty state.
1796    pub fn layout_runs(&mut self) -> LayoutRunIter<'_> {
1797        self.inner.shape_until_scroll(self.font_system, false);
1798        self.inner.layout_runs()
1799    }
1800
1801    /// Convert x, y position to Cursor (hit detection).
1802    ///
1803    /// Automatically resolves any pending dirty state.
1804    pub fn hit(&mut self, x: f32, y: f32) -> Option<Cursor> {
1805        self.inner.shape_until_scroll(self.font_system, false);
1806        self.inner.hit(x, y)
1807    }
1808
1809    /// Apply a [`Motion`] to a [`Cursor`]
1810    pub fn cursor_motion(
1811        &mut self,
1812        cursor: Cursor,
1813        cursor_x_opt: Option<i32>,
1814        motion: Motion,
1815    ) -> Option<(Cursor, Option<i32>)> {
1816        self.inner
1817            .cursor_motion(self.font_system, cursor, cursor_x_opt, motion)
1818    }
1819
1820    /// Draw the buffer.
1821    ///
1822    /// Automatically resolves any pending dirty state.
1823    #[cfg(feature = "swash")]
1824    pub fn draw<F>(&mut self, cache: &mut crate::SwashCache, color: Color, f: F)
1825    where
1826        F: FnMut(i32, i32, u32, u32, Color),
1827    {
1828        self.inner.draw(self.font_system, cache, color, f);
1829    }
1830}