use alloc::vec::Vec;
use ratatui_core::style::{Style, Styled};
use ratatui_core::text::Line;
use strum::{Display, EnumString};
pub use self::item::ListItem;
pub use self::state::ListState;
use crate::block::Block;
use crate::table::HighlightSpacing;
mod item;
mod rendering;
mod state;
#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
pub struct List<'a> {
pub(crate) block: Option<Block<'a>>,
pub(crate) items: Vec<ListItem<'a>>,
pub(crate) style: Style,
pub(crate) direction: ListDirection,
pub(crate) highlight_style: Style,
pub(crate) highlight_symbol: Option<Line<'a>>,
pub(crate) repeat_highlight_symbol: bool,
pub(crate) highlight_spacing: HighlightSpacing,
pub(crate) scroll_padding: usize,
}
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ListDirection {
#[default]
TopToBottom,
BottomToTop,
}
impl<'a> List<'a> {
pub fn new<T>(items: T) -> Self
where
T: IntoIterator,
T::Item: Into<ListItem<'a>>,
{
Self {
block: None,
style: Style::default(),
items: items.into_iter().map(Into::into).collect(),
direction: ListDirection::default(),
..Self::default()
}
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn items<T>(mut self, items: T) -> Self
where
T: IntoIterator,
T::Item: Into<ListItem<'a>>,
{
self.items = items.into_iter().map(Into::into).collect();
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block);
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn highlight_symbol<L: Into<Line<'a>>>(mut self, highlight_symbol: L) -> Self {
self.highlight_symbol = Some(highlight_symbol.into());
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn highlight_style<S: Into<Style>>(mut self, style: S) -> Self {
self.highlight_style = style.into();
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn repeat_highlight_symbol(mut self, repeat: bool) -> Self {
self.repeat_highlight_symbol = repeat;
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn highlight_spacing(mut self, value: HighlightSpacing) -> Self {
self.highlight_spacing = value;
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn direction(mut self, direction: ListDirection) -> Self {
self.direction = direction;
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn scroll_padding(mut self, padding: usize) -> Self {
self.scroll_padding = padding;
self
}
pub fn len(&self) -> usize {
self.items.len()
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
}
impl Styled for List<'_> {
type Item = Self;
fn style(&self) -> Style {
self.style
}
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
self.style(style)
}
}
impl Styled for ListItem<'_> {
type Item = Self;
fn style(&self) -> Style {
self.style
}
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
self.style(style)
}
}
impl<'a, Item> FromIterator<Item> for List<'a>
where
Item: Into<ListItem<'a>>,
{
fn from_iter<Iter: IntoIterator<Item = Item>>(iter: Iter) -> Self {
Self::new(iter)
}
}
#[cfg(test)]
mod tests {
use alloc::{format, vec};
use pretty_assertions::assert_eq;
use ratatui_core::buffer::Buffer;
use ratatui_core::layout::Rect;
use ratatui_core::style::{Color, Modifier, Stylize};
use ratatui_core::text::{Text, ToSpan};
use ratatui_core::widgets::StatefulWidget;
use super::*;
#[test]
fn collect_list_from_iterator() {
let collected: List = (0..3).map(|i| format!("Item{i}")).collect();
let expected = List::new(["Item0", "Item1", "Item2"]);
assert_eq!(collected, expected);
}
#[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 no_style() {
let text = Text::from("Item 1");
let list = List::new([ListItem::new(text)])
.highlight_symbol(">>")
.highlight_spacing(HighlightSpacing::Always);
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 1));
list.render(buffer.area, &mut buffer, &mut ListState::default());
assert_eq!(buffer, Buffer::with_lines([" Item 1 "]));
}
#[test]
fn styled_text() {
let text = Text::from("Item 1").bold();
let list = List::new([ListItem::new(text)])
.highlight_symbol(">>")
.highlight_spacing(HighlightSpacing::Always);
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 1));
list.render(buffer.area, &mut buffer, &mut ListState::default());
assert_eq!(
buffer,
Buffer::with_lines([Line::from(vec![" ".to_span(), "Item 1 ".bold(),])])
);
}
#[test]
fn styled_list_item() {
let text = Text::from("Item 1");
let item = ListItem::new(text).style(Modifier::ITALIC);
let list = List::new([item])
.highlight_symbol(">>")
.highlight_spacing(HighlightSpacing::Always);
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 1));
list.render(buffer.area, &mut buffer, &mut ListState::default());
assert_eq!(
buffer,
Buffer::with_lines([Line::from_iter([" Item 1 ".italic()])])
);
}
#[test]
fn styled_text_and_list_item() {
let text = Text::from("Item 1").bold();
let item = ListItem::new(text).style(Modifier::ITALIC);
let list = List::new([item])
.highlight_symbol(">>")
.highlight_spacing(HighlightSpacing::Always);
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 1));
list.render(buffer.area, &mut buffer, &mut ListState::default());
assert_eq!(
buffer,
Buffer::with_lines([Line::from(vec![" ".italic(), "Item 1 ".bold().italic()])])
);
}
#[test]
fn styled_highlight() {
let text = Text::from("Item 1").bold();
let item = ListItem::new(text).style(Modifier::ITALIC);
let mut state = ListState::default().with_selected(Some(0));
let list = List::new([item])
.highlight_symbol(">>")
.highlight_style(Color::Red);
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 1));
list.render(buffer.area, &mut buffer, &mut state);
assert_eq!(
buffer,
Buffer::with_lines([Line::from(vec![
">>".italic().red(),
"Item 1 ".bold().italic().red(),
])])
);
}
#[test]
fn style_inheritance() {
let bold = Modifier::BOLD;
let italic = Modifier::ITALIC;
let items = [
ListItem::new(Text::raw("Item 1")), ListItem::new(Text::styled("Item 2", bold)), ListItem::new(Text::raw("Item 3")).style(italic), ListItem::new(Text::styled("Item 4", bold)).style(italic), ListItem::new(Text::styled("Item 5", bold)).style(italic), ];
let mut state = ListState::default().with_selected(Some(4));
let list = List::new(items)
.highlight_symbol(">>")
.highlight_style(Color::Red)
.style(Style::new().on_blue());
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 5));
list.render(buffer.area, &mut buffer, &mut state);
assert_eq!(
buffer,
Buffer::with_lines(vec![
vec![" Item 1 ".on_blue()],
vec![" ".on_blue(), "Item 2 ".bold().on_blue()],
vec![" Item 3 ".italic().on_blue()],
vec![
" ".italic().on_blue(),
"Item 4 ".bold().italic().on_blue(),
],
vec![
">>".italic().red().on_blue(),
"Item 5 ".bold().italic().red().on_blue(),
],
])
);
}
#[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);
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);
list.render(buffer.area, &mut buffer, &mut state);
}
}