Compare commits
2 Commits
v0.28.1-al
...
220/baseli
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f5594beff | ||
|
|
9da02078fe |
211
src/buffer.rs
211
src/buffer.rs
@@ -3,7 +3,10 @@ use crate::{
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans},
|
||||
};
|
||||
use std::cmp::min;
|
||||
use std::{
|
||||
cmp::min,
|
||||
fmt::{Debug, Formatter, Result},
|
||||
};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
@@ -105,7 +108,7 @@ impl Default for Cell {
|
||||
/// buf.get_mut(5, 0).set_char('x');
|
||||
/// assert_eq!(buf.get(5, 0).symbol, "x");
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
#[derive(Clone, PartialEq, Eq, Default)]
|
||||
pub struct Buffer {
|
||||
/// The area represented by this buffer
|
||||
pub area: Rect,
|
||||
@@ -453,6 +456,113 @@ impl Buffer {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
format!(
|
||||
"{}: at ({}, {})\n expected: {:?}\n actual: {:?}",
|
||||
i, x, y, expected_cell, 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);
|
||||
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 {
|
||||
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::*;
|
||||
@@ -463,6 +573,62 @@ mod tests {
|
||||
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),
|
||||
);
|
||||
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: (empty),
|
||||
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);
|
||||
@@ -504,21 +670,38 @@ mod tests {
|
||||
|
||||
// Zero-width
|
||||
buffer.set_stringn(0, 0, "aaa", 0, Style::default());
|
||||
assert_eq!(buffer, Buffer::with_lines(vec![" "]));
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" "]));
|
||||
|
||||
buffer.set_string(0, 0, "aaa", Style::default());
|
||||
assert_eq!(buffer, Buffer::with_lines(vec!["aaa "]));
|
||||
assert_buffer_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 "]));
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["bbbb "]));
|
||||
|
||||
buffer.set_string(0, 0, "12345", Style::default());
|
||||
assert_eq!(buffer, Buffer::with_lines(vec!["12345"]));
|
||||
assert_buffer_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"]));
|
||||
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]
|
||||
@@ -529,12 +712,12 @@ mod tests {
|
||||
// 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"]));
|
||||
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_eq!(buffer, Buffer::with_lines(vec!["a"]));
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["a"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -542,11 +725,11 @@ mod tests {
|
||||
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!["コン "]));
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["コン "]));
|
||||
|
||||
// Only 1 space left.
|
||||
buffer.set_string(0, 0, "コンピ", Style::default());
|
||||
assert_eq!(buffer, Buffer::with_lines(vec!["コン "]));
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["コン "]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -671,7 +854,7 @@ mod tests {
|
||||
Cell::default().set_symbol("2"),
|
||||
);
|
||||
one.merge(&two);
|
||||
assert_eq!(one, Buffer::with_lines(vec!["11", "11", "22", "22"]));
|
||||
assert_buffer_eq!(one, Buffer::with_lines(vec!["11", "11", "22", "22"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -695,7 +878,7 @@ mod tests {
|
||||
Cell::default().set_symbol("2"),
|
||||
);
|
||||
one.merge(&two);
|
||||
assert_eq!(
|
||||
assert_buffer_eq!(
|
||||
one,
|
||||
Buffer::with_lines(vec!["22 ", "22 ", " 11", " 11"])
|
||||
);
|
||||
@@ -729,6 +912,6 @@ mod tests {
|
||||
width: 4,
|
||||
height: 4,
|
||||
};
|
||||
assert_eq!(one, merged);
|
||||
assert_buffer_eq!(one, merged);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ pub struct Sparkline<'a> {
|
||||
/// The maximum value to take to compute the maximum bar height (if nothing is specified, the
|
||||
/// widget uses the max of the dataset)
|
||||
max: Option<u64>,
|
||||
/// If true, draws a baseline of `bar::ONE_EIGHTH` spanning the bottom of the sparkline graph
|
||||
show_baseline: bool,
|
||||
/// A set of bar symbols used to represent the give data
|
||||
bar_set: symbols::bar::Set,
|
||||
// The direction to render the sparkine, either from left to right, or from right to left
|
||||
@@ -50,6 +52,7 @@ impl<'a> Default for Sparkline<'a> {
|
||||
style: Default::default(),
|
||||
data: &[],
|
||||
max: None,
|
||||
show_baseline: false,
|
||||
bar_set: symbols::bar::NINE_LEVELS,
|
||||
direction: RenderDirection::LeftToRight,
|
||||
}
|
||||
@@ -77,6 +80,11 @@ impl<'a> Sparkline<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn show_baseline(mut self, show_baseline: bool) -> Sparkline<'a> {
|
||||
self.show_baseline = show_baseline;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn bar_set(mut self, bar_set: symbols::bar::Set) -> Sparkline<'a> {
|
||||
self.bar_set = bar_set;
|
||||
self
|
||||
@@ -103,6 +111,14 @@ impl<'a> Widget for Sparkline<'a> {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.show_baseline {
|
||||
for i in spark_area.left()..spark_area.right() {
|
||||
buf.get_mut(i, spark_area.bottom() - 1)
|
||||
.set_symbol(self.bar_set.one_eighth)
|
||||
.set_style(self.style);
|
||||
}
|
||||
}
|
||||
|
||||
let max = match self.max {
|
||||
Some(v) => v,
|
||||
None => *self.data.iter().max().unwrap_or(&1u64),
|
||||
@@ -123,7 +139,13 @@ impl<'a> Widget for Sparkline<'a> {
|
||||
for j in (0..spark_area.height).rev() {
|
||||
for (i, d) in data.iter_mut().enumerate() {
|
||||
let symbol = match *d {
|
||||
0 => self.bar_set.empty,
|
||||
0 => {
|
||||
if self.show_baseline && j == spark_area.height - 1 {
|
||||
self.bar_set.one_eighth
|
||||
} else {
|
||||
self.bar_set.empty
|
||||
}
|
||||
}
|
||||
1 => self.bar_set.one_eighth,
|
||||
2 => self.bar_set.one_quarter,
|
||||
3 => self.bar_set.three_eighths,
|
||||
@@ -153,40 +175,91 @@ impl<'a> Widget for Sparkline<'a> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::buffer::Cell;
|
||||
|
||||
use super::*;
|
||||
use crate::{assert_buffer_eq, buffer::Cell};
|
||||
|
||||
// Helper function to render a sparkline to a buffer with a given width
|
||||
// filled with x symbols to make it easier to assert on the result
|
||||
fn render(widget: Sparkline, width: u16) -> Buffer {
|
||||
let area = Rect::new(0, 0, width, 1);
|
||||
fn render(widget: Sparkline, width: u16, height: u16) -> Buffer {
|
||||
let mut cell = Cell::default();
|
||||
cell.set_symbol("x");
|
||||
let mut buffer = Buffer::filled(area, &cell);
|
||||
widget.render(area, &mut buffer);
|
||||
let mut buffer = Buffer::filled(Rect::new(0, 0, width, height), &cell);
|
||||
widget.render(buffer.area, &mut buffer);
|
||||
buffer
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_does_not_panic_if_max_is_zero() {
|
||||
let widget = Sparkline::default().data(&[0, 0, 0]);
|
||||
let buffer = render(widget, 6);
|
||||
assert_eq!(buffer, Buffer::with_lines(vec![" xxx"]));
|
||||
let buffer = render(widget, 6, 1);
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" xxx"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_does_not_panic_if_max_is_set_to_zero() {
|
||||
let widget = Sparkline::default().data(&[0, 1, 2]).max(0);
|
||||
let buffer = render(widget, 6);
|
||||
assert_eq!(buffer, Buffer::with_lines(vec![" xxx"]));
|
||||
let buffer = render(widget, 6, 1);
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" xxx"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_draws() {
|
||||
fn it_renders() {
|
||||
let widget = Sparkline::default().data(&[0, 1, 2, 3, 4, 5, 6, 7, 8]);
|
||||
let buffer = render(widget, 12);
|
||||
assert_eq!(buffer, Buffer::with_lines(vec![" ▁▂▃▄▅▆▇█xxx"]));
|
||||
let buffer = render(widget, 12, 1);
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" ▁▂▃▄▅▆▇█xxx"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_renders_with_max_more_than_data_max() {
|
||||
let widget = Sparkline::default()
|
||||
.data(&[0, 1, 2, 3, 4, 5, 6, 7, 8])
|
||||
.max(16);
|
||||
let buffer = render(widget, 12, 1);
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" ▁▁▂▂▃▃▄xxx"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_renders_with_max_less_than_data_max() {
|
||||
let widget = Sparkline::default()
|
||||
.data(&[0, 1, 2, 3, 4, 5, 6, 7, 8])
|
||||
.max(4);
|
||||
let buffer = render(widget, 12, 1);
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" ▂▄▆█████xxx"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_renders_with_multi_line() {
|
||||
let widget = Sparkline::default().data(&[0, 1, 2, 3, 4, 5, 6, 7, 8]);
|
||||
let buffer = render(widget, 15, 3);
|
||||
assert_buffer_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec![
|
||||
" ▂▅█xxxxxx",
|
||||
" ▁▄▇███xxxxxx",
|
||||
" ▃▆██████xxxxxx",
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_renders_with_multi_line_and_baseline() {
|
||||
let widget = Sparkline::default()
|
||||
.data(&[0, 1, 2, 3, 4, 5, 6, 7, 8])
|
||||
.show_baseline(true);
|
||||
let buffer = render(widget, 15, 3);
|
||||
assert_buffer_eq!(
|
||||
buffer,
|
||||
// this currently fails because the baseline logic doesn't clear
|
||||
// the parts above the line
|
||||
// " ▂▅█xxxxxx",
|
||||
// " ▁▄▇███xxxxxx",
|
||||
// " ▃▆██████▁▁▁▁▁▁",
|
||||
Buffer::with_lines(vec![
|
||||
" ▂▅█ ",
|
||||
" ▁▄▇███ ",
|
||||
" ▃▆██████▁▁▁▁▁▁",
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -194,8 +267,8 @@ mod tests {
|
||||
let widget = Sparkline::default()
|
||||
.data(&[0, 1, 2, 3, 4, 5, 6, 7, 8])
|
||||
.direction(RenderDirection::LeftToRight);
|
||||
let buffer = render(widget, 12);
|
||||
assert_eq!(buffer, Buffer::with_lines(vec![" ▁▂▃▄▅▆▇█xxx"]));
|
||||
let buffer = render(widget, 12, 1);
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" ▁▂▃▄▅▆▇█xxx"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -203,7 +276,26 @@ mod tests {
|
||||
let widget = Sparkline::default()
|
||||
.data(&[0, 1, 2, 3, 4, 5, 6, 7, 8])
|
||||
.direction(RenderDirection::RightToLeft);
|
||||
let buffer = render(widget, 12);
|
||||
assert_eq!(buffer, Buffer::with_lines(vec!["xxx█▇▆▅▄▃▂▁ "]));
|
||||
let buffer = render(widget, 12, 1);
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["xxx█▇▆▅▄▃▂▁ "]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_renders_baseline() {
|
||||
let widget = Sparkline::default()
|
||||
.data(&[0, 1, 2, 3, 4, 5, 6, 7, 8])
|
||||
.show_baseline(true);
|
||||
let buffer = render(widget, 12, 1);
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["▁▁▂▃▄▅▆▇█▁▁▁"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_renders_baseline_right_to_left() {
|
||||
let widget = Sparkline::default()
|
||||
.data(&[0, 1, 2, 3, 4, 5, 6, 7, 8])
|
||||
.direction(RenderDirection::RightToLeft)
|
||||
.show_baseline(true);
|
||||
let buffer = render(widget, 12, 1);
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["▁▁▁█▇▆▅▄▃▂▁▁"]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use ratatui::{
|
||||
assert_buffer_eq,
|
||||
backend::TestBackend,
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Rect},
|
||||
@@ -237,6 +238,8 @@ fn widgets_block_title_alignment() {
|
||||
.unwrap();
|
||||
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
let area = Rect::new(1, 0, 13, 2);
|
||||
assert_buffer_eq!(Buffer::empty(area), Buffer::empty(area));
|
||||
};
|
||||
|
||||
// title top-left with all borders
|
||||
|
||||
Reference in New Issue
Block a user