| use crate::{ |
| layout::Rect, |
| style::{Color, Modifier, Style}, |
| text::{Span, Spans}, |
| }; |
| use std::cmp::min; |
| use unicode_segmentation::UnicodeSegmentation; |
| use unicode_width::UnicodeWidthStr; |
| |
| /// A buffer cell |
| #[derive(Debug, Clone, PartialEq)] |
| pub struct Cell { |
| pub symbol: String, |
| pub fg: Color, |
| pub bg: Color, |
| pub modifier: Modifier, |
| } |
| |
| impl Cell { |
| pub fn set_symbol(&mut self, symbol: &str) -> &mut Cell { |
| self.symbol.clear(); |
| self.symbol.push_str(symbol); |
| self |
| } |
| |
| pub fn set_char(&mut self, ch: char) -> &mut Cell { |
| self.symbol.clear(); |
| self.symbol.push(ch); |
| self |
| } |
| |
| pub fn set_fg(&mut self, color: Color) -> &mut Cell { |
| self.fg = color; |
| self |
| } |
| |
| pub fn set_bg(&mut self, color: Color) -> &mut Cell { |
| self.bg = color; |
| self |
| } |
| |
| pub fn set_style(&mut self, style: Style) -> &mut Cell { |
| if let Some(c) = style.fg { |
| self.fg = c; |
| } |
| if let Some(c) = style.bg { |
| self.bg = c; |
| } |
| self.modifier.insert(style.add_modifier); |
| self.modifier.remove(style.sub_modifier); |
| self |
| } |
| |
| pub fn reset(&mut self) { |
| self.symbol.clear(); |
| self.symbol.push(' '); |
| self.fg = Color::Reset; |
| self.bg = Color::Reset; |
| self.modifier = Modifier::empty(); |
| } |
| } |
| |
| impl Default for Cell { |
| fn default() -> Cell { |
| Cell { |
| symbol: " ".into(), |
| fg: Color::Reset, |
| bg: Color::Reset, |
| modifier: Modifier::empty(), |
| } |
| } |
| } |
| |
| /// A buffer that maps to the desired content of the terminal after the draw call |
| /// |
| /// No widget in the library interacts directly with the terminal. Instead each of them is required |
| /// to draw their state to an intermediate buffer. It is basically a grid where each cell contains |
| /// a grapheme, a foreground color and a background color. This grid will then be used to output |
| /// the appropriate escape sequences and characters to draw the UI as the user has defined it. |
| /// |
| /// # Examples: |
| /// |
| /// ``` |
| /// use tui::buffer::{Buffer, Cell}; |
| /// use tui::layout::Rect; |
| /// use tui::style::{Color, Style, Modifier}; |
| /// |
| /// let mut buf = Buffer::empty(Rect{x: 0, y: 0, width: 10, height: 5}); |
| /// buf.get_mut(0, 2).set_symbol("x"); |
| /// assert_eq!(buf.get(0, 2).symbol, "x"); |
| /// buf.set_string(3, 0, "string", Style::default().fg(Color::Red).bg(Color::White)); |
| /// assert_eq!(buf.get(5, 0), &Cell{ |
| /// symbol: String::from("r"), |
| /// fg: Color::Red, |
| /// bg: Color::White, |
| /// modifier: Modifier::empty() |
| /// }); |
| /// buf.get_mut(5, 0).set_char('x'); |
| /// assert_eq!(buf.get(5, 0).symbol, "x"); |
| /// ``` |
| #[derive(Debug, Clone, PartialEq)] |
| pub struct Buffer { |
| /// The area represented by this buffer |
| pub area: Rect, |
| /// The content of the buffer. The length of this Vec should always be equal to area.width * |
| /// area.height |
| pub content: Vec<Cell>, |
| } |
| |
| impl Default for Buffer { |
| fn default() -> Buffer { |
| Buffer { |
| area: Default::default(), |
| content: Vec::new(), |
| } |
| } |
| } |
| |
| impl Buffer { |
| /// Returns a Buffer with all cells set to the default one |
| pub fn empty(area: Rect) -> Buffer { |
| let cell: Cell = Default::default(); |
| Buffer::filled(area, &cell) |
| } |
| |
| /// Returns a Buffer with all cells initialized with the attributes of the given Cell |
| pub fn filled(area: Rect, cell: &Cell) -> Buffer { |
| let size = area.area() as usize; |
| let mut content = Vec::with_capacity(size); |
| for _ in 0..size { |
| content.push(cell.clone()); |
| } |
| Buffer { area, content } |
| } |
| |
| /// Returns a Buffer containing the given lines |
| pub fn with_lines<S>(lines: Vec<S>) -> Buffer |
| where |
| S: AsRef<str>, |
| { |
| let height = lines.len() as u16; |
| let width = lines.iter().fold(0, |acc, item| { |
| std::cmp::max(acc, item.as_ref().width() as u16) |
| }); |
| let mut buffer = Buffer::empty(Rect { |
| x: 0, |
| y: 0, |
| width, |
| height, |
| }); |
| for (y, line) in lines.iter().enumerate() { |
| buffer.set_string(0, y as u16, line, Style::default()); |
| } |
| buffer |
| } |
| |
| /// Returns the content of the buffer as a slice |
| pub fn content(&self) -> &[Cell] { |
| &self.content |
| } |
| |
| /// Returns the area covered by this buffer |
| pub fn area(&self) -> &Rect { |
| &self.area |
| } |
| |
| /// Returns a reference to Cell at the given coordinates |
| pub fn get(&self, x: u16, y: u16) -> &Cell { |
| let i = self.index_of(x, y); |
| &self.content[i] |
| } |
| |
| /// Returns a mutable reference to Cell at the given coordinates |
| pub fn get_mut(&mut self, x: u16, y: u16) -> &mut Cell { |
| let i = self.index_of(x, y); |
| &mut self.content[i] |
| } |
| |
| /// Returns the index in the Vec<Cell> for the given global (x, y) coordinates. |
| /// |
| /// Global coordinates are offset by the Buffer's area offset (`x`/`y`). |
| /// |
| /// # Examples |
| /// |
| /// ``` |
| /// # use tui::buffer::Buffer; |
| /// # use tui::layout::Rect; |
| /// let rect = Rect::new(200, 100, 10, 10); |
| /// let buffer = Buffer::empty(rect); |
| /// // Global coordinates to the top corner of this buffer's area |
| /// assert_eq!(buffer.index_of(200, 100), 0); |
| /// ``` |
| /// |
| /// # Panics |
| /// |
| /// Panics when given an coordinate that is outside of this Buffer's area. |
| /// |
| /// ```should_panic |
| /// # use tui::buffer::Buffer; |
| /// # use tui::layout::Rect; |
| /// let rect = Rect::new(200, 100, 10, 10); |
| /// let buffer = Buffer::empty(rect); |
| /// // Top coordinate is outside of the buffer in global coordinate space, as the Buffer's area |
| /// // starts at (200, 100). |
| /// buffer.index_of(0, 0); // Panics |
| /// ``` |
| pub fn index_of(&self, x: u16, y: u16) -> usize { |
| debug_assert!( |
| x >= self.area.left() |
| && x < self.area.right() |
| && y >= self.area.top() |
| && y < self.area.bottom(), |
| "Trying to access position outside the buffer: x={}, y={}, area={:?}", |
| x, |
| y, |
| self.area |
| ); |
| ((y - self.area.y) * self.area.width + (x - self.area.x)) as usize |
| } |
| |
| /// Returns the (global) coordinates of a cell given its index |
| /// |
| /// Global coordinates are offset by the Buffer's area offset (`x`/`y`). |
| /// |
| /// # Examples |
| /// |
| /// ``` |
| /// # use tui::buffer::Buffer; |
| /// # use tui::layout::Rect; |
| /// let rect = Rect::new(200, 100, 10, 10); |
| /// let buffer = Buffer::empty(rect); |
| /// assert_eq!(buffer.pos_of(0), (200, 100)); |
| /// assert_eq!(buffer.pos_of(14), (204, 101)); |
| /// ``` |
| /// |
| /// # Panics |
| /// |
| /// Panics when given an index that is outside the Buffer's content. |
| /// |
| /// ```should_panic |
| /// # use tui::buffer::Buffer; |
| /// # use tui::layout::Rect; |
| /// let rect = Rect::new(0, 0, 10, 10); // 100 cells in total |
| /// let buffer = Buffer::empty(rect); |
| /// // Index 100 is the 101th cell, which lies outside of the area of this Buffer. |
| /// buffer.pos_of(100); // Panics |
| /// ``` |
| pub fn pos_of(&self, i: usize) -> (u16, u16) { |
| debug_assert!( |
| i < self.content.len(), |
| "Trying to get the coords of a cell outside the buffer: i={} len={}", |
| i, |
| self.content.len() |
| ); |
| ( |
| self.area.x + i as u16 % self.area.width, |
| self.area.y + i as u16 / self.area.width, |
| ) |
| } |
| |
| /// Print a string, starting at the position (x, y) |
| pub fn set_string<S>(&mut self, x: u16, y: u16, string: S, style: Style) |
| where |
| S: AsRef<str>, |
| { |
| self.set_stringn(x, y, string, usize::MAX, style); |
| } |
| |
| /// Print at most the first n characters of a string if enough space is available |
| /// until the end of the line |
| pub fn set_stringn<S>( |
| &mut self, |
| x: u16, |
| y: u16, |
| string: S, |
| width: usize, |
| style: Style, |
| ) -> (u16, u16) |
| where |
| S: AsRef<str>, |
| { |
| let mut index = self.index_of(x, y); |
| let mut x_offset = x as usize; |
| let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true); |
| let max_offset = min(self.area.right() as usize, width.saturating_add(x as usize)); |
| for s in graphemes { |
| let width = s.width(); |
| if width == 0 { |
| continue; |
| } |
| // `x_offset + width > max_offset` could be integer overflow on 32-bit machines if we |
| // change dimenstions to usize or u32 and someone resizes the terminal to 1x2^32. |
| if width > max_offset.saturating_sub(x_offset) { |
| break; |
| } |
| |
| self.content[index].set_symbol(s); |
| self.content[index].set_style(style); |
| // Reset following cells if multi-width (they would be hidden by the grapheme), |
| for i in index + 1..index + width { |
| self.content[i].reset(); |
| } |
| index += width; |
| x_offset += width; |
| } |
| (x_offset as u16, y) |
| } |
| |
| pub fn set_spans<'a>(&mut self, x: u16, y: u16, spans: &Spans<'a>, width: u16) -> (u16, u16) { |
| let mut remaining_width = width; |
| let mut x = x; |
| for span in &spans.0 { |
| if remaining_width == 0 { |
| break; |
| } |
| let pos = self.set_stringn( |
| x, |
| y, |
| span.content.as_ref(), |
| remaining_width as usize, |
| span.style, |
| ); |
| let w = pos.0.saturating_sub(x); |
| x = pos.0; |
| remaining_width = remaining_width.saturating_sub(w); |
| } |
| (x, y) |
| } |
| |
| pub fn set_span<'a>(&mut self, x: u16, y: u16, span: &Span<'a>, width: u16) -> (u16, u16) { |
| self.set_stringn(x, y, span.content.as_ref(), width as usize, span.style) |
| } |
| |
| #[deprecated( |
| since = "0.10.0", |
| note = "You should use styling capabilities of `Buffer::set_style`" |
| )] |
| pub fn set_background(&mut self, area: Rect, color: Color) { |
| for y in area.top()..area.bottom() { |
| for x in area.left()..area.right() { |
| self.get_mut(x, y).set_bg(color); |
| } |
| } |
| } |
| |
| pub fn set_style(&mut self, area: Rect, style: Style) { |
| for y in area.top()..area.bottom() { |
| for x in area.left()..area.right() { |
| self.get_mut(x, y).set_style(style); |
| } |
| } |
| } |
| |
| /// Resize the buffer so that the mapped area matches the given area and that the buffer |
| /// length is equal to area.width * area.height |
| pub fn resize(&mut self, area: Rect) { |
| let length = area.area() as usize; |
| if self.content.len() > length { |
| self.content.truncate(length); |
| } else { |
| self.content.resize(length, Default::default()); |
| } |
| self.area = area; |
| } |
| |
| /// Reset all cells in the buffer |
| pub fn reset(&mut self) { |
| for c in &mut self.content { |
| c.reset(); |
| } |
| } |
| |
| /// Merge an other buffer into this one |
| pub fn merge(&mut self, other: &Buffer) { |
| let area = self.area.union(other.area); |
| let cell: Cell = Default::default(); |
| self.content.resize(area.area() as usize, cell.clone()); |
| |
| // Move original content to the appropriate space |
| let size = self.area.area() as usize; |
| for i in (0..size).rev() { |
| let (x, y) = self.pos_of(i); |
| // New index in content |
| let k = ((y - area.y) * area.width + x - area.x) as usize; |
| if i != k { |
| self.content[k] = self.content[i].clone(); |
| self.content[i] = cell.clone(); |
| } |
| } |
| |
| // Push content of the other buffer into this one (may erase previous |
| // data) |
| let size = other.area.area() as usize; |
| for i in 0..size { |
| let (x, y) = other.pos_of(i); |
| // New index in content |
| let k = ((y - area.y) * area.width + x - area.x) as usize; |
| self.content[k] = other.content[i].clone(); |
| } |
| self.area = area; |
| } |
| |
| /// Builds a minimal sequence of coordinates and Cells necessary to update the UI from |
| /// self to other. |
| /// |
| /// We're assuming that buffers are well-formed, that is no double-width cell is followed by |
| /// a non-blank cell. |
| /// |
| /// # Multi-width characters handling: |
| /// |
| /// ```text |
| /// (Index:) `01` |
| /// Prev: `コ` |
| /// Next: `aa` |
| /// Updates: `0: a, 1: a' |
| /// ``` |
| /// |
| /// ```text |
| /// (Index:) `01` |
| /// Prev: `a ` |
| /// Next: `コ` |
| /// Updates: `0: コ` (double width symbol at index 0 - skip index 1) |
| /// ``` |
| /// |
| /// ```text |
| /// (Index:) `012` |
| /// Prev: `aaa` |
| /// Next: `aコ` |
| /// Updates: `0: a, 1: コ` (double width symbol at index 1 - skip index 2) |
| /// ``` |
| pub fn diff<'a>(&self, other: &'a Buffer) -> Vec<(u16, u16, &'a Cell)> { |
| let previous_buffer = &self.content; |
| let next_buffer = &other.content; |
| let width = self.area.width; |
| |
| let mut updates: Vec<(u16, u16, &Cell)> = vec![]; |
| // Cells invalidated by drawing/replacing preceeding multi-width characters: |
| let mut invalidated: usize = 0; |
| // Cells from the current buffer to skip due to preceeding multi-width characters taking their |
| // place (the skipped cells should be blank anyway): |
| let mut to_skip: usize = 0; |
| for (i, (current, previous)) in next_buffer.iter().zip(previous_buffer.iter()).enumerate() { |
| if (current != previous || invalidated > 0) && to_skip == 0 { |
| let x = i as u16 % width; |
| let y = i as u16 / width; |
| updates.push((x, y, &next_buffer[i])); |
| } |
| |
| to_skip = current.symbol.width().saturating_sub(1); |
| |
| let affected_width = std::cmp::max(current.symbol.width(), previous.symbol.width()); |
| invalidated = std::cmp::max(affected_width, invalidated).saturating_sub(1); |
| } |
| updates |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| |
| fn cell(s: &str) -> Cell { |
| let mut cell = Cell::default(); |
| cell.set_symbol(s); |
| cell |
| } |
| |
| #[test] |
| fn it_translates_to_and_from_coordinates() { |
| let rect = Rect::new(200, 100, 50, 80); |
| let buf = Buffer::empty(rect); |
| |
| // First cell is at the upper left corner. |
| assert_eq!(buf.pos_of(0), (200, 100)); |
| assert_eq!(buf.index_of(200, 100), 0); |
| |
| // Last cell is in the lower right. |
| assert_eq!(buf.pos_of(buf.content.len() - 1), (249, 179)); |
| assert_eq!(buf.index_of(249, 179), buf.content.len() - 1); |
| } |
| |
| #[test] |
| #[should_panic(expected = "outside the buffer")] |
| fn pos_of_panics_on_out_of_bounds() { |
| let rect = Rect::new(0, 0, 10, 10); |
| let buf = Buffer::empty(rect); |
| |
| // There are a total of 100 cells; zero-indexed means that 100 would be the 101st cell. |
| buf.pos_of(100); |
| } |
| |
| #[test] |
| #[should_panic(expected = "outside the buffer")] |
| fn index_of_panics_on_out_of_bounds() { |
| let rect = Rect::new(0, 0, 10, 10); |
| let buf = Buffer::empty(rect); |
| |
| // width is 10; zero-indexed means that 10 would be the 11th cell. |
| buf.index_of(10, 0); |
| } |
| |
| #[test] |
| fn buffer_set_string() { |
| let area = Rect::new(0, 0, 5, 1); |
| let mut buffer = Buffer::empty(area); |
| |
| // Zero-width |
| buffer.set_stringn(0, 0, "aaa", 0, Style::default()); |
| assert_eq!(buffer, Buffer::with_lines(vec![" "])); |
| |
| buffer.set_string(0, 0, "aaa", Style::default()); |
| assert_eq!(buffer, Buffer::with_lines(vec!["aaa "])); |
| |
| // Width limit: |
| buffer.set_stringn(0, 0, "bbbbbbbbbbbbbb", 4, Style::default()); |
| assert_eq!(buffer, Buffer::with_lines(vec!["bbbb "])); |
| |
| buffer.set_string(0, 0, "12345", Style::default()); |
| assert_eq!(buffer, Buffer::with_lines(vec!["12345"])); |
| |
| // Width truncation: |
| buffer.set_string(0, 0, "123456", Style::default()); |
| assert_eq!(buffer, Buffer::with_lines(vec!["12345"])); |
| } |
| |
| #[test] |
| fn buffer_set_string_zero_width() { |
| let area = Rect::new(0, 0, 1, 1); |
| let mut buffer = Buffer::empty(area); |
| |
| // Leading grapheme with zero width |
| let s = "\u{1}a"; |
| buffer.set_stringn(0, 0, s, 1, Style::default()); |
| assert_eq!(buffer, Buffer::with_lines(vec!["a"])); |
| |
| // Trailing grapheme with zero with |
| let s = "a\u{1}"; |
| buffer.set_stringn(0, 0, s, 1, Style::default()); |
| assert_eq!(buffer, Buffer::with_lines(vec!["a"])); |
| } |
| |
| #[test] |
| fn buffer_set_string_double_width() { |
| let area = Rect::new(0, 0, 5, 1); |
| let mut buffer = Buffer::empty(area); |
| buffer.set_string(0, 0, "コン", Style::default()); |
| assert_eq!(buffer, Buffer::with_lines(vec!["コン "])); |
| |
| // Only 1 space left. |
| buffer.set_string(0, 0, "コンピ", Style::default()); |
| assert_eq!(buffer, Buffer::with_lines(vec!["コン "])); |
| } |
| |
| #[test] |
| fn buffer_with_lines() { |
| let buffer = |
| Buffer::with_lines(vec!["┌────────┐", "│コンピュ│", "│ーa 上で│", "└────────┘"]); |
| assert_eq!(buffer.area.x, 0); |
| assert_eq!(buffer.area.y, 0); |
| assert_eq!(buffer.area.width, 10); |
| assert_eq!(buffer.area.height, 4); |
| } |
| |
| #[test] |
| fn buffer_diffing_empty_empty() { |
| let area = Rect::new(0, 0, 40, 40); |
| let prev = Buffer::empty(area); |
| let next = Buffer::empty(area); |
| let diff = prev.diff(&next); |
| assert_eq!(diff, vec![]); |
| } |
| |
| #[test] |
| fn buffer_diffing_empty_filled() { |
| let area = Rect::new(0, 0, 40, 40); |
| let prev = Buffer::empty(area); |
| let next = Buffer::filled(area, Cell::default().set_symbol("a")); |
| let diff = prev.diff(&next); |
| assert_eq!(diff.len(), 40 * 40); |
| } |
| |
| #[test] |
| fn buffer_diffing_filled_filled() { |
| let area = Rect::new(0, 0, 40, 40); |
| let prev = Buffer::filled(area, Cell::default().set_symbol("a")); |
| let next = Buffer::filled(area, Cell::default().set_symbol("a")); |
| let diff = prev.diff(&next); |
| assert_eq!(diff, vec![]); |
| } |
| |
| #[test] |
| fn buffer_diffing_single_width() { |
| let prev = Buffer::with_lines(vec![ |
| " ", |
| "┌Title─┐ ", |
| "│ │ ", |
| "│ │ ", |
| "└──────┘ ", |
| ]); |
| let next = Buffer::with_lines(vec![ |
| " ", |
| "┌TITLE─┐ ", |
| "│ │ ", |
| "│ │ ", |
| "└──────┘ ", |
| ]); |
| let diff = prev.diff(&next); |
| assert_eq!( |
| diff, |
| vec![ |
| (2, 1, &cell("I")), |
| (3, 1, &cell("T")), |
| (4, 1, &cell("L")), |
| (5, 1, &cell("E")), |
| ] |
| ); |
| } |
| |
| #[test] |
| #[rustfmt::skip] |
| fn buffer_diffing_multi_width() { |
| let prev = Buffer::with_lines(vec![ |
| "┌Title─┐ ", |
| "└──────┘ ", |
| ]); |
| let next = Buffer::with_lines(vec![ |
| "┌称号──┐ ", |
| "└──────┘ ", |
| ]); |
| let diff = prev.diff(&next); |
| assert_eq!( |
| diff, |
| vec![ |
| (1, 0, &cell("称")), |
| // Skipped "i" |
| (3, 0, &cell("号")), |
| // Skipped "l" |
| (5, 0, &cell("─")), |
| ] |
| ); |
| } |
| |
| #[test] |
| fn buffer_diffing_multi_width_offset() { |
| let prev = Buffer::with_lines(vec!["┌称号──┐"]); |
| let next = Buffer::with_lines(vec!["┌─称号─┐"]); |
| |
| let diff = prev.diff(&next); |
| assert_eq!( |
| diff, |
| vec![(1, 0, &cell("─")), (2, 0, &cell("称")), (4, 0, &cell("号")),] |
| ); |
| } |
| |
| #[test] |
| fn buffer_merge() { |
| let mut one = Buffer::filled( |
| Rect { |
| x: 0, |
| y: 0, |
| width: 2, |
| height: 2, |
| }, |
| Cell::default().set_symbol("1"), |
| ); |
| let two = Buffer::filled( |
| Rect { |
| x: 0, |
| y: 2, |
| width: 2, |
| height: 2, |
| }, |
| Cell::default().set_symbol("2"), |
| ); |
| one.merge(&two); |
| assert_eq!(one, Buffer::with_lines(vec!["11", "11", "22", "22"])); |
| } |
| |
| #[test] |
| fn buffer_merge2() { |
| let mut one = Buffer::filled( |
| Rect { |
| x: 2, |
| y: 2, |
| width: 2, |
| height: 2, |
| }, |
| Cell::default().set_symbol("1"), |
| ); |
| let two = Buffer::filled( |
| Rect { |
| x: 0, |
| y: 0, |
| width: 2, |
| height: 2, |
| }, |
| Cell::default().set_symbol("2"), |
| ); |
| one.merge(&two); |
| assert_eq!( |
| one, |
| Buffer::with_lines(vec!["22 ", "22 ", " 11", " 11"]) |
| ); |
| } |
| |
| #[test] |
| fn buffer_merge3() { |
| let mut one = Buffer::filled( |
| Rect { |
| x: 3, |
| y: 3, |
| width: 2, |
| height: 2, |
| }, |
| Cell::default().set_symbol("1"), |
| ); |
| let two = Buffer::filled( |
| Rect { |
| x: 1, |
| y: 1, |
| width: 3, |
| height: 4, |
| }, |
| Cell::default().set_symbol("2"), |
| ); |
| one.merge(&two); |
| let mut merged = Buffer::with_lines(vec!["222 ", "222 ", "2221", "2221"]); |
| merged.area = Rect { |
| x: 1, |
| y: 1, |
| width: 4, |
| height: 4, |
| }; |
| assert_eq!(one, merged); |
| } |
| } |