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:
Jagoda Estera Ślązak
2025-07-20 21:39:18 +02:00
committed by GitHub
parent cfebd68e18
commit c1b8528b69
14 changed files with 355 additions and 4 deletions

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}