aboutsummaryrefslogtreecommitdiff
path: root/helix-tui/src/buffer.rs
diff options
context:
space:
mode:
Diffstat (limited to 'helix-tui/src/buffer.rs')
-rw-r--r--helix-tui/src/buffer.rs745
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);
+ }
+}