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