ratatui/src/buffer.rs

1066 lines
34 KiB
Rust
Raw Normal View History

use std::{
cmp::min,
fmt::{Debug, Formatter, Result},
};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use crate::{
layout::Rect,
style::{Color, Modifier, Style},
text::{Line, Span},
};
2016-11-03 23:59:04 +01:00
/// A buffer cell
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Cell {
#[deprecated(
since = "0.24.1",
note = "This field will be hidden at next major version. Use `Cell::symbol` method to get \
the value. Use `Cell::set_symbol` to update the field. Use `Cell::default` to \
create `Cell` instance"
)]
2016-10-26 19:19:46 +02:00
pub symbol: String,
pub fg: Color,
pub bg: Color,
#[cfg(feature = "underline-color")]
pub underline_color: Color,
pub modifier: Modifier,
feat(cell): add voluntary skipping capability for sixel (#215) > Sixel is a bitmap graphics format supported by terminals. > "Sixel mode" is entered by sending the sequence ESC+Pq. > The "String Terminator" sequence ESC+\ exits the mode. The graphics are then rendered with the top left positioned at the cursor position. It is actually possible to render sixels in ratatui with just `buf.get_mut(x, y).set_symbol("^[Pq ... ^[\")`. But any buffer covering the "image area" will overwrite the graphics. This is most likely the same buffer, even though it consists of empty characters `' '`, except for the top-left character that starts the sequence. Thus, either the buffer or cells must be specialized to avoid drawing over the graphics. This patch specializes the `Cell` with a `set_skip(bool)` method, based on James' patch: https://github.com/TurtleTheSeaHobo/tui-rs/tree/sixel-support I unsuccessfully tried specializing the `Buffer`, but as far as I can tell buffers get merged all the way "up" and thus skipping must be set on the Cells. Otherwise some kind of "skipping area" state would be required, which I think is too complicated. Having access to the buffer now it is possible to skipp all cells but the first one which can then `set_symbol(sixel)`. It is up to the user to deal with the graphics size and buffer area size. It is possible to get the terminal's font size in pixels with a syscall. An image widget for ratatui that uses this `skip` flag is available at https://github.com/benjajaja/ratatu-image. Co-authored-by: James <james@rectangle.pizza>
2023-08-25 10:20:36 +01:00
pub skip: bool,
2016-10-09 19:46:53 +02:00
}
#[allow(deprecated)] // For Cell::symbol
impl Cell {
pub fn symbol(&self) -> &str {
self.symbol.as_str()
}
2016-11-06 18:49:57 +01:00
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;
2016-11-06 18:49:57 +01:00
self
}
pub fn set_bg(&mut self, color: Color) -> &mut Cell {
self.bg = color;
2016-11-06 18:49:57 +01:00
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;
}
#[cfg(feature = "underline-color")]
if let Some(c) = style.underline_color {
self.underline_color = c;
}
self.modifier.insert(style.add_modifier);
self.modifier.remove(style.sub_modifier);
2016-11-06 18:49:57 +01:00
self
}
#[cfg(feature = "underline-color")]
pub fn style(&self) -> Style {
Style::default()
.fg(self.fg)
.bg(self.bg)
.underline_color(self.underline_color)
.add_modifier(self.modifier)
}
#[cfg(not(feature = "underline-color"))]
pub fn style(&self) -> Style {
Style::default()
.fg(self.fg)
.bg(self.bg)
.add_modifier(self.modifier)
}
feat(cell): add voluntary skipping capability for sixel (#215) > Sixel is a bitmap graphics format supported by terminals. > "Sixel mode" is entered by sending the sequence ESC+Pq. > The "String Terminator" sequence ESC+\ exits the mode. The graphics are then rendered with the top left positioned at the cursor position. It is actually possible to render sixels in ratatui with just `buf.get_mut(x, y).set_symbol("^[Pq ... ^[\")`. But any buffer covering the "image area" will overwrite the graphics. This is most likely the same buffer, even though it consists of empty characters `' '`, except for the top-left character that starts the sequence. Thus, either the buffer or cells must be specialized to avoid drawing over the graphics. This patch specializes the `Cell` with a `set_skip(bool)` method, based on James' patch: https://github.com/TurtleTheSeaHobo/tui-rs/tree/sixel-support I unsuccessfully tried specializing the `Buffer`, but as far as I can tell buffers get merged all the way "up" and thus skipping must be set on the Cells. Otherwise some kind of "skipping area" state would be required, which I think is too complicated. Having access to the buffer now it is possible to skipp all cells but the first one which can then `set_symbol(sixel)`. It is up to the user to deal with the graphics size and buffer area size. It is possible to get the terminal's font size in pixels with a syscall. An image widget for ratatui that uses this `skip` flag is available at https://github.com/benjajaja/ratatu-image. Co-authored-by: James <james@rectangle.pizza>
2023-08-25 10:20:36 +01:00
/// Sets the cell to be skipped when copying (diffing) the buffer to the screen.
///
/// This is helpful when it is necessary to prevent the buffer from overwriting a cell that is
/// covered by an image from some terminal graphics protocol (Sixel / iTerm / Kitty ...).
pub fn set_skip(&mut self, skip: bool) -> &mut Cell {
self.skip = skip;
self
}
pub fn reset(&mut self) {
2016-10-26 19:19:46 +02:00
self.symbol.clear();
self.symbol.push(' ');
self.fg = Color::Reset;
self.bg = Color::Reset;
#[cfg(feature = "underline-color")]
{
self.underline_color = Color::Reset;
}
self.modifier = Modifier::empty();
feat(cell): add voluntary skipping capability for sixel (#215) > Sixel is a bitmap graphics format supported by terminals. > "Sixel mode" is entered by sending the sequence ESC+Pq. > The "String Terminator" sequence ESC+\ exits the mode. The graphics are then rendered with the top left positioned at the cursor position. It is actually possible to render sixels in ratatui with just `buf.get_mut(x, y).set_symbol("^[Pq ... ^[\")`. But any buffer covering the "image area" will overwrite the graphics. This is most likely the same buffer, even though it consists of empty characters `' '`, except for the top-left character that starts the sequence. Thus, either the buffer or cells must be specialized to avoid drawing over the graphics. This patch specializes the `Cell` with a `set_skip(bool)` method, based on James' patch: https://github.com/TurtleTheSeaHobo/tui-rs/tree/sixel-support I unsuccessfully tried specializing the `Buffer`, but as far as I can tell buffers get merged all the way "up" and thus skipping must be set on the Cells. Otherwise some kind of "skipping area" state would be required, which I think is too complicated. Having access to the buffer now it is possible to skipp all cells but the first one which can then `set_symbol(sixel)`. It is up to the user to deal with the graphics size and buffer area size. It is possible to get the terminal's font size in pixels with a syscall. An image widget for ratatui that uses this `skip` flag is available at https://github.com/benjajaja/ratatu-image. Co-authored-by: James <james@rectangle.pizza>
2023-08-25 10:20:36 +01:00
self.skip = false;
}
}
impl Default for Cell {
fn default() -> Cell {
#[allow(deprecated)] // For Cell::symbol
2016-10-09 19:46:53 +02:00
Cell {
symbol: " ".into(),
fg: Color::Reset,
bg: Color::Reset,
#[cfg(feature = "underline-color")]
underline_color: Color::Reset,
modifier: Modifier::empty(),
feat(cell): add voluntary skipping capability for sixel (#215) > Sixel is a bitmap graphics format supported by terminals. > "Sixel mode" is entered by sending the sequence ESC+Pq. > The "String Terminator" sequence ESC+\ exits the mode. The graphics are then rendered with the top left positioned at the cursor position. It is actually possible to render sixels in ratatui with just `buf.get_mut(x, y).set_symbol("^[Pq ... ^[\")`. But any buffer covering the "image area" will overwrite the graphics. This is most likely the same buffer, even though it consists of empty characters `' '`, except for the top-left character that starts the sequence. Thus, either the buffer or cells must be specialized to avoid drawing over the graphics. This patch specializes the `Cell` with a `set_skip(bool)` method, based on James' patch: https://github.com/TurtleTheSeaHobo/tui-rs/tree/sixel-support I unsuccessfully tried specializing the `Buffer`, but as far as I can tell buffers get merged all the way "up" and thus skipping must be set on the Cells. Otherwise some kind of "skipping area" state would be required, which I think is too complicated. Having access to the buffer now it is possible to skipp all cells but the first one which can then `set_symbol(sixel)`. It is up to the user to deal with the graphics size and buffer area size. It is possible to get the terminal's font size in pixels with a syscall. An image widget for ratatui that uses this `skip` flag is available at https://github.com/benjajaja/ratatu-image. Co-authored-by: James <james@rectangle.pizza>
2023-08-25 10:20:36 +01:00
skip: false,
2016-10-09 19:46:53 +02:00
}
}
}
2016-11-03 23:59:04 +01:00
/// 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 ratatui::{prelude::*, buffer::Cell};
2016-11-03 23:59:04 +01:00
///
/// let mut buf = Buffer::empty(Rect{x: 0, y: 0, width: 10, height: 5});
2016-11-06 18:49:57 +01:00
/// 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));
/// let cell = buf.get_mut(5, 0);
/// assert_eq!(cell.symbol(), "r");
/// assert_eq!(cell.fg, Color::Red);
/// assert_eq!(cell.bg, Color::White);
///
2016-11-06 18:49:57 +01:00
/// buf.get_mut(5, 0).set_char('x');
/// assert_eq!(buf.get(5, 0).symbol(), "x");
2016-11-03 23:59:04 +01:00
/// ```
#[derive(Default, Clone, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Buffer {
2016-11-03 23:59:04 +01:00
/// The area represented by this buffer
pub area: Rect,
2016-11-03 23:59:04 +01:00
/// The content of the buffer. The length of this Vec should always be equal to area.width *
/// area.height
pub content: Vec<Cell>,
2016-10-09 19:46:53 +02:00
}
impl Buffer {
2016-11-03 23:59:04 +01:00
/// Returns a Buffer with all cells set to the default one
pub fn empty(area: Rect) -> Buffer {
let cell = Cell::default();
2017-12-26 17:09:04 +01:00
Buffer::filled(area, &cell)
2016-10-09 19:46:53 +02:00
}
2016-11-03 23:59:04 +01:00
/// Returns a Buffer with all cells initialized with the attributes of the given Cell
2017-12-26 17:09:04 +01:00
pub fn filled(area: Rect, cell: &Cell) -> Buffer {
2016-10-09 19:46:53 +02:00
let size = area.area() as usize;
let mut content = Vec::with_capacity(size);
for _ in 0..size {
content.push(cell.clone());
}
2018-08-13 00:27:56 +02:00
Buffer { area, content }
2016-10-09 19:46:53 +02:00
}
2018-11-04 20:16:10 +01:00
/// Returns a Buffer containing the given lines
pub fn with_lines<'a, S>(lines: Vec<S>) -> Buffer
2018-11-04 20:16:10 +01:00
where
S: Into<Line<'a>>,
2018-11-04 20:16:10 +01:00
{
let lines = lines.into_iter().map(Into::into).collect::<Vec<_>>();
2018-11-04 20:16:10 +01:00
let height = lines.len() as u16;
let width = lines.iter().map(Line::width).max().unwrap_or_default() as u16;
let mut buffer = Buffer::empty(Rect::new(0, 0, width, height));
2019-02-03 22:07:58 +01:00
for (y, line) in lines.iter().enumerate() {
buffer.set_line(0, y as u16, line, width);
2018-11-04 20:16:10 +01:00
}
buffer
}
2016-11-03 23:59:04 +01:00
/// Returns the content of the buffer as a slice
pub fn content(&self) -> &[Cell] {
2016-10-09 19:46:53 +02:00
&self.content
}
2016-11-03 23:59:04 +01:00
/// Returns the area covered by this buffer
2016-10-09 19:46:53 +02:00
pub fn area(&self) -> &Rect {
&self.area
}
2016-11-03 23:59:04 +01:00
/// Returns a reference to Cell at the given coordinates
2016-11-06 18:49:57 +01:00
pub fn get(&self, x: u16, y: u16) -> &Cell {
let i = self.index_of(x, y);
&self.content[i]
2016-10-09 19:46:53 +02:00
}
2016-11-06 18:49:57 +01:00
/// 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 ratatui::prelude::*;
/// 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 ratatui::prelude::*;
/// 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 {
2017-09-11 07:58:37 +02:00
debug_assert!(
x >= self.area.left()
&& x < self.area.right()
&& y >= self.area.top()
2017-10-30 22:28:18 +01:00
&& y < self.area.bottom(),
2023-05-21 23:46:02 -04:00
"Trying to access position outside the buffer: x={x}, y={y}, area={:?}",
2017-09-11 07:58:37 +02:00
self.area
);
2016-11-07 22:57:13 +01:00
((y - self.area.y) * self.area.width + (x - self.area.x)) as usize
2016-10-09 19:46:53 +02:00
}
/// Returns the (global) coordinates of a cell given its index
///
/// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
///
/// # Examples
///
/// ```
/// # use ratatui::prelude::*;
/// 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 ratatui::prelude::*;
/// 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) {
2017-09-11 07:58:37 +02:00
debug_assert!(
i < self.content.len(),
2023-05-21 23:46:02 -04:00
"Trying to get the coords of a cell outside the buffer: i={i} len={}",
2017-09-11 07:58:37 +02:00
self.content.len()
);
(
self.area.x + (i as u16) % self.area.width,
self.area.y + (i as u16) / self.area.width,
2017-09-11 07:58:37 +02:00
)
2016-10-09 19:46:53 +02:00
}
2016-11-03 23:59:04 +01:00
/// 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>,
{
2016-11-06 18:49:57 +01:00
self.set_stringn(x, y, string, usize::MAX, style);
2016-10-22 12:51:41 +02:00
}
2016-11-03 23:59:04 +01:00
/// Print at most the first n characters of a string if enough space is available
/// until the end of the line
feat: add stateful widgets Most widgets can be drawn directly based on the input parameters. However, some features may require some kind of associated state to be implemented. For example, the `List` widget can highlight the item currently selected. This can be translated in an offset, which is the number of elements to skip in order to have the selected item within the viewport currently allocated to this widget. The widget can therefore only provide the following behavior: whenever the selected item is out of the viewport scroll to a predefined position (make the selected item the last viewable item or the one in the middle). Nonetheless, if the widget has access to the last computed offset then it can implement a natural scrolling experience where the last offset is reused until the selected item is out of the viewport. To allow such behavior within the widgets, this commit introduces the following changes: - Add a `StatefulWidget` trait with an associated `State` type. Widgets that can take advantage of having a "memory" between two draw calls needs to implement this trait. - Add a `render_stateful_widget` method on `Frame` where the associated state is given as a parameter. The chosen approach is thus to let the developers manage their widgets' states themselves as they are already responsible for the lifecycle of the wigets (given that the crate exposes an immediate mode api). The following changes were also introduced: - `Widget::render` has been deleted. Developers should use `Frame::render_widget` instead. - `Widget::background` has been deleted. Developers should use `Buffer::set_background` instead. - `SelectableList` has been deleted. Developers can directly use `List` where `SelectableList` features have been back-ported.
2019-12-15 21:38:18 +01:00
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 dimensions 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;
2016-10-09 19:46:53 +02:00
}
feat: add stateful widgets Most widgets can be drawn directly based on the input parameters. However, some features may require some kind of associated state to be implemented. For example, the `List` widget can highlight the item currently selected. This can be translated in an offset, which is the number of elements to skip in order to have the selected item within the viewport currently allocated to this widget. The widget can therefore only provide the following behavior: whenever the selected item is out of the viewport scroll to a predefined position (make the selected item the last viewable item or the one in the middle). Nonetheless, if the widget has access to the last computed offset then it can implement a natural scrolling experience where the last offset is reused until the selected item is out of the viewport. To allow such behavior within the widgets, this commit introduces the following changes: - Add a `StatefulWidget` trait with an associated `State` type. Widgets that can take advantage of having a "memory" between two draw calls needs to implement this trait. - Add a `render_stateful_widget` method on `Frame` where the associated state is given as a parameter. The chosen approach is thus to let the developers manage their widgets' states themselves as they are already responsible for the lifecycle of the wigets (given that the crate exposes an immediate mode api). The following changes were also introduced: - `Widget::render` has been deleted. Developers should use `Frame::render_widget` instead. - `Widget::background` has been deleted. Developers should use `Buffer::set_background` instead. - `SelectableList` has been deleted. Developers can directly use `List` where `SelectableList` features have been back-ported.
2019-12-15 21:38:18 +01:00
(x_offset as u16, y)
}
pub fn set_line(&mut self, x: u16, y: u16, line: &Line<'_>, width: u16) -> (u16, u16) {
let mut remaining_width = width;
let mut x = x;
for span in &line.spans {
if remaining_width == 0 {
break;
}
let pos = self.set_stringn(
x,
y,
span.content.as_ref(),
2020-05-10 15:44:30 +02:00
remaining_width as usize,
span.style,
2020-05-10 15:44:30 +02:00
);
let w = pos.0.saturating_sub(x);
x = pos.0;
remaining_width = remaining_width.saturating_sub(w);
}
(x, y)
}
pub fn set_span(&mut self, x: u16, y: u16, span: &Span<'_>, width: u16) -> (u16, u16) {
self.set_stringn(x, y, span.content.as_ref(), width as usize, span.style)
2020-05-10 15:44:30 +02:00
}
#[deprecated(
since = "0.10.0",
note = "You should use styling capabilities of `Buffer::set_style`"
)]
feat: add stateful widgets Most widgets can be drawn directly based on the input parameters. However, some features may require some kind of associated state to be implemented. For example, the `List` widget can highlight the item currently selected. This can be translated in an offset, which is the number of elements to skip in order to have the selected item within the viewport currently allocated to this widget. The widget can therefore only provide the following behavior: whenever the selected item is out of the viewport scroll to a predefined position (make the selected item the last viewable item or the one in the middle). Nonetheless, if the widget has access to the last computed offset then it can implement a natural scrolling experience where the last offset is reused until the selected item is out of the viewport. To allow such behavior within the widgets, this commit introduces the following changes: - Add a `StatefulWidget` trait with an associated `State` type. Widgets that can take advantage of having a "memory" between two draw calls needs to implement this trait. - Add a `render_stateful_widget` method on `Frame` where the associated state is given as a parameter. The chosen approach is thus to let the developers manage their widgets' states themselves as they are already responsible for the lifecycle of the wigets (given that the crate exposes an immediate mode api). The following changes were also introduced: - `Widget::render` has been deleted. Developers should use `Frame::render_widget` instead. - `Widget::background` has been deleted. Developers should use `Buffer::set_background` instead. - `SelectableList` has been deleted. Developers can directly use `List` where `SelectableList` features have been back-ported.
2019-12-15 21:38:18 +01:00
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);
}
}
2016-10-09 19:46:53 +02:00
}
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);
}
}
}
2016-11-03 23:59:04 +01:00
/// 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, Cell::default());
}
self.area = area;
}
2016-11-03 23:59:04 +01:00
/// Reset all cells in the buffer
pub fn reset(&mut self) {
for c in &mut self.content {
c.reset();
}
2016-10-14 19:44:52 +02:00
}
2016-11-03 23:59:04 +01:00
/// Merge an other buffer into this one
2017-12-26 17:09:04 +01:00
pub fn merge(&mut self, other: &Buffer) {
2018-08-13 00:27:56 +02:00
let area = self.area.union(other.area);
let cell = Cell::default();
2016-10-09 19:46:53 +02:00
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);
2016-10-09 19:46:53 +02:00
// New index in content
let k = ((y - area.y) * area.width + x - area.x) as usize;
2016-10-09 19:46:53 +02:00
if i != k {
self.content[k] = self.content[i].clone();
2016-10-09 19:46:53 +02:00
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);
2016-10-09 19:46:53 +02:00
// New index in content
let k = ((y - area.y) * area.width + x - area.x) as usize;
2016-10-09 19:46:53 +02:00
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 mut updates: Vec<(u16, u16, &Cell)> = vec![];
// Cells invalidated by drawing/replacing preceding multi-width characters:
let mut invalidated: usize = 0;
// Cells from the current buffer to skip due to preceding multi-width characters taking
feat(cell): add voluntary skipping capability for sixel (#215) > Sixel is a bitmap graphics format supported by terminals. > "Sixel mode" is entered by sending the sequence ESC+Pq. > The "String Terminator" sequence ESC+\ exits the mode. The graphics are then rendered with the top left positioned at the cursor position. It is actually possible to render sixels in ratatui with just `buf.get_mut(x, y).set_symbol("^[Pq ... ^[\")`. But any buffer covering the "image area" will overwrite the graphics. This is most likely the same buffer, even though it consists of empty characters `' '`, except for the top-left character that starts the sequence. Thus, either the buffer or cells must be specialized to avoid drawing over the graphics. This patch specializes the `Cell` with a `set_skip(bool)` method, based on James' patch: https://github.com/TurtleTheSeaHobo/tui-rs/tree/sixel-support I unsuccessfully tried specializing the `Buffer`, but as far as I can tell buffers get merged all the way "up" and thus skipping must be set on the Cells. Otherwise some kind of "skipping area" state would be required, which I think is too complicated. Having access to the buffer now it is possible to skipp all cells but the first one which can then `set_symbol(sixel)`. It is up to the user to deal with the graphics size and buffer area size. It is possible to get the terminal's font size in pixels with a syscall. An image widget for ratatui that uses this `skip` flag is available at https://github.com/benjajaja/ratatu-image. Co-authored-by: James <james@rectangle.pizza>
2023-08-25 10:20:36 +01:00
// their place (the skipped cells should be blank anyway), or due to per-cell-skipping:
let mut to_skip: usize = 0;
for (i, (current, previous)) in next_buffer.iter().zip(previous_buffer.iter()).enumerate() {
feat(cell): add voluntary skipping capability for sixel (#215) > Sixel is a bitmap graphics format supported by terminals. > "Sixel mode" is entered by sending the sequence ESC+Pq. > The "String Terminator" sequence ESC+\ exits the mode. The graphics are then rendered with the top left positioned at the cursor position. It is actually possible to render sixels in ratatui with just `buf.get_mut(x, y).set_symbol("^[Pq ... ^[\")`. But any buffer covering the "image area" will overwrite the graphics. This is most likely the same buffer, even though it consists of empty characters `' '`, except for the top-left character that starts the sequence. Thus, either the buffer or cells must be specialized to avoid drawing over the graphics. This patch specializes the `Cell` with a `set_skip(bool)` method, based on James' patch: https://github.com/TurtleTheSeaHobo/tui-rs/tree/sixel-support I unsuccessfully tried specializing the `Buffer`, but as far as I can tell buffers get merged all the way "up" and thus skipping must be set on the Cells. Otherwise some kind of "skipping area" state would be required, which I think is too complicated. Having access to the buffer now it is possible to skipp all cells but the first one which can then `set_symbol(sixel)`. It is up to the user to deal with the graphics size and buffer area size. It is possible to get the terminal's font size in pixels with a syscall. An image widget for ratatui that uses this `skip` flag is available at https://github.com/benjajaja/ratatu-image. Co-authored-by: James <james@rectangle.pizza>
2023-08-25 10:20:36 +01:00
if !current.skip && (current != previous || invalidated > 0) && to_skip == 0 {
let (x, y) = self.pos_of(i);
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
}
2016-10-09 19:46:53 +02:00
}
/// Assert that two buffers are equal by comparing their areas and content.
///
/// On panic, displays the areas or the content and a diff of the contents.
#[macro_export]
macro_rules! assert_buffer_eq {
($actual_expr:expr, $expected_expr:expr) => {
match (&$actual_expr, &$expected_expr) {
(actual, expected) => {
if actual.area != expected.area {
panic!(
indoc::indoc!(
"
buffer areas not equal
expected: {:?}
actual: {:?}"
),
expected, actual
);
}
let diff = expected.diff(&actual);
if !diff.is_empty() {
let nice_diff = diff
.iter()
.enumerate()
.map(|(i, (x, y, cell))| {
let expected_cell = expected.get(*x, *y);
indoc::formatdoc! {"
{i}: at ({x}, {y})
expected: {expected_cell:?}
actual: {cell:?}
"}
})
.collect::<Vec<String>>()
.join("\n");
panic!(
indoc::indoc!(
"
buffer contents not equal
expected: {:?}
actual: {:?}
diff:
{}"
),
expected, actual, nice_diff
);
}
// shouldn't get here, but this guards against future behavior
// that changes equality but not area or content
assert_eq!(actual, expected, "buffers not equal");
}
}
};
}
impl Debug for Buffer {
/// Writes a debug representation of the buffer to the given formatter.
///
/// The format is like a pretty printed struct, with the following fields:
/// * `area`: displayed as `Rect { x: 1, y: 2, width: 3, height: 4 }`
/// * `content`: displayed as a list of strings representing the content of the buffer
/// * `styles`: displayed as a list of: `{ x: 1, y: 2, fg: Color::Red, bg: Color::Blue,
/// modifier: Modifier::BOLD }` only showing a value when there is a change in style.
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
f.write_fmt(format_args!(
"Buffer {{\n area: {:?},\n content: [\n",
&self.area
))?;
let mut last_style = None;
let mut styles = vec![];
for (y, line) in self.content.chunks(self.area.width as usize).enumerate() {
let mut overwritten = vec![];
let mut skip: usize = 0;
f.write_str(" \"")?;
for (x, c) in line.iter().enumerate() {
if skip == 0 {
f.write_str(c.symbol())?;
} else {
overwritten.push((x, c.symbol()));
}
skip = std::cmp::max(skip, c.symbol().width()).saturating_sub(1);
#[cfg(feature = "underline-color")]
{
let style = (c.fg, c.bg, c.underline_color, c.modifier);
if last_style != Some(style) {
last_style = Some(style);
styles.push((x, y, c.fg, c.bg, c.underline_color, c.modifier));
}
}
#[cfg(not(feature = "underline-color"))]
{
let style = (c.fg, c.bg, c.modifier);
if last_style != Some(style) {
last_style = Some(style);
styles.push((x, y, c.fg, c.bg, c.modifier));
}
}
}
if !overwritten.is_empty() {
f.write_fmt(format_args!(
"// hidden by multi-width symbols: {overwritten:?}"
))?;
}
f.write_str("\",\n")?;
}
f.write_str(" ],\n styles: [\n")?;
for s in styles {
#[cfg(feature = "underline-color")]
f.write_fmt(format_args!(
" x: {}, y: {}, fg: {:?}, bg: {:?}, underline: {:?}, modifier: {:?},\n",
s.0, s.1, s.2, s.3, s.4, s.5
))?;
#[cfg(not(feature = "underline-color"))]
f.write_fmt(format_args!(
" x: {}, y: {}, fg: {:?}, bg: {:?}, modifier: {:?},\n",
s.0, s.1, s.2, s.3, s.4
))?;
}
f.write_str(" ]\n}")?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cell(s: &str) -> Cell {
let mut cell = Cell::default();
cell.set_symbol(s);
cell
}
#[test]
fn it_implements_debug() {
let mut buf = Buffer::empty(Rect::new(0, 0, 12, 2));
buf.set_string(0, 0, "Hello World!", Style::default());
buf.set_string(
0,
1,
"G'day World!",
Style::default()
.fg(Color::Green)
.bg(Color::Yellow)
.add_modifier(Modifier::BOLD),
);
#[cfg(feature = "underline-color")]
assert_eq!(
format!("{buf:?}"),
indoc::indoc!(
"
Buffer {
area: Rect { x: 0, y: 0, width: 12, height: 2 },
content: [
\"Hello World!\",
\"G'day World!\",
],
styles: [
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 0, y: 1, fg: Green, bg: Yellow, underline: Reset, modifier: BOLD,
]
}"
)
);
#[cfg(not(feature = "underline-color"))]
assert_eq!(
format!("{buf:?}"),
indoc::indoc!(
"
Buffer {
area: Rect { x: 0, y: 0, width: 12, height: 2 },
content: [
\"Hello World!\",
\"G'day World!\",
],
styles: [
x: 0, y: 0, fg: Reset, bg: Reset, modifier: NONE,
x: 0, y: 1, fg: Green, bg: Yellow, modifier: BOLD,
]
}"
)
);
}
#[test]
fn assert_buffer_eq_does_not_panic_on_equal_buffers() {
let buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
let other_buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
assert_buffer_eq!(buffer, other_buffer);
}
#[should_panic]
#[test]
fn assert_buffer_eq_panics_on_unequal_area() {
let buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
let other_buffer = Buffer::empty(Rect::new(0, 0, 6, 1));
assert_buffer_eq!(buffer, other_buffer);
}
#[should_panic]
#[test]
fn assert_buffer_eq_panics_on_unequal_style() {
let buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
let mut other_buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
other_buffer.set_string(0, 0, " ", Style::default().fg(Color::Red));
assert_buffer_eq!(buffer, other_buffer);
}
#[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_buffer_eq!(buffer, Buffer::with_lines(vec![" "]));
buffer.set_string(0, 0, "aaa", Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["aaa "]));
// Width limit:
buffer.set_stringn(0, 0, "bbbbbbbbbbbbbb", 4, Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["bbbb "]));
buffer.set_string(0, 0, "12345", Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345"]));
// Width truncation:
buffer.set_string(0, 0, "123456", Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345"]));
// multi-line
buffer = Buffer::empty(Rect::new(0, 0, 5, 2));
buffer.set_string(0, 0, "12345", Style::default());
buffer.set_string(0, 1, "67890", Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345", "67890"]));
}
#[test]
fn buffer_set_string_multi_width_overwrite() {
let area = Rect::new(0, 0, 5, 1);
let mut buffer = Buffer::empty(area);
// multi-width overwrite
buffer.set_string(0, 0, "aaaaa", Style::default());
buffer.set_string(0, 0, "称号", Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["称号a"]));
}
#[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_buffer_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_buffer_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_buffer_eq!(buffer, Buffer::with_lines(vec!["コン "]));
// Only 1 space left.
buffer.set_string(0, 0, "コンピ", Style::default());
assert_buffer_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("")),]
);
}
feat(cell): add voluntary skipping capability for sixel (#215) > Sixel is a bitmap graphics format supported by terminals. > "Sixel mode" is entered by sending the sequence ESC+Pq. > The "String Terminator" sequence ESC+\ exits the mode. The graphics are then rendered with the top left positioned at the cursor position. It is actually possible to render sixels in ratatui with just `buf.get_mut(x, y).set_symbol("^[Pq ... ^[\")`. But any buffer covering the "image area" will overwrite the graphics. This is most likely the same buffer, even though it consists of empty characters `' '`, except for the top-left character that starts the sequence. Thus, either the buffer or cells must be specialized to avoid drawing over the graphics. This patch specializes the `Cell` with a `set_skip(bool)` method, based on James' patch: https://github.com/TurtleTheSeaHobo/tui-rs/tree/sixel-support I unsuccessfully tried specializing the `Buffer`, but as far as I can tell buffers get merged all the way "up" and thus skipping must be set on the Cells. Otherwise some kind of "skipping area" state would be required, which I think is too complicated. Having access to the buffer now it is possible to skipp all cells but the first one which can then `set_symbol(sixel)`. It is up to the user to deal with the graphics size and buffer area size. It is possible to get the terminal's font size in pixels with a syscall. An image widget for ratatui that uses this `skip` flag is available at https://github.com/benjajaja/ratatu-image. Co-authored-by: James <james@rectangle.pizza>
2023-08-25 10:20:36 +01:00
#[test]
fn buffer_diffing_skip() {
let prev = Buffer::with_lines(vec!["123"]);
let mut next = Buffer::with_lines(vec!["456"]);
for i in 1..3 {
next.content[i].set_skip(true);
}
let diff = prev.diff(&next);
assert_eq!(diff, vec![(0, 0, &cell("4"))],);
}
#[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_buffer_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_buffer_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_buffer_eq!(one, merged);
}
feat(cell): add voluntary skipping capability for sixel (#215) > Sixel is a bitmap graphics format supported by terminals. > "Sixel mode" is entered by sending the sequence ESC+Pq. > The "String Terminator" sequence ESC+\ exits the mode. The graphics are then rendered with the top left positioned at the cursor position. It is actually possible to render sixels in ratatui with just `buf.get_mut(x, y).set_symbol("^[Pq ... ^[\")`. But any buffer covering the "image area" will overwrite the graphics. This is most likely the same buffer, even though it consists of empty characters `' '`, except for the top-left character that starts the sequence. Thus, either the buffer or cells must be specialized to avoid drawing over the graphics. This patch specializes the `Cell` with a `set_skip(bool)` method, based on James' patch: https://github.com/TurtleTheSeaHobo/tui-rs/tree/sixel-support I unsuccessfully tried specializing the `Buffer`, but as far as I can tell buffers get merged all the way "up" and thus skipping must be set on the Cells. Otherwise some kind of "skipping area" state would be required, which I think is too complicated. Having access to the buffer now it is possible to skipp all cells but the first one which can then `set_symbol(sixel)`. It is up to the user to deal with the graphics size and buffer area size. It is possible to get the terminal's font size in pixels with a syscall. An image widget for ratatui that uses this `skip` flag is available at https://github.com/benjajaja/ratatu-image. Co-authored-by: James <james@rectangle.pizza>
2023-08-25 10:20:36 +01:00
#[test]
fn buffer_merge_skip() {
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: 1,
width: 2,
height: 2,
},
Cell::default().set_symbol("2").set_skip(true),
);
one.merge(&two);
let skipped: Vec<bool> = one.content().iter().map(|c| c.skip).collect();
assert_eq!(skipped, vec![false, false, true, true, true, true]);
}
#[test]
fn buffer_merge_skip2() {
let mut one = Buffer::filled(
Rect {
x: 0,
y: 0,
width: 2,
height: 2,
},
Cell::default().set_symbol("1").set_skip(true),
);
let two = Buffer::filled(
Rect {
x: 0,
y: 1,
width: 2,
height: 2,
},
Cell::default().set_symbol("2"),
);
one.merge(&two);
let skipped: Vec<bool> = one.content().iter().map(|c| c.skip).collect();
assert_eq!(skipped, vec![true, true, false, false, false, false]);
}
#[test]
fn with_lines_accepts_into_lines() {
use crate::style::Stylize;
let mut buf = Buffer::empty(Rect::new(0, 0, 3, 2));
buf.set_string(0, 0, "foo", Style::new().red());
buf.set_string(0, 1, "bar", Style::new().blue());
assert_eq!(buf, Buffer::with_lines(vec!["foo".red(), "bar".blue()]));
}
#[test]
fn cell_symbol_field() {
let mut cell = Cell::default();
assert_eq!(cell.symbol(), " ");
cell.set_symbol(""); // Multi-byte character
assert_eq!(cell.symbol(), "");
cell.set_symbol("👨‍👩‍👧‍👦"); // Multiple code units combined with ZWJ
assert_eq!(cell.symbol(), "👨‍👩‍👧‍👦");
}
}