diff options
Diffstat (limited to 'helix-tui/src/buffer.rs')
-rw-r--r-- | helix-tui/src/buffer.rs | 745 |
1 files changed, 745 insertions, 0 deletions
diff --git a/helix-tui/src/buffer.rs b/helix-tui/src/buffer.rs new file mode 100644 index 00000000..8a227ce7 --- /dev/null +++ b/helix-tui/src/buffer.rs @@ -0,0 +1,745 @@ +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 style(&self) -> Style { + Style::default() + .fg(self.fg) + .bg(self.bg) + .add_modifier(self.modifier) + } + + 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 helix_tui::buffer::{Buffer, Cell}; +/// use helix_tui::layout::Rect; +/// use helix_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() + .map(|i| i.as_ref().width() as u16) + .max() + .unwrap_or_default(); + 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 helix_tui::buffer::Buffer; + /// # use helix_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 helix_tui::buffer::Buffer; + /// # use helix_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 helix_tui::buffer::Buffer; + /// # use helix_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 helix_tui::buffer::Buffer; + /// # use helix_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); + } +} |