You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
helix-plus/helix-tui/src/buffer.rs

844 lines
26 KiB
Rust

use crate::text::{Span, Spans};
use helix_core::unicode::width::UnicodeWidthStr;
use std::cmp::min;
use unicode_segmentation::UnicodeSegmentation;
use helix_view::graphics::{Color, Modifier, Rect, Style};
/// 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_view::graphics::{Rect, Color, Style, Modifier};
///
/// let mut buf = Buffer::empty(Rect{x: 0, y: 0, width: 10, height: 5});
/// buf[(0, 2)].set_symbol("x");
/// assert_eq!(buf[(0, 2)].symbol, "x");
/// buf.set_string(3, 0, "string", Style::default().fg(Color::Red).bg(Color::White));
/// assert_eq!(buf[(5, 0)], Cell{
/// symbol: String::from("r"),
/// fg: Color::Red,
/// bg: Color::White,
/// modifier: Modifier::empty()
/// });
/// buf[(5, 0)].set_char('x');
/// assert_eq!(buf[(5, 0)].symbol, "x");
/// ```
#[derive(Debug, Default, 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 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) -> Option<&Cell> {
self.index_of_opt(x, y).map(|i| &self.content[i])
}
/// Returns a mutable reference to Cell at the given coordinates
pub fn get_mut(&mut self, x: u16, y: u16) -> Option<&mut Cell> {
self.index_of_opt(x, y).map(|i| &mut self.content[i])
}
/// Tells whether the global (x, y) coordinates are inside the Buffer's area.
///
/// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
///
/// # Examples
///
/// ```
/// # use helix_tui::buffer::Buffer;
/// # use helix_view::graphics::Rect;
/// let rect = Rect::new(200, 100, 10, 10);
/// let buffer = Buffer::empty(rect);
/// // Global coordinates inside the Buffer's area
/// assert!(buffer.in_bounds(209, 100));
/// // Global coordinates outside the Buffer's area
/// assert!(!buffer.in_bounds(210, 100));
/// ```
///
/// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
pub fn in_bounds(&self, x: u16, y: u16) -> bool {
x >= self.area.left()
&& x < self.area.right()
&& y >= self.area.top()
&& y < self.area.bottom()
}
/// 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_view::graphics::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.
pub fn index_of(&self, x: u16, y: u16) -> usize {
debug_assert!(
self.in_bounds(x, y),
"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 index in the Vec<Cell> for the given global (x, y) coordinates,
/// or `None` if the coordinates are outside the buffer's area.
fn index_of_opt(&self, x: u16, y: u16) -> Option<usize> {
if self.in_bounds(x, y) {
Some(self.index_of(x, y))
} else {
None
}
}
/// 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_view::graphics::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.
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>,
{
self.set_string_truncated(x, y, string, width, style, false, false)
}
/// Print at most the first `width` characters of a string if enough space is available
/// until the end of the line. If `ellipsis` is true appends a `…` at the end of
/// truncated lines. If `truncate_start` is `true`, truncate the beginning of the string
/// instead of the end.
#[allow(clippy::too_many_arguments)]
pub fn set_string_truncated<S>(
&mut self,
x: u16,
y: u16,
string: S,
width: usize,
style: Style,
ellipsis: bool,
truncate_start: bool,
) -> (u16, u16)
where
S: AsRef<str>,
{
// prevent panic if out of range
if !self.in_bounds(x, y) || width == 0 {
return (x, y);
}
let mut index = self.index_of(x, y);
let mut x_offset = x as usize;
let width = if ellipsis { width - 1 } else { width };
let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true);
let max_offset = min(self.area.right() as usize, width.saturating_add(x as usize));
if !truncate_start {
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;
}
if ellipsis && x_offset - (x as usize) < string.as_ref().width() {
self.content[index].set_symbol("…");
}
} else {
let mut start_index = self.index_of(x, y);
let mut index = self.index_of(max_offset as u16, y);
let total_width = string.as_ref().width();
let truncated = total_width > width;
if ellipsis && truncated {
self.content[start_index].set_symbol("…");
start_index += 1;
}
if !truncated {
index -= width - total_width;
}
for s in graphemes.rev() {
let width = s.width();
if width == 0 {
continue;
}
let start = index - width;
if start < start_index {
break;
}
self.content[start].set_symbol(s);
self.content[start].set_style(style);
for i in start + 1..index {
self.content[i].reset();
}
index -= 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[(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[(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();
}
}
/// Clear an area in the buffer
pub fn clear(&mut self, area: Rect) {
for x in area.left()..area.right() {
for y in area.top()..area.bottom() {
self[(x, y)].reset();
}
}
}
/// Clear an area in the buffer with a default style.
pub fn clear_with(&mut self, area: Rect, style: Style) {
for x in area.left()..area.right() {
for y in area.top()..area.bottom() {
let cell = &mut self[(x, y)];
cell.reset();
cell.set_style(style);
}
}
}
/// 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
}
}
impl std::ops::Index<(u16, u16)> for Buffer {
type Output = Cell;
fn index(&self, (x, y): (u16, u16)) -> &Self::Output {
let i = self.index_of(x, y);
&self.content[i]
}
}
impl std::ops::IndexMut<(u16, u16)> for Buffer {
fn index_mut(&mut self, (x, y): (u16, u16)) -> &mut Self::Output {
let i = self.index_of(x, y);
&mut self.content[i]
}
}
#[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")]
#[cfg(debug_assertions)]
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")]
#[cfg(debug_assertions)]
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);
}
}