mirror of
https://github.com/ratatui-org/ratatui
synced 2024-11-22 20:53:19 +00:00
Buffer: correct diffing of buffers with multi-width characters
Resolves #104
This commit is contained in:
parent
10642d0e04
commit
3fd9e23851
3 changed files with 264 additions and 48 deletions
258
src/buffer.rs
258
src/buffer.rs
|
@ -3,6 +3,7 @@ use std::fmt;
|
||||||
use std::usize;
|
use std::usize;
|
||||||
|
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
use layout::Rect;
|
use layout::Rect;
|
||||||
use style::{Color, Modifier, Style};
|
use style::{Color, Modifier, Style};
|
||||||
|
@ -117,8 +118,25 @@ impl fmt::Debug for Buffer {
|
||||||
writeln!(f, "Buffer: {:?}", self.area)?;
|
writeln!(f, "Buffer: {:?}", self.area)?;
|
||||||
f.write_str("Content (quoted lines):\n")?;
|
f.write_str("Content (quoted lines):\n")?;
|
||||||
for cells in self.content.chunks(self.area.width as usize) {
|
for cells in self.content.chunks(self.area.width as usize) {
|
||||||
let line: String = cells.iter().map(|cell| &cell.symbol[..]).collect();
|
let mut line = String::new();
|
||||||
f.write_fmt(format_args!("{:?},\n", line))?;
|
let mut overwritten = vec![];
|
||||||
|
let mut skip: usize = 0;
|
||||||
|
for (x, c) in cells.iter().enumerate() {
|
||||||
|
if skip == 0 {
|
||||||
|
line.push_str(&c.symbol);
|
||||||
|
} else {
|
||||||
|
overwritten.push((x, &c.symbol))
|
||||||
|
}
|
||||||
|
skip = std::cmp::max(skip, c.symbol.width()).saturating_sub(1);
|
||||||
|
}
|
||||||
|
f.write_fmt(format_args!("{:?},", line))?;
|
||||||
|
if !overwritten.is_empty() {
|
||||||
|
f.write_fmt(format_args!(
|
||||||
|
" Hidden by multi-width symbols: {:?}",
|
||||||
|
overwritten
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
f.write_str("\n")?;
|
||||||
}
|
}
|
||||||
f.write_str("Style:\n")?;
|
f.write_str("Style:\n")?;
|
||||||
for cells in self.content.chunks(self.area.width as usize) {
|
for cells in self.content.chunks(self.area.width as usize) {
|
||||||
|
@ -162,10 +180,7 @@ impl Buffer {
|
||||||
{
|
{
|
||||||
let height = lines.len() as u16;
|
let height = lines.len() as u16;
|
||||||
let width = lines.iter().fold(0, |acc, item| {
|
let width = lines.iter().fold(0, |acc, item| {
|
||||||
std::cmp::max(
|
std::cmp::max(acc, item.as_ref().width() as u16)
|
||||||
acc,
|
|
||||||
UnicodeSegmentation::graphemes(item.as_ref(), true).count() as u16,
|
|
||||||
)
|
|
||||||
});
|
});
|
||||||
let mut buffer = Buffer::empty(Rect {
|
let mut buffer = Buffer::empty(Rect {
|
||||||
x: 0,
|
x: 0,
|
||||||
|
@ -295,18 +310,30 @@ impl Buffer {
|
||||||
|
|
||||||
/// Print at most the first n characters of a string if enough space is available
|
/// Print at most the first n characters of a string if enough space is available
|
||||||
/// until the end of the line
|
/// until the end of the line
|
||||||
pub fn set_stringn<S>(&mut self, x: u16, y: u16, string: S, limit: usize, style: Style)
|
pub fn set_stringn<S>(&mut self, x: u16, y: u16, string: S, width: usize, style: Style)
|
||||||
where
|
where
|
||||||
S: AsRef<str>,
|
S: AsRef<str>,
|
||||||
{
|
{
|
||||||
let mut index = self.index_of(x, y);
|
let mut index = self.index_of(x, y);
|
||||||
|
let mut x_offset = x as usize;
|
||||||
let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true);
|
let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true);
|
||||||
let max_index = min((self.area.right() - x) as usize, limit);
|
let max_offset = min(self.area.right() as usize, width.saturating_add(x as usize));
|
||||||
for s in graphemes.take(max_index) {
|
for s in graphemes {
|
||||||
self.content[index].symbol.clear();
|
let width = s.width();
|
||||||
self.content[index].symbol.push_str(s);
|
// `x_offset + width > max_offset` could be integer overflow on 32-bit machines if we
|
||||||
self.content[index].style = style;
|
// change dimenstions to usize or u32 and someone resizes the terminal to 1x2^32.
|
||||||
index += 1;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -362,12 +389,72 @@ impl Buffer {
|
||||||
}
|
}
|
||||||
self.area = area;
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
fn cell(s: &str) -> Cell {
|
||||||
|
let mut cell = Cell::default();
|
||||||
|
cell.set_symbol(s);
|
||||||
|
cell
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn it_translates_to_and_from_coordinates() {
|
fn it_translates_to_and_from_coordinates() {
|
||||||
let rect = Rect::new(200, 100, 50, 80);
|
let rect = Rect::new(200, 100, 50, 80);
|
||||||
|
@ -401,4 +488,149 @@ mod tests {
|
||||||
// width is 10; zero-indexed means that 10 would be the 11th cell.
|
// width is 10; zero-indexed means that 10 would be the 11th cell.
|
||||||
buf.index_of(10, 0);
|
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_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("号")),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -97,26 +97,13 @@ where
|
||||||
&mut self.backend
|
&mut self.backend
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds a string representing the minimal escape sequences and characters set necessary to
|
/// Obtains a difference between the previous and the current buffer and passes it to the
|
||||||
/// update the UI and writes it to stdout.
|
/// current backend for drawing.
|
||||||
pub fn flush(&mut self) -> io::Result<()> {
|
pub fn flush(&mut self) -> io::Result<()> {
|
||||||
let width = self.buffers[self.current].area.width;
|
let previous_buffer = &self.buffers[1 - self.current];
|
||||||
let content = self.buffers[self.current]
|
let current_buffer = &self.buffers[self.current];
|
||||||
.content
|
let updates = previous_buffer.diff(current_buffer);
|
||||||
.iter()
|
self.backend.draw(updates.into_iter())
|
||||||
.zip(self.buffers[1 - self.current].content.iter())
|
|
||||||
.enumerate()
|
|
||||||
.filter_map(|(i, (c, p))| {
|
|
||||||
if c != p {
|
|
||||||
let i = i as u16;
|
|
||||||
let x = i % width;
|
|
||||||
let y = i / width;
|
|
||||||
Some((x, y, c))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
});
|
|
||||||
self.backend.draw(content)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates the Terminal so that internal buffers match the requested size. Requested size will
|
/// Updates the Terminal so that internal buffers match the requested size. Requested size will
|
||||||
|
|
|
@ -99,18 +99,15 @@ fn paragraph_render_double_width() {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let expected = Buffer::with_lines(vec![
|
let expected = Buffer::with_lines(vec![
|
||||||
// This is "OK" - these are double-width characters. In terminal each occupies 2 spaces,
|
|
||||||
// which means the buffer contains a Cell with a full grapheme in it, followed by a vacant
|
|
||||||
// one. Here however, we have plain text, so each character is visibly followed by a space.
|
|
||||||
"┌────────┐",
|
"┌────────┐",
|
||||||
"│コ ン ピ ュ │",
|
"│コンピュ│",
|
||||||
"│ー タ 上 で │",
|
"│ータ上で│",
|
||||||
"│文 字 を 扱 │",
|
"│文字を扱│",
|
||||||
"│う 場 合 、 │",
|
"│う場合、│",
|
||||||
"│典 型 的 に │",
|
"│典型的に│",
|
||||||
"│は 文 字 に │",
|
"│は文字に│",
|
||||||
"│よ る 通 信 │",
|
"│よる通信│",
|
||||||
"│を 行 う 場 │",
|
"│を行う場│",
|
||||||
"└────────┘",
|
"└────────┘",
|
||||||
]);
|
]);
|
||||||
assert_eq!(&expected, terminal.backend().buffer());
|
assert_eq!(&expected, terminal.backend().buffer());
|
||||||
|
@ -136,11 +133,11 @@ fn paragraph_render_mixed_width() {
|
||||||
let expected = Buffer::with_lines(vec![
|
let expected = Buffer::with_lines(vec![
|
||||||
// The internal width is 8 so only 4 slots for double-width characters.
|
// The internal width is 8 so only 4 slots for double-width characters.
|
||||||
"┌────────┐",
|
"┌────────┐",
|
||||||
"│aコ ン ピ │", // Here we have 1 latin character so only 3 double-width ones can fit.
|
"│aコンピ │", // Here we have 1 latin character so only 3 double-width ones can fit.
|
||||||
"│ュ ー タ 上 │",
|
"│ュータ上│",
|
||||||
"│で 文 字 を │",
|
"│で文字を│",
|
||||||
"│扱 う 場 合 │",
|
"│扱う場合│",
|
||||||
"│、 │",
|
"│、 │",
|
||||||
"└────────┘",
|
"└────────┘",
|
||||||
]);
|
]);
|
||||||
assert_eq!(&expected, terminal.backend().buffer());
|
assert_eq!(&expected, terminal.backend().buffer());
|
||||||
|
|
Loading…
Reference in a new issue