zui_widgets/
buffer.rs

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