use unicode_width::UnicodeWidthStr;
use crate::{
buffer::Buffer,
layout::Rect,
widgets::{
block::BlockExt, List, ListDirection, ListState, StatefulWidget, StatefulWidgetRef, Widget,
WidgetRef,
},
};
impl Widget for List<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
WidgetRef::render_ref(&self, area, buf);
}
}
impl WidgetRef for List<'_> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
let mut state = ListState::default();
StatefulWidgetRef::render_ref(self, area, buf, &mut state);
}
}
impl StatefulWidget for List<'_> {
type State = ListState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
StatefulWidgetRef::render_ref(&self, area, buf, state);
}
}
impl StatefulWidget for &List<'_> {
type State = ListState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
StatefulWidgetRef::render_ref(self, area, buf, state);
}
}
impl StatefulWidgetRef for List<'_> {
type State = ListState;
fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
buf.set_style(area, self.style);
self.block.render_ref(area, buf);
let list_area = self.block.inner_if_some(area);
if list_area.is_empty() {
return;
}
if self.items.is_empty() {
state.select(None);
return;
}
if state.selected.is_some_and(|s| s >= self.items.len()) {
state.select(Some(self.items.len().saturating_sub(1)));
}
let list_height = list_area.height as usize;
let (first_visible_index, last_visible_index) =
self.get_items_bounds(state.selected, state.offset, list_height);
state.offset = first_visible_index;
let highlight_symbol = self.highlight_symbol.unwrap_or("");
let blank_symbol = " ".repeat(highlight_symbol.width());
let mut current_height = 0;
let selection_spacing = self.highlight_spacing.should_add(state.selected.is_some());
for (i, item) in self
.items
.iter()
.enumerate()
.skip(state.offset)
.take(last_visible_index - first_visible_index)
{
let (x, y) = if self.direction == ListDirection::BottomToTop {
current_height += item.height() as u16;
(list_area.left(), list_area.bottom() - current_height)
} else {
let pos = (list_area.left(), list_area.top() + current_height);
current_height += item.height() as u16;
pos
};
let row_area = Rect {
x,
y,
width: list_area.width,
height: item.height() as u16,
};
let item_style = self.style.patch(item.style);
buf.set_style(row_area, item_style);
let is_selected = state.selected.map_or(false, |s| s == i);
let item_area = if selection_spacing {
let highlight_symbol_width = self.highlight_symbol.unwrap_or("").width() as u16;
Rect {
x: row_area.x + highlight_symbol_width,
width: row_area.width.saturating_sub(highlight_symbol_width),
..row_area
}
} else {
row_area
};
item.content.render_ref(item_area, buf);
for j in 0..item.content.height() {
let symbol = if is_selected && (j == 0 || self.repeat_highlight_symbol) {
highlight_symbol
} else {
&blank_symbol
};
if selection_spacing {
buf.set_stringn(
x,
y + j as u16,
symbol,
list_area.width as usize,
item_style,
);
}
}
if is_selected {
buf.set_style(row_area, self.highlight_style);
}
}
}
}
impl List<'_> {
fn get_items_bounds(
&self,
selected: Option<usize>,
offset: usize,
max_height: usize,
) -> (usize, usize) {
let offset = offset.min(self.items.len().saturating_sub(1));
let mut first_visible_index = offset;
let mut last_visible_index = offset;
let mut height_from_offset = 0;
for item in self.items.iter().skip(offset) {
if height_from_offset + item.height() > max_height {
break;
}
height_from_offset += item.height();
last_visible_index += 1;
}
let index_to_display = self
.apply_scroll_padding_to_selected_index(
selected,
max_height,
first_visible_index,
last_visible_index,
)
.unwrap_or(offset);
while index_to_display >= last_visible_index {
height_from_offset =
height_from_offset.saturating_add(self.items[last_visible_index].height());
last_visible_index += 1;
while height_from_offset > max_height {
height_from_offset =
height_from_offset.saturating_sub(self.items[first_visible_index].height());
first_visible_index += 1;
}
}
while index_to_display < first_visible_index {
first_visible_index -= 1;
height_from_offset =
height_from_offset.saturating_add(self.items[first_visible_index].height());
while height_from_offset > max_height {
last_visible_index -= 1;
height_from_offset =
height_from_offset.saturating_sub(self.items[last_visible_index].height());
}
}
(first_visible_index, last_visible_index)
}
fn apply_scroll_padding_to_selected_index(
&self,
selected: Option<usize>,
max_height: usize,
first_visible_index: usize,
last_visible_index: usize,
) -> Option<usize> {
let last_valid_index = self.items.len().saturating_sub(1);
let selected = selected?.min(last_valid_index);
let mut scroll_padding = self.scroll_padding;
while scroll_padding > 0 {
let mut height_around_selected = 0;
for index in selected.saturating_sub(scroll_padding)
..=selected
.saturating_add(scroll_padding)
.min(last_valid_index)
{
height_around_selected += self.items[index].height();
}
if height_around_selected <= max_height {
break;
}
scroll_padding -= 1;
}
Some(
if (selected + scroll_padding).min(last_valid_index) >= last_visible_index {
selected + scroll_padding
} else if selected.saturating_sub(scroll_padding) < first_visible_index {
selected.saturating_sub(scroll_padding)
} else {
selected
}
.min(last_valid_index),
)
}
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use rstest::{fixture, rstest};
use super::*;
use crate::{
backend,
layout::{Alignment, Rect},
style::{Color, Modifier, Style, Stylize},
text::Line,
widgets::{Block, HighlightSpacing, ListItem, StatefulWidget, Widget},
Terminal,
};
#[fixture]
fn single_line_buf() -> Buffer {
Buffer::empty(Rect::new(0, 0, 10, 1))
}
#[rstest]
fn empty_list(mut single_line_buf: Buffer) {
let mut state = ListState::default();
let items: Vec<ListItem> = Vec::new();
let list = List::new(items);
state.select_first();
StatefulWidget::render(list, single_line_buf.area, &mut single_line_buf, &mut state);
assert_eq!(state.selected, None);
}
#[rstest]
fn single_item(mut single_line_buf: Buffer) {
let mut state = ListState::default();
let items = vec![ListItem::new("Item 1")];
let list = List::new(items);
state.select_first();
StatefulWidget::render(
&list,
single_line_buf.area,
&mut single_line_buf,
&mut state,
);
assert_eq!(state.selected, Some(0));
state.select_last();
StatefulWidget::render(
&list,
single_line_buf.area,
&mut single_line_buf,
&mut state,
);
assert_eq!(state.selected, Some(0));
state.select_previous();
StatefulWidget::render(
&list,
single_line_buf.area,
&mut single_line_buf,
&mut state,
);
assert_eq!(state.selected, Some(0));
state.select_next();
StatefulWidget::render(
&list,
single_line_buf.area,
&mut single_line_buf,
&mut state,
);
assert_eq!(state.selected, Some(0));
}
fn widget(widget: List<'_>, width: u16, height: u16) -> Buffer {
let mut buffer = Buffer::empty(Rect::new(0, 0, width, height));
Widget::render(widget, buffer.area, &mut buffer);
buffer
}
fn stateful_widget(widget: List<'_>, state: &mut ListState, width: u16, height: u16) -> Buffer {
let mut buffer = Buffer::empty(Rect::new(0, 0, width, height));
StatefulWidget::render(widget, buffer.area, &mut buffer, state);
buffer
}
#[test]
fn does_not_render_in_small_space() {
let items = vec!["Item 0", "Item 1", "Item 2"];
let list = List::new(items.clone()).highlight_symbol(">>");
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
Widget::render(list.clone(), Rect::new(0, 0, 0, 3), &mut buffer);
assert_eq!(&buffer, &Buffer::empty(buffer.area));
Widget::render(list.clone(), Rect::new(0, 0, 15, 0), &mut buffer);
assert_eq!(&buffer, &Buffer::empty(buffer.area));
let list = List::new(items)
.highlight_symbol(">>")
.block(Block::bordered());
Widget::render(list, Rect::new(0, 0, 15, 2), &mut buffer);
#[rustfmt::skip]
let expected = Buffer::with_lines([
"┌─────────────┐",
"└─────────────┘",
" ",
]);
assert_eq!(buffer, expected,);
}
#[allow(clippy::too_many_lines)]
#[test]
fn combinations() {
#[track_caller]
fn test_case_render<'line, Lines>(items: &[ListItem], expected: Lines)
where
Lines: IntoIterator,
Lines::Item: Into<Line<'line>>,
{
let list = List::new(items.to_owned()).highlight_symbol(">>");
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 5));
Widget::render(list, buffer.area, &mut buffer);
assert_eq!(buffer, Buffer::with_lines(expected));
}
#[track_caller]
fn test_case_render_stateful<'line, Lines>(
items: &[ListItem],
selected: Option<usize>,
expected: Lines,
) where
Lines: IntoIterator,
Lines::Item: Into<Line<'line>>,
{
let list = List::new(items.to_owned()).highlight_symbol(">>");
let mut state = ListState::default().with_selected(selected);
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 5));
StatefulWidget::render(list, buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines(expected));
}
let empty_items = Vec::new();
let single_item = vec!["Item 0".into()];
let multiple_items = vec!["Item 0".into(), "Item 1".into(), "Item 2".into()];
let multi_line_items = vec!["Item 0\nLine 2".into(), "Item 1".into(), "Item 2".into()];
test_case_render(
&empty_items,
[
" ",
" ",
" ",
" ",
" ",
],
);
test_case_render_stateful(
&empty_items,
None,
[
" ",
" ",
" ",
" ",
" ",
],
);
test_case_render_stateful(
&empty_items,
Some(0),
[
" ",
" ",
" ",
" ",
" ",
],
);
test_case_render(
&single_item,
[
"Item 0 ",
" ",
" ",
" ",
" ",
],
);
test_case_render_stateful(
&single_item,
None,
[
"Item 0 ",
" ",
" ",
" ",
" ",
],
);
test_case_render_stateful(
&single_item,
Some(0),
[
">>Item 0 ",
" ",
" ",
" ",
" ",
],
);
test_case_render_stateful(
&single_item,
Some(1),
[
">>Item 0 ",
" ",
" ",
" ",
" ",
],
);
test_case_render(
&multiple_items,
[
"Item 0 ",
"Item 1 ",
"Item 2 ",
" ",
" ",
],
);
test_case_render_stateful(
&multiple_items,
None,
[
"Item 0 ",
"Item 1 ",
"Item 2 ",
" ",
" ",
],
);
test_case_render_stateful(
&multiple_items,
Some(0),
[
">>Item 0 ",
" Item 1 ",
" Item 2 ",
" ",
" ",
],
);
test_case_render_stateful(
&multiple_items,
Some(1),
[
" Item 0 ",
">>Item 1 ",
" Item 2 ",
" ",
" ",
],
);
test_case_render_stateful(
&multiple_items,
Some(3),
[
" Item 0 ",
" Item 1 ",
">>Item 2 ",
" ",
" ",
],
);
test_case_render(
&multi_line_items,
[
"Item 0 ",
"Line 2 ",
"Item 1 ",
"Item 2 ",
" ",
],
);
test_case_render_stateful(
&multi_line_items,
None,
[
"Item 0 ",
"Line 2 ",
"Item 1 ",
"Item 2 ",
" ",
],
);
test_case_render_stateful(
&multi_line_items,
Some(0),
[
">>Item 0 ",
" Line 2 ",
" Item 1 ",
" Item 2 ",
" ",
],
);
test_case_render_stateful(
&multi_line_items,
Some(1),
[
" Item 0 ",
" Line 2 ",
">>Item 1 ",
" Item 2 ",
" ",
],
);
}
#[test]
fn items() {
let list = List::default().items(["Item 0", "Item 1", "Item 2"]);
let buffer = widget(list, 10, 5);
let expected = Buffer::with_lines([
"Item 0 ",
"Item 1 ",
"Item 2 ",
" ",
" ",
]);
assert_eq!(buffer, expected);
}
#[test]
fn empty_strings() {
let list = List::new(["Item 0", "", "", "Item 1", "Item 2"])
.block(Block::bordered().title("List"));
let buffer = widget(list, 10, 7);
let expected = Buffer::with_lines([
"┌List────┐",
"│Item 0 │",
"│ │",
"│ │",
"│Item 1 │",
"│Item 2 │",
"└────────┘",
]);
assert_eq!(buffer, expected);
}
#[test]
fn block() {
let list = List::new(["Item 0", "Item 1", "Item 2"]).block(Block::bordered().title("List"));
let buffer = widget(list, 10, 7);
let expected = Buffer::with_lines([
"┌List────┐",
"│Item 0 │",
"│Item 1 │",
"│Item 2 │",
"│ │",
"│ │",
"└────────┘",
]);
assert_eq!(buffer, expected);
}
#[test]
fn style() {
let list = List::new(["Item 0", "Item 1", "Item 2"]).style(Style::default().fg(Color::Red));
let buffer = widget(list, 10, 5);
let expected = Buffer::with_lines([
"Item 0 ".red(),
"Item 1 ".red(),
"Item 2 ".red(),
" ".red(),
" ".red(),
]);
assert_eq!(buffer, expected);
}
#[test]
fn highlight_symbol_and_style() {
let list = List::new(["Item 0", "Item 1", "Item 2"])
.highlight_symbol(">>")
.highlight_style(Style::default().fg(Color::Yellow));
let mut state = ListState::default();
state.select(Some(1));
let buffer = stateful_widget(list, &mut state, 10, 5);
let expected = Buffer::with_lines([
" Item 0 ".into(),
">>Item 1 ".yellow(),
" Item 2 ".into(),
" ".into(),
" ".into(),
]);
assert_eq!(buffer, expected);
}
#[test]
fn highlight_spacing_default_when_selected() {
{
let list = List::new(["Item 0", "Item 1", "Item 2"]).highlight_symbol(">>");
let mut state = ListState::default();
let buffer = stateful_widget(list, &mut state, 10, 5);
let expected = Buffer::with_lines([
"Item 0 ",
"Item 1 ",
"Item 2 ",
" ",
" ",
]);
assert_eq!(buffer, expected);
}
{
let list = List::new(["Item 0", "Item 1", "Item 2"]).highlight_symbol(">>");
let mut state = ListState::default();
state.select(Some(1));
let buffer = stateful_widget(list, &mut state, 10, 5);
let expected = Buffer::with_lines([
" Item 0 ",
">>Item 1 ",
" Item 2 ",
" ",
" ",
]);
assert_eq!(buffer, expected);
}
}
#[test]
fn highlight_spacing_default_always() {
{
let list = List::new(["Item 0", "Item 1", "Item 2"])
.highlight_symbol(">>")
.highlight_spacing(HighlightSpacing::Always);
let mut state = ListState::default();
let buffer = stateful_widget(list, &mut state, 10, 5);
let expected = Buffer::with_lines([
" Item 0 ",
" Item 1 ",
" Item 2 ",
" ",
" ",
]);
assert_eq!(buffer, expected);
}
{
let list = List::new(["Item 0", "Item 1", "Item 2"])
.highlight_symbol(">>")
.highlight_spacing(HighlightSpacing::Always);
let mut state = ListState::default();
state.select(Some(1));
let buffer = stateful_widget(list, &mut state, 10, 5);
let expected = Buffer::with_lines([
" Item 0 ",
">>Item 1 ",
" Item 2 ",
" ",
" ",
]);
assert_eq!(buffer, expected);
}
}
#[test]
fn highlight_spacing_default_never() {
{
let list = List::new(["Item 0", "Item 1", "Item 2"])
.highlight_symbol(">>")
.highlight_spacing(HighlightSpacing::Never);
let mut state = ListState::default();
let buffer = stateful_widget(list, &mut state, 10, 5);
let expected = Buffer::with_lines([
"Item 0 ",
"Item 1 ",
"Item 2 ",
" ",
" ",
]);
assert_eq!(buffer, expected);
}
{
let list = List::new(["Item 0", "Item 1", "Item 2"])
.highlight_symbol(">>")
.highlight_spacing(HighlightSpacing::Never);
let mut state = ListState::default();
state.select(Some(1));
let buffer = stateful_widget(list, &mut state, 10, 5);
let expected = Buffer::with_lines([
"Item 0 ",
"Item 1 ",
"Item 2 ",
" ",
" ",
]);
assert_eq!(buffer, expected);
}
}
#[test]
fn repeat_highlight_symbol() {
let list = List::new(["Item 0\nLine 2", "Item 1", "Item 2"])
.highlight_symbol(">>")
.highlight_style(Style::default().fg(Color::Yellow))
.repeat_highlight_symbol(true);
let mut state = ListState::default();
state.select(Some(0));
let buffer = stateful_widget(list, &mut state, 10, 5);
let expected = Buffer::with_lines([
">>Item 0 ".yellow(),
">>Line 2 ".yellow(),
" Item 1 ".into(),
" Item 2 ".into(),
" ".into(),
]);
assert_eq!(buffer, expected);
}
#[rstest]
#[case::top_to_bottom(ListDirection::TopToBottom, [
"Item 0 ",
"Item 1 ",
"Item 2 ",
" ",
])]
#[case::top_to_bottom(ListDirection::BottomToTop, [
" ",
"Item 2 ",
"Item 1 ",
"Item 0 ",
])]
fn list_direction<'line, Lines>(#[case] direction: ListDirection, #[case] expected: Lines)
where
Lines: IntoIterator,
Lines::Item: Into<Line<'line>>,
{
let list = List::new(["Item 0", "Item 1", "Item 2"]).direction(direction);
let buffer = widget(list, 10, 4);
assert_eq!(buffer, Buffer::with_lines(expected));
}
#[test]
fn truncate_items() {
let list = List::new(["Item 0", "Item 1", "Item 2", "Item 3", "Item 4"]);
let buffer = widget(list, 10, 3);
#[rustfmt::skip]
let expected = Buffer::with_lines([
"Item 0 ",
"Item 1 ",
"Item 2 ",
]);
assert_eq!(buffer, expected);
}
#[test]
fn offset_renders_shifted() {
let list = List::new([
"Item 0", "Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6",
]);
let mut state = ListState::default().with_offset(3);
let buffer = stateful_widget(list, &mut state, 6, 3);
let expected = Buffer::with_lines(["Item 3", "Item 4", "Item 5"]);
assert_eq!(buffer, expected);
}
#[rstest]
#[case(None, [
"Item 0 with a v",
"Item 1 ",
"Item 2 ",
])]
#[case(Some(0), [
">>Item 0 with a",
" Item 1 ",
" Item 2 ",
])]
fn long_lines<'line, Lines>(#[case] selected: Option<usize>, #[case] expected: Lines)
where
Lines: IntoIterator,
Lines::Item: Into<Line<'line>>,
{
let items = [
"Item 0 with a very long line that will be truncated",
"Item 1",
"Item 2",
];
let list = List::new(items).highlight_symbol(">>");
let mut state = ListState::default().with_selected(selected);
let buffer = stateful_widget(list, &mut state, 15, 3);
assert_eq!(buffer, Buffer::with_lines(expected));
}
#[test]
fn selected_item_ensures_selected_item_is_visible_when_offset_is_before_visible_range() {
let items = [
"Item 0", "Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6",
];
let list = List::new(items).highlight_symbol(">>");
let mut state = ListState::default().with_selected(Some(1)).with_offset(3);
let buffer = stateful_widget(list, &mut state, 10, 3);
#[rustfmt::skip]
let expected = Buffer::with_lines([
">>Item 1 ",
" Item 2 ",
" Item 3 ",
]);
assert_eq!(buffer, expected);
assert_eq!(state.selected, Some(1));
assert_eq!(
state.offset, 1,
"did not scroll the selected item into view"
);
}
#[test]
fn selected_item_ensures_selected_item_is_visible_when_offset_is_after_visible_range() {
let items = [
"Item 0", "Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6",
];
let list = List::new(items).highlight_symbol(">>");
let mut state = ListState::default().with_selected(Some(6)).with_offset(3);
let buffer = stateful_widget(list, &mut state, 10, 3);
#[rustfmt::skip]
let expected = Buffer::with_lines([
" Item 4 ",
" Item 5 ",
">>Item 6 ",
]);
assert_eq!(buffer, expected);
assert_eq!(state.selected, Some(6));
assert_eq!(
state.offset, 4,
"did not scroll the selected item into view"
);
}
#[test]
fn can_be_stylized() {
assert_eq!(
List::new::<Vec<&str>>(vec![])
.black()
.on_white()
.bold()
.not_dim()
.style,
Style::default()
.fg(Color::Black)
.bg(Color::White)
.add_modifier(Modifier::BOLD)
.remove_modifier(Modifier::DIM)
);
}
#[test]
fn with_alignment() {
let list = List::new([
Line::from("Left").alignment(Alignment::Left),
Line::from("Center").alignment(Alignment::Center),
Line::from("Right").alignment(Alignment::Right),
]);
let buffer = widget(list, 10, 4);
let expected = Buffer::with_lines(["Left ", " Center ", " Right", ""]);
assert_eq!(buffer, expected);
}
#[test]
fn alignment_odd_line_odd_area() {
let list = List::new([
Line::from("Odd").alignment(Alignment::Left),
Line::from("Even").alignment(Alignment::Center),
Line::from("Width").alignment(Alignment::Right),
]);
let buffer = widget(list, 7, 4);
let expected = Buffer::with_lines(["Odd ", " Even ", " Width", ""]);
assert_eq!(buffer, expected);
}
#[test]
fn alignment_even_line_even_area() {
let list = List::new([
Line::from("Odd").alignment(Alignment::Left),
Line::from("Even").alignment(Alignment::Center),
Line::from("Width").alignment(Alignment::Right),
]);
let buffer = widget(list, 6, 4);
let expected = Buffer::with_lines(["Odd ", " Even ", " Width", ""]);
assert_eq!(buffer, expected);
}
#[test]
fn alignment_odd_line_even_area() {
let list = List::new([
Line::from("Odd").alignment(Alignment::Left),
Line::from("Even").alignment(Alignment::Center),
Line::from("Width").alignment(Alignment::Right),
]);
let buffer = widget(list, 8, 4);
let expected = Buffer::with_lines(["Odd ", " Even ", " Width", ""]);
assert_eq!(buffer, expected);
}
#[test]
fn alignment_even_line_odd_area() {
let list = List::new([
Line::from("Odd").alignment(Alignment::Left),
Line::from("Even").alignment(Alignment::Center),
Line::from("Width").alignment(Alignment::Right),
]);
let buffer = widget(list, 6, 4);
let expected = Buffer::with_lines(["Odd ", " Even ", " Width", ""]);
assert_eq!(buffer, expected);
}
#[test]
fn alignment_zero_line_width() {
let list = List::new([Line::from("This line has zero width").alignment(Alignment::Center)]);
let buffer = widget(list, 0, 2);
assert_eq!(buffer, Buffer::with_lines([""; 2]));
}
#[test]
fn alignment_zero_area_width() {
let list = List::new([Line::from("Text").alignment(Alignment::Left)]);
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 1));
Widget::render(list, Rect::new(0, 0, 4, 0), &mut buffer);
assert_eq!(buffer, Buffer::with_lines([" "]));
}
#[test]
fn alignment_line_less_than_width() {
let list = List::new([Line::from("Small").alignment(Alignment::Center)]);
let buffer = widget(list, 10, 2);
let expected = Buffer::with_lines([" Small ", ""]);
assert_eq!(buffer, expected);
}
#[test]
fn alignment_line_equal_to_width() {
let list = List::new([Line::from("Exact").alignment(Alignment::Left)]);
let buffer = widget(list, 5, 2);
assert_eq!(buffer, Buffer::with_lines(["Exact", ""]));
}
#[test]
fn alignment_line_greater_than_width() {
let list = List::new([Line::from("Large line").alignment(Alignment::Left)]);
let buffer = widget(list, 5, 2);
assert_eq!(buffer, Buffer::with_lines(["Large", ""]));
}
#[rstest]
#[case::no_padding(
4,
2, 0, Some(2), [
">> Item 2 ",
" Item 3 ",
" Item 4 ",
" Item 5 ",
]
)]
#[case::one_before(
4,
2, 1, Some(2), [
" Item 1 ",
">> Item 2 ",
" Item 3 ",
" Item 4 ",
]
)]
#[case::one_after(
4,
1, 1, Some(4), [
" Item 2 ",
" Item 3 ",
">> Item 4 ",
" Item 5 ",
]
)]
#[case::check_padding_overflow(
4,
1, 2, Some(4), [
" Item 2 ",
" Item 3 ",
">> Item 4 ",
" Item 5 ",
]
)]
#[case::no_padding_offset_behavior(
5, 2, 0, Some(3), [
" Item 2 ",
">> Item 3 ",
" Item 4 ",
" Item 5 ",
" ",
]
)]
#[case::two_before(
5, 2, 2, Some(3), [
" Item 1 ",
" Item 2 ",
">> Item 3 ",
" Item 4 ",
" Item 5 ",
]
)]
#[case::keep_selected_visible(
4,
0, 4, Some(1), [
" Item 0 ",
">> Item 1 ",
" Item 2 ",
" Item 3 ",
]
)]
fn with_padding<'line, Lines>(
#[case] render_height: u16,
#[case] offset: usize,
#[case] padding: usize,
#[case] selected: Option<usize>,
#[case] expected: Lines,
) where
Lines: IntoIterator,
Lines::Item: Into<Line<'line>>,
{
let backend = backend::TestBackend::new(10, render_height);
let mut terminal = Terminal::new(backend).unwrap();
let mut state = ListState::default();
*state.offset_mut() = offset;
state.select(selected);
let list = List::new(["Item 0", "Item 1", "Item 2", "Item 3", "Item 4", "Item 5"])
.scroll_padding(padding)
.highlight_symbol(">> ");
terminal
.draw(|f| f.render_stateful_widget(list, f.area(), &mut state))
.unwrap();
terminal.backend().assert_buffer_lines(expected);
}
#[test]
fn padding_flicker() {
let backend = backend::TestBackend::new(10, 5);
let mut terminal = Terminal::new(backend).unwrap();
let mut state = ListState::default();
*state.offset_mut() = 2;
state.select(Some(4));
let items = [
"Item 0", "Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6", "Item 7",
];
let list = List::new(items).scroll_padding(3).highlight_symbol(">> ");
terminal
.draw(|f| f.render_stateful_widget(&list, f.area(), &mut state))
.unwrap();
let offset_after_render = state.offset();
terminal
.draw(|f| f.render_stateful_widget(&list, f.area(), &mut state))
.unwrap();
assert_eq!(offset_after_render, state.offset());
}
#[test]
fn padding_inconsistent_item_sizes() {
let backend = backend::TestBackend::new(10, 3);
let mut terminal = Terminal::new(backend).unwrap();
let mut state = ListState::default().with_offset(0).with_selected(Some(3));
let items = [
ListItem::new("Item 0"),
ListItem::new("Item 1"),
ListItem::new("Item 2"),
ListItem::new("Item 3"),
ListItem::new("Item 4\nTest\nTest"),
ListItem::new("Item 5"),
];
let list = List::new(items).scroll_padding(1).highlight_symbol(">> ");
terminal
.draw(|f| f.render_stateful_widget(list, f.area(), &mut state))
.unwrap();
#[rustfmt::skip]
let expected = [
" Item 1 ",
" Item 2 ",
">> Item 3 ",
];
terminal.backend().assert_buffer_lines(expected);
}
#[test]
fn padding_offset_pushback_break() {
let backend = backend::TestBackend::new(10, 4);
let mut terminal = Terminal::new(backend).unwrap();
let mut state = ListState::default();
*state.offset_mut() = 1;
state.select(Some(2));
let items = [
ListItem::new("Item 0\nTest\nTest"),
ListItem::new("Item 1"),
ListItem::new("Item 2"),
ListItem::new("Item 3"),
];
let list = List::new(items).scroll_padding(2).highlight_symbol(">> ");
terminal
.draw(|f| f.render_stateful_widget(list, f.area(), &mut state))
.unwrap();
terminal.backend().assert_buffer_lines([
" Item 1 ",
">> Item 2 ",
" Item 3 ",
" ",
]);
}
#[rstest]
#[case::under(">>>>", "Item1", ">>>>Item1 ")] #[case::exact(">>>>>", "Item1", ">>>>>Item1")] #[case::overflow(">>>>>>", "Item1", ">>>>>>Item")] fn highlight_symbol_overflow(
#[case] highlight_symbol: &str,
#[case] item: &str,
#[case] expected: &str,
mut single_line_buf: Buffer,
) {
let list = List::new([item]).highlight_symbol(highlight_symbol);
let mut state = ListState::default();
state.select(Some(0));
StatefulWidget::render(list, single_line_buf.area, &mut single_line_buf, &mut state);
assert_eq!(single_line_buf, Buffer::with_lines([expected]));
}
}