use std::cmp::min; use std::fmt; use std::usize; use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; use layout::Rect; use style::{Color, Modifier, Style}; /// A buffer cell #[derive(Debug, Clone, PartialEq)] pub struct Cell { pub symbol: String, pub style: Style, } 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.style.fg = color; self } pub fn set_bg(&mut self, color: Color) -> &mut Cell { self.style.bg = color; self } pub fn set_modifier(&mut self, modifier: Modifier) -> &mut Cell { self.style.modifier = modifier; self } pub fn set_style(&mut self, style: Style) -> &mut Cell { self.style = style; self } pub fn reset(&mut self) { self.symbol.clear(); self.symbol.push(' '); self.style.reset(); } } impl Default for Cell { fn default() -> Cell { Cell { symbol: " ".into(), style: Default::default(), } } } /// 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: /// /// ``` /// # extern crate tui; /// use tui::buffer::{Buffer, Cell}; /// use tui::layout::Rect; /// use tui::style::{Color, Style, Modifier}; /// /// # fn main() { /// 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"), /// style: Style { /// fg: Color::Red, /// bg: Color::White, /// modifier: Modifier::Reset /// }}); /// buf.get_mut(5, 0).set_char('x'); /// assert_eq!(buf.get(5, 0).symbol, "x"); /// # } /// ``` #[derive(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, } impl Default for Buffer { fn default() -> Buffer { Buffer { area: Default::default(), content: Vec::new(), } } } impl fmt::Debug for Buffer { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "Buffer: {:?}", self.area)?; f.write_str("Content (quoted lines):\n")?; for cells in self.content.chunks(self.area.width as usize) { let line: String = cells.iter().map(|cell| &cell.symbol[..]).collect(); f.write_fmt(format_args!("{:?},\n", line))?; } f.write_str("Style:\n")?; for cells in self.content.chunks(self.area.width as usize) { f.write_str("|")?; for cell in cells { write!( f, "{} {} {}|", cell.style.fg.code(), cell.style.bg.code(), cell.style.modifier.code() )?; } f.write_str("\n")?; } Ok(()) } } 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(lines: Vec) -> Buffer where S: AsRef, { 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, }); let mut y = 0; for line in &lines { buffer.set_string(0, y, line, Style::default()); y += 1; } 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 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(&mut self, x: u16, y: u16, string: S, style: Style) where S: AsRef, { 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(&mut self, x: u16, y: u16, string: S, limit: usize, style: Style) where S: AsRef, { let mut index = self.index_of(x, y); let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true); let max_index = min((self.area.right() - x) as usize, limit); for s in graphemes.take(max_index) { self.content[index].symbol.clear(); self.content[index].symbol.push_str(s); self.content[index].style = style; index += 1; } } /// 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 offset_x = self.area.x - area.x; let offset_y = self.area.y - area.y; 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 + offset_y) * area.width + (x + offset_x)) as usize; self.content[k] = self.content[i].clone(); if i != k { self.content[i] = cell.clone(); } } // Push content of the other buffer into this one (may erase previous // data) let offset_x = other.area.x - area.x; let offset_y = other.area.y - area.y; 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 + offset_y) * area.width + (x + offset_x)) as usize; self.content[k] = other.content[i].clone(); } self.area = area; } } #[cfg(test)] mod tests { use super::*; #[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); } }