fix: panic when rendering widgets on too small buffer (#1996)
Fixes panic on overflow on horizontal `Barchart` and `RatatuiMascot` and adds proper tests to all widgets. --------- Co-authored-by: Orhun Parmaksız <orhun@archlinux.org>
This commit is contained in:
committed by
GitHub
parent
cfebd68e18
commit
c1b8528b69
@@ -515,7 +515,7 @@ impl BarChart<'_> {
|
||||
let margin = u16::from(label_size != 0);
|
||||
Rect {
|
||||
x: area.x + label_size + margin,
|
||||
width: area.width - label_size - margin,
|
||||
width: area.width.saturating_sub(label_size).saturating_sub(margin),
|
||||
..area
|
||||
}
|
||||
};
|
||||
@@ -579,10 +579,10 @@ impl BarChart<'_> {
|
||||
}
|
||||
|
||||
fn render_vertical(&self, buf: &mut Buffer, area: Rect) {
|
||||
let label_info = self.label_info(area.height - 1);
|
||||
let label_info = self.label_info(area.height.saturating_sub(1));
|
||||
|
||||
let bars_area = Rect {
|
||||
height: area.height - label_info.height,
|
||||
height: area.height.saturating_sub(label_info.height),
|
||||
..area
|
||||
};
|
||||
|
||||
@@ -722,6 +722,7 @@ mod tests {
|
||||
use ratatui_core::layout::Alignment;
|
||||
use ratatui_core::style::{Color, Modifier, Stylize};
|
||||
use ratatui_core::text::Span;
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
use crate::borders::BorderType;
|
||||
@@ -1472,4 +1473,35 @@ mod tests {
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::horizontal(Direction::Horizontal)]
|
||||
#[case::vertical(Direction::Vertical)]
|
||||
fn render_in_minimal_buffer(#[case] direction: Direction) {
|
||||
let chart = BarChart::default()
|
||||
.data(&[("A", 1), ("B", 2)])
|
||||
.bar_width(3)
|
||||
.bar_gap(1)
|
||||
.direction(direction);
|
||||
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
// This should not panic, even if the buffer is too small to render the chart.
|
||||
chart.render(buffer.area, &mut buffer);
|
||||
assert_eq!(buffer, Buffer::with_lines([" "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::horizontal(Direction::Horizontal)]
|
||||
#[case::vertical(Direction::Vertical)]
|
||||
fn render_in_zero_size_buffer(#[case] direction: Direction) {
|
||||
let chart = BarChart::default()
|
||||
.data(&[("A", 1), ("B", 2)])
|
||||
.bar_width(3)
|
||||
.bar_gap(1)
|
||||
.direction(direction);
|
||||
|
||||
let mut buffer = Buffer::empty(Rect::ZERO);
|
||||
// This should not panic, even if the buffer has zero size.
|
||||
chart.render(buffer.area, &mut buffer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2134,4 +2134,25 @@ mod tests {
|
||||
.render(buffer.area, &mut buffer);
|
||||
assert_eq!(buffer, Buffer::with_lines([" C1R67890"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_in_minimal_buffer() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
// This should not panic, even if the buffer is too small to render the block.
|
||||
Block::bordered()
|
||||
.title("I'm too big for this buffer")
|
||||
.padding(Padding::uniform(10))
|
||||
.render(buffer.area, &mut buffer);
|
||||
assert_eq!(buffer, Buffer::with_lines(["┌"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_in_zero_size_buffer() {
|
||||
let mut buffer = Buffer::empty(Rect::ZERO);
|
||||
// This should not panic, even if the buffer has zero size.
|
||||
Block::bordered()
|
||||
.title("I'm too big for this buffer")
|
||||
.padding(Padding::uniform(10))
|
||||
.render(buffer.area, &mut buffer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,4 +302,27 @@ mod tests {
|
||||
fn test_today() {
|
||||
CalendarEventStore::today(Style::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_in_minimal_buffer() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
let calendar = Monthly::new(
|
||||
Date::from_calendar_date(1984, Month::January, 1).unwrap(),
|
||||
CalendarEventStore::default(),
|
||||
);
|
||||
// This should not panic, even if the buffer is too small to render the calendar.
|
||||
calendar.render(buffer.area, &mut buffer);
|
||||
assert_eq!(buffer, Buffer::with_lines([" "]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_in_zero_size_buffer() {
|
||||
let mut buffer = Buffer::empty(Rect::ZERO);
|
||||
let calendar = Monthly::new(
|
||||
Date::from_calendar_date(1984, Month::January, 1).unwrap(),
|
||||
CalendarEventStore::default(),
|
||||
);
|
||||
// This should not panic, even if the buffer has zero size.
|
||||
calendar.render(buffer.area, &mut buffer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -976,4 +976,27 @@ mod tests {
|
||||
b_grid.paint(usize::MAX, usize::MAX, Color::Red);
|
||||
c_grid.paint(usize::MAX, usize::MAX, Color::Red);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_in_minimal_buffer() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
let canvas = Canvas::default()
|
||||
.x_bounds([0.0, 10.0])
|
||||
.y_bounds([0.0, 10.0])
|
||||
.paint(|_ctx| {});
|
||||
// This should not panic, even if the buffer is too small to render the canvas.
|
||||
canvas.render(buffer.area, &mut buffer);
|
||||
assert_eq!(buffer, Buffer::with_lines([" "]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_in_zero_size_buffer() {
|
||||
let mut buffer = Buffer::empty(Rect::ZERO);
|
||||
let canvas = Canvas::default()
|
||||
.x_bounds([0.0, 10.0])
|
||||
.y_bounds([0.0, 10.0])
|
||||
.paint(|_ctx| {});
|
||||
// This should not panic, even if the buffer has zero size.
|
||||
canvas.render(buffer.area, &mut buffer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1547,4 +1547,25 @@ mod tests {
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_in_minimal_buffer() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
let chart = Chart::new(vec![Dataset::default().data(&[(0.0, 0.0), (1.0, 1.0)])])
|
||||
.x_axis(Axis::default().bounds([0.0, 1.0]))
|
||||
.y_axis(Axis::default().bounds([0.0, 1.0]));
|
||||
// This should not panic, even if the buffer is too small to render the chart.
|
||||
chart.render(buffer.area, &mut buffer);
|
||||
assert_eq!(buffer, Buffer::with_lines(["•"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_in_zero_size_buffer() {
|
||||
let mut buffer = Buffer::empty(Rect::ZERO);
|
||||
let chart = Chart::new(vec![Dataset::default().data(&[(0.0, 0.0), (1.0, 1.0)])])
|
||||
.x_axis(Axis::default().bounds([0.0, 1.0]))
|
||||
.y_axis(Axis::default().bounds([0.0, 1.0]));
|
||||
// This should not panic, even if the buffer has zero size.
|
||||
chart.render(buffer.area, &mut buffer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -586,4 +586,38 @@ mod tests {
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_in_minimal_buffer_gauge() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
let gauge = Gauge::default().percent(50);
|
||||
// This should not panic, even if the buffer is too small to render the gauge.
|
||||
gauge.render(buffer.area, &mut buffer);
|
||||
assert_eq!(buffer, Buffer::with_lines(["5"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_in_minimal_buffer_line_gauge() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
let line_gauge = LineGauge::default().ratio(0.5);
|
||||
// This should not panic, even if the buffer is too small to render the line gauge.
|
||||
line_gauge.render(buffer.area, &mut buffer);
|
||||
assert_eq!(buffer, Buffer::with_lines(["5"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_in_zero_size_buffer_gauge() {
|
||||
let mut buffer = Buffer::empty(Rect::ZERO);
|
||||
let gauge = Gauge::default().percent(50);
|
||||
// This should not panic, even if the buffer has zero size.
|
||||
gauge.render(buffer.area, &mut buffer);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_in_zero_size_buffer_line_gauge() {
|
||||
let mut buffer = Buffer::empty(Rect::ZERO);
|
||||
let line_gauge = LineGauge::default().ratio(0.5);
|
||||
// This should not panic, even if the buffer has zero size.
|
||||
line_gauge.render(buffer.area, &mut buffer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -625,4 +625,33 @@ mod tests {
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_in_minimal_buffer() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
let mut state = ListState::default().with_selected(None);
|
||||
let items = vec![
|
||||
ListItem::new("Item 1"),
|
||||
ListItem::new("Item 2"),
|
||||
ListItem::new("Item 3"),
|
||||
];
|
||||
let list = List::new(items);
|
||||
// This should not panic, even if the buffer is too small to render the list.
|
||||
list.render(buffer.area, &mut buffer, &mut state);
|
||||
assert_eq!(buffer, Buffer::with_lines(["I"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_in_zero_size_buffer() {
|
||||
let mut buffer = Buffer::empty(Rect::ZERO);
|
||||
let mut state = ListState::default().with_selected(None);
|
||||
let items = vec![
|
||||
ListItem::new("Item 1"),
|
||||
ListItem::new("Item 2"),
|
||||
ListItem::new("Item 3"),
|
||||
];
|
||||
let list = List::new(items);
|
||||
// This should not panic, even if the buffer has zero size.
|
||||
list.render(buffer.area, &mut buffer, &mut state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,4 +236,25 @@ mod tests {
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::tiny(Size::Tiny, Buffer::with_lines(["▛"]))]
|
||||
#[case::small(Size::Small, Buffer::with_lines(["█"]))]
|
||||
fn render_in_minimal_buffer(#[case] size: Size, #[case] expected: Buffer) {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
let logo = RatatuiLogo::new(size);
|
||||
// This should not panic, even if the buffer is too small to render the logo.
|
||||
logo.render(buffer.area, &mut buffer);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::tiny(Size::Tiny)]
|
||||
#[case::small(Size::Small)]
|
||||
fn render_in_zero_size_buffer(#[case] size: Size) {
|
||||
let mut buffer = Buffer::empty(Rect::ZERO);
|
||||
let logo = RatatuiLogo::new(size);
|
||||
// This should not panic, even if the buffer has zero size.
|
||||
logo.render(buffer.area, &mut buffer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,10 +135,21 @@ impl Widget for RatatuiMascot {
|
||||
/// The logo colors are hardcorded in the widget.
|
||||
/// The eye color depends on whether it's open / blinking
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let area = area.intersection(buf.area);
|
||||
if area.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
for (y, (line1, line2)) in RATATUI_MASCOT.lines().tuples().enumerate() {
|
||||
for (x, (ch1, ch2)) in line1.chars().zip(line2.chars()).enumerate() {
|
||||
let x = area.left() + x as u16;
|
||||
let y = area.top() + y as u16;
|
||||
|
||||
// Check if coordinates are within the buffer area
|
||||
if x >= area.right() || y >= area.bottom() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let cell = &mut buf[(x, y)];
|
||||
// given two cells which make up the top and bottom of the character,
|
||||
// Foreground color should be the non-space, non-terminal
|
||||
@@ -229,4 +240,21 @@ mod tests {
|
||||
.collect::<String>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_in_minimal_buffer() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
let mascot = RatatuiMascot::new();
|
||||
// This should not panic, even if the buffer is too small to render the mascot.
|
||||
mascot.render(buffer.area, &mut buffer);
|
||||
assert_eq!(buffer, Buffer::with_lines([" "]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_in_zero_size_buffer() {
|
||||
let mut buffer = Buffer::empty(Rect::ZERO);
|
||||
let mascot = RatatuiMascot::new();
|
||||
// This should not panic, even if the buffer has zero size.
|
||||
mascot.render(buffer.area, &mut buffer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1219,4 +1219,21 @@ mod tests {
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_in_minimal_buffer() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
let paragraph = Paragraph::new("Lorem ipsum");
|
||||
// This should not panic, even if the buffer is too small to render the paragraph.
|
||||
paragraph.render(buffer.area, &mut buffer);
|
||||
assert_eq!(buffer, Buffer::with_lines(["L"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_in_zero_size_buffer() {
|
||||
let mut buffer = Buffer::empty(Rect::ZERO);
|
||||
let paragraph = Paragraph::new("Lorem ipsum");
|
||||
// This should not panic, even if the buffer has zero size.
|
||||
paragraph.render(buffer.area, &mut buffer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -970,6 +970,8 @@ mod tests {
|
||||
#[case::position_8("<---#####>", 8, 10)]
|
||||
#[case::position_9("<----####>", 9, 10)]
|
||||
#[case::position_one_out_of_bounds("<----####>", 10, 10)]
|
||||
#[case::position_few_out_of_bounds("<----####>", 15, 10)]
|
||||
#[case::position_very_many_out_of_bounds("<----####>", 500, 10)]
|
||||
fn render_scrollbar_vertical_left(
|
||||
#[case] expected: &str,
|
||||
#[case] position: usize,
|
||||
@@ -1000,7 +1002,9 @@ mod tests {
|
||||
#[case::position_8("<---#####>", 8, 10)]
|
||||
#[case::position_9("<----####>", 9, 10)]
|
||||
#[case::position_one_out_of_bounds("<----####>", 10, 10)]
|
||||
fn render_scrollbar_vertical_rightl(
|
||||
#[case::position_few_out_of_bounds("<----####>", 15, 10)]
|
||||
#[case::position_very_many_out_of_bounds("<----####>", 500, 10)]
|
||||
fn render_scrollbar_vertical_right(
|
||||
#[case] expected: &str,
|
||||
#[case] position: usize,
|
||||
#[case] content_length: usize,
|
||||
@@ -1089,4 +1093,31 @@ mod tests {
|
||||
let mut state = ScrollbarState::new(10);
|
||||
scrollbar.render(zero_width_area, &mut buffer, &mut state);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::vertical_left(ScrollbarOrientation::VerticalLeft)]
|
||||
#[case::vertical_right(ScrollbarOrientation::VerticalRight)]
|
||||
#[case::horizontal_top(ScrollbarOrientation::HorizontalTop)]
|
||||
#[case::horizontal_bottom(ScrollbarOrientation::HorizontalBottom)]
|
||||
fn render_in_minimal_buffer(#[case] orientation: ScrollbarOrientation) {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
let scrollbar = Scrollbar::new(orientation);
|
||||
let mut state = ScrollbarState::new(10).position(5);
|
||||
// This should not panic, even if the buffer is too small to render the scrollbar.
|
||||
scrollbar.render(buffer.area, &mut buffer, &mut state);
|
||||
assert_eq!(buffer, Buffer::with_lines([" "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::vertical_left(ScrollbarOrientation::VerticalLeft)]
|
||||
#[case::vertical_right(ScrollbarOrientation::VerticalRight)]
|
||||
#[case::horizontal_top(ScrollbarOrientation::HorizontalTop)]
|
||||
#[case::horizontal_bottom(ScrollbarOrientation::HorizontalBottom)]
|
||||
fn render_in_zero_size_buffer(#[case] orientation: ScrollbarOrientation) {
|
||||
let mut buffer = Buffer::empty(Rect::ZERO);
|
||||
let scrollbar = Scrollbar::new(orientation);
|
||||
let mut state = ScrollbarState::new(10).position(5);
|
||||
// This should not panic, even if the buffer has zero size.
|
||||
scrollbar.render(buffer.area, &mut buffer, &mut state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -699,4 +699,25 @@ mod tests {
|
||||
.remove_modifier(Modifier::DIM)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_in_minimal_buffer() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
let sparkline = Sparkline::default()
|
||||
.data([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
|
||||
.max(10);
|
||||
// This should not panic, even if the buffer is too small to render the sparkline.
|
||||
sparkline.render(buffer.area, &mut buffer);
|
||||
assert_eq!(buffer, Buffer::with_lines([" "]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_in_zero_size_buffer() {
|
||||
let mut buffer = Buffer::empty(Rect::ZERO);
|
||||
let sparkline = Sparkline::default()
|
||||
.data([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
|
||||
.max(10);
|
||||
// This should not panic, even if the buffer has zero size.
|
||||
sparkline.render(buffer.area, &mut buffer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2273,4 +2273,33 @@ mod tests {
|
||||
let column_count = table.column_count();
|
||||
assert_eq!(column_count, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_in_minimal_buffer() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
let rows = vec![
|
||||
Row::new(vec!["Cell1", "Cell2", "Cell3"]),
|
||||
Row::new(vec!["Cell4", "Cell5", "Cell6"]),
|
||||
];
|
||||
let table = Table::new(rows, [Constraint::Length(10); 3])
|
||||
.header(Row::new(vec!["Header1", "Header2", "Header3"]))
|
||||
.footer(Row::new(vec!["Footer1", "Footer2", "Footer3"]));
|
||||
// This should not panic, even if the buffer is too small to render the table.
|
||||
Widget::render(table, buffer.area, &mut buffer);
|
||||
assert_eq!(buffer, Buffer::with_lines([" "]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_in_zero_size_buffer() {
|
||||
let mut buffer = Buffer::empty(Rect::ZERO);
|
||||
let rows = vec![
|
||||
Row::new(vec!["Cell1", "Cell2", "Cell3"]),
|
||||
Row::new(vec!["Cell4", "Cell5", "Cell6"]),
|
||||
];
|
||||
let table = Table::new(rows, [Constraint::Length(10); 3])
|
||||
.header(Row::new(vec!["Header1", "Header2", "Header3"]))
|
||||
.footer(Row::new(vec!["Footer1", "Footer2", "Footer3"]));
|
||||
// This should not panic, even if the buffer has zero size.
|
||||
Widget::render(table, buffer.area, &mut buffer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -676,4 +676,25 @@ mod tests {
|
||||
Style::default().black().on_white().bold().not_italic()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_in_minimal_buffer() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"])
|
||||
.select(1)
|
||||
.divider("|");
|
||||
// This should not panic, even if the buffer is too small to render the tabs.
|
||||
tabs.render(buffer.area, &mut buffer);
|
||||
assert_eq!(buffer, Buffer::with_lines([" "]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_in_zero_size_buffer() {
|
||||
let mut buffer = Buffer::empty(Rect::ZERO);
|
||||
let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"])
|
||||
.select(1)
|
||||
.divider("|");
|
||||
// This should not panic, even if the buffer has zero size.
|
||||
tabs.render(buffer.area, &mut buffer);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user