Skip to main content

rumatui_tui/
buffer.rs

1use std::cmp::min;
2use std::fmt;
3use std::usize;
4
5use unicode_segmentation::UnicodeSegmentation;
6use unicode_width::UnicodeWidthStr;
7
8use crate::layout::Rect;
9use crate::style::{Color, Modifier, Style};
10
11/// A buffer cell
12#[derive(Debug, Clone, PartialEq)]
13pub struct Cell {
14    pub symbol: String,
15    pub style: Style,
16}
17
18impl Cell {
19    pub fn set_symbol(&mut self, symbol: &str) -> &mut Cell {
20        self.symbol.clear();
21        self.symbol.push_str(symbol);
22        self
23    }
24
25    pub fn set_char(&mut self, ch: char) -> &mut Cell {
26        self.symbol.clear();
27        self.symbol.push(ch);
28        self
29    }
30
31    pub fn set_fg(&mut self, color: Color) -> &mut Cell {
32        self.style.fg = color;
33        self
34    }
35
36    pub fn set_bg(&mut self, color: Color) -> &mut Cell {
37        self.style.bg = color;
38        self
39    }
40
41    pub fn set_modifier(&mut self, modifier: Modifier) -> &mut Cell {
42        self.style.modifier = modifier;
43        self
44    }
45
46    pub fn set_style(&mut self, style: Style) -> &mut Cell {
47        self.style = style;
48        self
49    }
50
51    pub fn reset(&mut self) {
52        self.symbol.clear();
53        self.symbol.push(' ');
54        self.style.reset();
55    }
56}
57
58impl Default for Cell {
59    fn default() -> Cell {
60        Cell {
61            symbol: " ".into(),
62            style: Default::default(),
63        }
64    }
65}
66
67/// A buffer that maps to the desired content of the terminal after the draw call
68///
69/// No widget in the library interacts directly with the terminal. Instead each of them is required
70/// to draw their state to an intermediate buffer. It is basically a grid where each cell contains
71/// a grapheme, a foreground color and a background color. This grid will then be used to output
72/// the appropriate escape sequences and characters to draw the UI as the user has defined it.
73///
74/// # Examples:
75///
76/// ```
77/// use rumatui_tui::buffer::{Buffer, Cell};
78/// use rumatui_tui::layout::Rect;
79/// use rumatui_tui::style::{Color, Style, Modifier};
80///
81/// let mut buf = Buffer::empty(Rect{x: 0, y: 0, width: 10, height: 5});
82/// buf.get_mut(0, 2).set_symbol("x");
83/// assert_eq!(buf.get(0, 2).symbol, "x");
84/// buf.set_string(3, 0, "string", Style::default().fg(Color::Red).bg(Color::White));
85/// assert_eq!(buf.get(5, 0), &Cell{
86///     symbol: String::from("r"),
87///     style: Style {
88///         fg: Color::Red,
89///         bg: Color::White,
90///         modifier: Modifier::empty()
91///     }});
92/// buf.get_mut(5, 0).set_char('x');
93/// assert_eq!(buf.get(5, 0).symbol, "x");
94/// ```
95#[derive(Clone, PartialEq)]
96pub struct Buffer {
97    /// The area represented by this buffer
98    pub area: Rect,
99    /// The content of the buffer. The length of this Vec should always be equal to area.width *
100    /// area.height
101    pub content: Vec<Cell>,
102}
103
104impl Default for Buffer {
105    fn default() -> Buffer {
106        Buffer {
107            area: Default::default(),
108            content: Vec::new(),
109        }
110    }
111}
112
113impl fmt::Debug for Buffer {
114    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
115        writeln!(f, "Buffer: {:?}", self.area)?;
116        f.write_str("Content (quoted lines):\n")?;
117        for cells in self.content.chunks(self.area.width as usize) {
118            let mut line = String::new();
119            let mut overwritten = vec![];
120            let mut skip: usize = 0;
121            for (x, c) in cells.iter().enumerate() {
122                if skip == 0 {
123                    line.push_str(&c.symbol);
124                } else {
125                    overwritten.push((x, &c.symbol))
126                }
127                skip = std::cmp::max(skip, c.symbol.width()).saturating_sub(1);
128            }
129            f.write_fmt(format_args!("{:?},", line))?;
130            if !overwritten.is_empty() {
131                f.write_fmt(format_args!(
132                    " Hidden by multi-width symbols: {:?}",
133                    overwritten
134                ))?;
135            }
136            f.write_str("\n")?;
137        }
138        f.write_str("Style:\n")?;
139        for cells in self.content.chunks(self.area.width as usize) {
140            f.write_str("|")?;
141            for cell in cells {
142                write!(
143                    f,
144                    "{} {} {}|",
145                    cell.style.fg.code(),
146                    cell.style.bg.code(),
147                    cell.style.modifier.code()
148                )?;
149            }
150            f.write_str("\n")?;
151        }
152        Ok(())
153    }
154}
155
156impl Buffer {
157    /// Returns a Buffer with all cells set to the default one
158    pub fn empty(area: Rect) -> Buffer {
159        let cell: Cell = Default::default();
160        Buffer::filled(area, &cell)
161    }
162
163    /// Returns a Buffer with all cells initialized with the attributes of the given Cell
164    pub fn filled(area: Rect, cell: &Cell) -> Buffer {
165        let size = area.area() as usize;
166        let content = vec![cell.clone(); size];
167        Buffer { area, content }
168    }
169
170    /// Returns a Buffer containing the given lines
171    pub fn with_lines<S>(lines: Vec<S>) -> Buffer
172    where
173        S: AsRef<str>,
174    {
175        let height = lines.len() as u16;
176        let width = lines.iter().fold(0, |acc, item| {
177            std::cmp::max(acc, item.as_ref().width() as u16)
178        });
179        let mut buffer = Buffer::empty(Rect {
180            x: 0,
181            y: 0,
182            width,
183            height,
184        });
185        for (y, line) in lines.iter().enumerate() {
186            buffer.set_string(0, y as u16, line, Style::default());
187        }
188        buffer
189    }
190
191    /// Returns the content of the buffer as a slice
192    pub fn content(&self) -> &[Cell] {
193        &self.content
194    }
195
196    /// Returns the area covered by this buffer
197    pub fn area(&self) -> &Rect {
198        &self.area
199    }
200
201    /// Returns a reference to Cell at the given coordinates
202    pub fn get(&self, x: u16, y: u16) -> &Cell {
203        let i = self.index_of(x, y);
204        &self.content[i]
205    }
206
207    /// Returns a mutable reference to Cell at the given coordinates
208    pub fn get_mut(&mut self, x: u16, y: u16) -> &mut Cell {
209        let i = self.index_of(x, y);
210        &mut self.content[i]
211    }
212
213    /// Returns the index in the Vec<Cell> for the given global (x, y) coordinates.
214    ///
215    /// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
216    ///
217    /// # Examples
218    ///
219    /// ```
220    /// # use rumatui_tui::buffer::Buffer;
221    /// # use rumatui_tui::layout::Rect;
222    /// let rect = Rect::new(200, 100, 10, 10);
223    /// let buffer = Buffer::empty(rect);
224    /// // Global coordinates to the top corner of this buffer's area
225    /// assert_eq!(buffer.index_of(200, 100), 0);
226    /// ```
227    ///
228    /// # Panics
229    ///
230    /// Panics when given an coordinate that is outside of this Buffer's area.
231    ///
232    /// ```should_panic
233    /// # use rumatui_tui::buffer::Buffer;
234    /// # use rumatui_tui::layout::Rect;
235    /// let rect = Rect::new(200, 100, 10, 10);
236    /// let buffer = Buffer::empty(rect);
237    /// // Top coordinate is outside of the buffer in global coordinate space, as the Buffer's area
238    /// // starts at (200, 100).
239    /// buffer.index_of(0, 0); // Panics
240    /// ```
241    pub fn index_of(&self, x: u16, y: u16) -> usize {
242        debug_assert!(
243            x >= self.area.left()
244                && x < self.area.right()
245                && y >= self.area.top()
246                && y < self.area.bottom(),
247            "Trying to access position outside the buffer: x={}, y={}, area={:?}",
248            x,
249            y,
250            self.area
251        );
252        ((y - self.area.y) * self.area.width + (x - self.area.x)) as usize
253    }
254
255    /// Returns the (global) coordinates of a cell given its index
256    ///
257    /// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
258    ///
259    /// # Examples
260    ///
261    /// ```
262    /// # use rumatui_tui::buffer::Buffer;
263    /// # use rumatui_tui::layout::Rect;
264    /// let rect = Rect::new(200, 100, 10, 10);
265    /// let buffer = Buffer::empty(rect);
266    /// assert_eq!(buffer.pos_of(0), (200, 100));
267    /// assert_eq!(buffer.pos_of(14), (204, 101));
268    /// ```
269    ///
270    /// # Panics
271    ///
272    /// Panics when given an index that is outside the Buffer's content.
273    ///
274    /// ```should_panic
275    /// # use rumatui_tui::buffer::Buffer;
276    /// # use rumatui_tui::layout::Rect;
277    /// let rect = Rect::new(0, 0, 10, 10); // 100 cells in total
278    /// let buffer = Buffer::empty(rect);
279    /// // Index 100 is the 101th cell, which lies outside of the area of this Buffer.
280    /// buffer.pos_of(100); // Panics
281    /// ```
282    pub fn pos_of(&self, i: usize) -> (u16, u16) {
283        debug_assert!(
284            i < self.content.len(),
285            "Trying to get the coords of a cell outside the buffer: i={} len={}",
286            i,
287            self.content.len()
288        );
289        (
290            self.area.x + i as u16 % self.area.width,
291            self.area.y + i as u16 / self.area.width,
292        )
293    }
294
295    /// Print a string, starting at the position (x, y)
296    pub fn set_string<S>(&mut self, x: u16, y: u16, string: S, style: Style)
297    where
298        S: AsRef<str>,
299    {
300        self.set_stringn(x, y, string, usize::MAX, style);
301    }
302
303    /// Print at most the first n characters of a string if enough space is available
304    /// until the end of the line
305    pub fn set_stringn<S>(
306        &mut self,
307        x: u16,
308        y: u16,
309        string: S,
310        width: usize,
311        style: Style,
312    ) -> (u16, u16)
313    where
314        S: AsRef<str>,
315    {
316        let mut index = self.index_of(x, y);
317        let mut x_offset = x as usize;
318        let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true);
319        let max_offset = min(self.area.right() as usize, width.saturating_add(x as usize));
320        for s in graphemes {
321            let width = s.width();
322            // `x_offset + width > max_offset` could be integer overflow on 32-bit machines if we
323            // change dimenstions to usize or u32 and someone resizes the terminal to 1x2^32.
324            if width > max_offset.saturating_sub(x_offset) {
325                break;
326            }
327
328            self.content[index].set_symbol(s);
329            self.content[index].set_style(style);
330            // Reset following cells if multi-width (they would be hidden by the grapheme),
331            for i in index + 1..index + width {
332                self.content[i].reset();
333            }
334            index += width;
335            x_offset += width;
336        }
337        (x_offset as u16, y)
338    }
339
340    pub fn set_background(&mut self, area: Rect, color: Color) {
341        for y in area.top()..area.bottom() {
342            for x in area.left()..area.right() {
343                self.get_mut(x, y).set_bg(color);
344            }
345        }
346    }
347
348    /// Resize the buffer so that the mapped area matches the given area and that the buffer
349    /// length is equal to area.width * area.height
350    pub fn resize(&mut self, area: Rect) {
351        let length = area.area() as usize;
352        if self.content.len() > length {
353            self.content.truncate(length);
354        } else {
355            self.content.resize(length, Default::default());
356        }
357        self.area = area;
358    }
359
360    /// Reset all cells in the buffer
361    pub fn reset(&mut self) {
362        for c in &mut self.content {
363            c.reset();
364        }
365    }
366
367    /// Merge an other buffer into this one
368    pub fn merge(&mut self, other: &Buffer) {
369        let area = self.area.union(other.area);
370        let cell: Cell = Default::default();
371        self.content.resize(area.area() as usize, cell.clone());
372
373        // Move original content to the appropriate space
374        let size = self.area.area() as usize;
375        for i in (0..size).rev() {
376            let (x, y) = self.pos_of(i);
377            // New index in content
378            let k = ((y - area.y) * area.width + x - area.x) as usize;
379            if i != k {
380                self.content[k] = self.content[i].clone();
381                self.content[i] = cell.clone();
382            }
383        }
384
385        // Push content of the other buffer into this one (may erase previous
386        // data)
387        let size = other.area.area() as usize;
388        for i in 0..size {
389            let (x, y) = other.pos_of(i);
390            // New index in content
391            let k = ((y - area.y) * area.width + x - area.x) as usize;
392            self.content[k] = other.content[i].clone();
393        }
394        self.area = area;
395    }
396
397    /// Builds a minimal sequence of coordinates and Cells necessary to update the UI from
398    /// self to other.
399    ///
400    /// We're assuming that buffers are well-formed, that is no double-width cell is followed by
401    /// a non-blank cell.
402    ///
403    /// # Multi-width characters handling:
404    ///
405    /// ```text
406    /// (Index:) `01`
407    /// Prev:    `コ`
408    /// Next:    `aa`
409    /// Updates: `0: a, 1: a'
410    /// ```
411    ///
412    /// ```text
413    /// (Index:) `01`
414    /// Prev:    `a `
415    /// Next:    `コ`
416    /// Updates: `0: コ` (double width symbol at index 0 - skip index 1)
417    /// ```
418    ///
419    /// ```text
420    /// (Index:) `012`
421    /// Prev:    `aaa`
422    /// Next:    `aコ`
423    /// Updates: `0: a, 1: コ` (double width symbol at index 1 - skip index 2)
424    /// ```
425    pub fn diff<'a>(&self, other: &'a Buffer) -> Vec<(u16, u16, &'a Cell)> {
426        let previous_buffer = &self.content;
427        let next_buffer = &other.content;
428        let width = self.area.width;
429
430        let mut updates: Vec<(u16, u16, &Cell)> = vec![];
431        // Cells invalidated by drawing/replacing preceeding multi-width characters:
432        let mut invalidated: usize = 0;
433        // Cells from the current buffer to skip due to preceeding multi-width characters taking their
434        // place (the skipped cells should be blank anyway):
435        let mut to_skip: usize = 0;
436        for (i, (current, previous)) in next_buffer.iter().zip(previous_buffer.iter()).enumerate() {
437            if (current != previous || invalidated > 0) && to_skip == 0 {
438                let x = i as u16 % width;
439                let y = i as u16 / width;
440                updates.push((x, y, &next_buffer[i]));
441            }
442
443            to_skip = current.symbol.width().saturating_sub(1);
444
445            let affected_width = std::cmp::max(current.symbol.width(), previous.symbol.width());
446            invalidated = std::cmp::max(affected_width, invalidated).saturating_sub(1);
447        }
448        updates
449    }
450}
451
452#[cfg(test)]
453mod tests {
454    use super::*;
455
456    fn cell(s: &str) -> Cell {
457        let mut cell = Cell::default();
458        cell.set_symbol(s);
459        cell
460    }
461
462    #[test]
463    fn it_translates_to_and_from_coordinates() {
464        let rect = Rect::new(200, 100, 50, 80);
465        let buf = Buffer::empty(rect);
466
467        // First cell is at the upper left corner.
468        assert_eq!(buf.pos_of(0), (200, 100));
469        assert_eq!(buf.index_of(200, 100), 0);
470
471        // Last cell is in the lower right.
472        assert_eq!(buf.pos_of(buf.content.len() - 1), (249, 179));
473        assert_eq!(buf.index_of(249, 179), buf.content.len() - 1);
474    }
475
476    #[test]
477    #[should_panic(expected = "outside the buffer")]
478    fn pos_of_panics_on_out_of_bounds() {
479        let rect = Rect::new(0, 0, 10, 10);
480        let buf = Buffer::empty(rect);
481
482        // There are a total of 100 cells; zero-indexed means that 100 would be the 101st cell.
483        buf.pos_of(100);
484    }
485
486    #[test]
487    #[should_panic(expected = "outside the buffer")]
488    fn index_of_panics_on_out_of_bounds() {
489        let rect = Rect::new(0, 0, 10, 10);
490        let buf = Buffer::empty(rect);
491
492        // width is 10; zero-indexed means that 10 would be the 11th cell.
493        buf.index_of(10, 0);
494    }
495
496    #[test]
497    fn buffer_set_string() {
498        let area = Rect::new(0, 0, 5, 1);
499        let mut buffer = Buffer::empty(area);
500
501        // Zero-width
502        buffer.set_stringn(0, 0, "aaa", 0, Style::default());
503        assert_eq!(buffer, Buffer::with_lines(vec!["     "]));
504
505        buffer.set_string(0, 0, "aaa", Style::default());
506        assert_eq!(buffer, Buffer::with_lines(vec!["aaa  "]));
507
508        // Width limit:
509        buffer.set_stringn(0, 0, "bbbbbbbbbbbbbb", 4, Style::default());
510        assert_eq!(buffer, Buffer::with_lines(vec!["bbbb "]));
511
512        buffer.set_string(0, 0, "12345", Style::default());
513        assert_eq!(buffer, Buffer::with_lines(vec!["12345"]));
514
515        // Width truncation:
516        buffer.set_string(0, 0, "123456", Style::default());
517        assert_eq!(buffer, Buffer::with_lines(vec!["12345"]));
518    }
519
520    #[test]
521    fn buffer_set_string_double_width() {
522        let area = Rect::new(0, 0, 5, 1);
523        let mut buffer = Buffer::empty(area);
524        buffer.set_string(0, 0, "コン", Style::default());
525        assert_eq!(buffer, Buffer::with_lines(vec!["コン "]));
526
527        // Only 1 space left.
528        buffer.set_string(0, 0, "コンピ", Style::default());
529        assert_eq!(buffer, Buffer::with_lines(vec!["コン "]));
530    }
531
532    #[test]
533    fn buffer_with_lines() {
534        let buffer =
535            Buffer::with_lines(vec!["┌────────┐", "│コンピュ│", "│ーa 上で│", "└────────┘"]);
536        assert_eq!(buffer.area.x, 0);
537        assert_eq!(buffer.area.y, 0);
538        assert_eq!(buffer.area.width, 10);
539        assert_eq!(buffer.area.height, 4);
540    }
541
542    #[test]
543    fn buffer_diffing_empty_empty() {
544        let area = Rect::new(0, 0, 40, 40);
545        let prev = Buffer::empty(area);
546        let next = Buffer::empty(area);
547        let diff = prev.diff(&next);
548        assert_eq!(diff, vec![]);
549    }
550
551    #[test]
552    fn buffer_diffing_empty_filled() {
553        let area = Rect::new(0, 0, 40, 40);
554        let prev = Buffer::empty(area);
555        let next = Buffer::filled(area, Cell::default().set_symbol("a"));
556        let diff = prev.diff(&next);
557        assert_eq!(diff.len(), 40 * 40);
558    }
559
560    #[test]
561    fn buffer_diffing_filled_filled() {
562        let area = Rect::new(0, 0, 40, 40);
563        let prev = Buffer::filled(area, Cell::default().set_symbol("a"));
564        let next = Buffer::filled(area, Cell::default().set_symbol("a"));
565        let diff = prev.diff(&next);
566        assert_eq!(diff, vec![]);
567    }
568
569    #[test]
570    fn buffer_diffing_single_width() {
571        let prev = Buffer::with_lines(vec![
572            "          ",
573            "┌Title─┐  ",
574            "│      │  ",
575            "│      │  ",
576            "└──────┘  ",
577        ]);
578        let next = Buffer::with_lines(vec![
579            "          ",
580            "┌TITLE─┐  ",
581            "│      │  ",
582            "│      │  ",
583            "└──────┘  ",
584        ]);
585        let diff = prev.diff(&next);
586        assert_eq!(
587            diff,
588            vec![
589                (2, 1, &cell("I")),
590                (3, 1, &cell("T")),
591                (4, 1, &cell("L")),
592                (5, 1, &cell("E")),
593            ]
594        );
595    }
596
597    #[test]
598    #[rustfmt::skip]
599    fn buffer_diffing_multi_width() {
600        let prev = Buffer::with_lines(vec![
601            "┌Title─┐  ",
602            "└──────┘  ",
603        ]);
604        let next = Buffer::with_lines(vec![
605            "┌称号──┐  ",
606            "└──────┘  ",
607        ]);
608        let diff = prev.diff(&next);
609        assert_eq!(
610            diff,
611            vec![
612                (1, 0, &cell("称")),
613                // Skipped "i"
614                (3, 0, &cell("号")),
615                // Skipped "l"
616                (5, 0, &cell("─")),
617            ]
618        );
619    }
620
621    #[test]
622    fn buffer_diffing_multi_width_offset() {
623        let prev = Buffer::with_lines(vec!["┌称号──┐"]);
624        let next = Buffer::with_lines(vec!["┌─称号─┐"]);
625
626        let diff = prev.diff(&next);
627        assert_eq!(
628            diff,
629            vec![(1, 0, &cell("─")), (2, 0, &cell("称")), (4, 0, &cell("号")),]
630        );
631    }
632
633    #[test]
634    fn buffer_merge() {
635        let mut one = Buffer::filled(
636            Rect {
637                x: 0,
638                y: 0,
639                width: 2,
640                height: 2,
641            },
642            Cell::default().set_symbol("1"),
643        );
644        let two = Buffer::filled(
645            Rect {
646                x: 0,
647                y: 2,
648                width: 2,
649                height: 2,
650            },
651            Cell::default().set_symbol("2"),
652        );
653        one.merge(&two);
654        assert_eq!(one, Buffer::with_lines(vec!["11", "11", "22", "22"]));
655    }
656
657    #[test]
658    fn buffer_merge2() {
659        let mut one = Buffer::filled(
660            Rect {
661                x: 2,
662                y: 2,
663                width: 2,
664                height: 2,
665            },
666            Cell::default().set_symbol("1"),
667        );
668        let two = Buffer::filled(
669            Rect {
670                x: 0,
671                y: 0,
672                width: 2,
673                height: 2,
674            },
675            Cell::default().set_symbol("2"),
676        );
677        one.merge(&two);
678        assert_eq!(
679            one,
680            Buffer::with_lines(vec!["22  ", "22  ", "  11", "  11"])
681        );
682    }
683
684    #[test]
685    fn buffer_merge3() {
686        let mut one = Buffer::filled(
687            Rect {
688                x: 3,
689                y: 3,
690                width: 2,
691                height: 2,
692            },
693            Cell::default().set_symbol("1"),
694        );
695        let two = Buffer::filled(
696            Rect {
697                x: 1,
698                y: 1,
699                width: 3,
700                height: 4,
701            },
702            Cell::default().set_symbol("2"),
703        );
704        one.merge(&two);
705        let mut merged = Buffer::with_lines(vec!["222 ", "222 ", "2221", "2221"]);
706        merged.area = Rect {
707            x: 1,
708            y: 1,
709            width: 4,
710            height: 4,
711        };
712        assert_eq!(one, merged);
713    }
714}