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