use alloc::vec::Vec;
use core::hash::{Hash, Hasher};
use ratatui_core::layout::Rect;
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, BlockExt};
use crate::table::HighlightSpacing;
mod item;
mod rendering;
mod state;
#[derive(Debug, Clone)]
pub struct List<'a, Item, Message = ()>
where
Item: Clone + Into<ListItem<'a>>,
{
pub(crate) block: Option<Block<'a>>,
pub(crate) items: Vec<Item>,
pub(crate) list_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,
pub(crate) on_select: Option<fn(&Item) -> Message>,
pub(crate) focus: bool,
}
impl<'a, Item, Message> Default for List<'a, Item, Message>
where
Item: Clone + Into<ListItem<'a>>,
{
fn default() -> Self {
Self {
block: Default::default(),
items: Default::default(),
list_items: Default::default(),
style: Default::default(),
direction: Default::default(),
highlight_style: Default::default(),
highlight_symbol: Default::default(),
repeat_highlight_symbol: Default::default(),
highlight_spacing: Default::default(),
scroll_padding: Default::default(),
on_select: Default::default(),
focus: false,
}
}
}
impl<'a, Item, Message> PartialEq for List<'a, Item, Message>
where
Item: Clone + Into<ListItem<'a>> + PartialEq,
Message: PartialEq,
{
fn eq(&self, other: &Self) -> bool {
self.block == other.block
&& self.items == other.items
&& self.list_items == other.list_items
&& self.style == other.style
&& self.direction == other.direction
&& self.highlight_style == other.highlight_style
&& self.highlight_symbol == other.highlight_symbol
&& self.repeat_highlight_symbol == other.repeat_highlight_symbol
&& self.highlight_spacing == other.highlight_spacing
&& self.scroll_padding == other.scroll_padding
&& self.focus == other.focus
}
}
impl<'a, Item, Message> Eq for List<'a, Item, Message>
where
Item: Clone + Into<ListItem<'a>> + Eq,
Message: Eq,
{
}
impl<'a, Item, Message> Hash for List<'a, Item, Message>
where
Item: Clone + Into<ListItem<'a>> + Hash,
Message: Hash,
{
fn hash<H: Hasher>(&self, state: &mut H) {
self.block.hash(state);
self.items.hash(state);
self.list_items.hash(state);
self.style.hash(state);
self.direction.hash(state);
self.highlight_style.hash(state);
self.highlight_symbol.hash(state);
self.repeat_highlight_symbol.hash(state);
self.highlight_spacing.hash(state);
self.scroll_padding.hash(state);
}
}
#[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, Item, Message> List<'a, Item, Message>
where
Item: Clone + Into<ListItem<'a>>,
{
pub fn new(items: impl Into<Vec<Item>>) -> Self {
let items = items.into();
let list_items = items.iter().cloned().map(Item::into).collect();
Self {
block: None,
style: Style::default(),
items,
list_items,
direction: ListDirection::default(),
..Self::default()
}
}
pub fn items_layout(&self, area: Rect) -> Rect {
self.block.inner_if_some(area)
}
pub fn items_as_slice(&self) -> &[Item] {
self.items.as_slice()
}
pub fn direction_ref(&self) -> ListDirection {
self.direction
}
pub fn on_select_ref(&self) -> Option<fn(&Item) -> Message> {
self.on_select
}
pub fn on_select(mut self, f: fn(&Item) -> Message) -> Self {
self.on_select = Some(f);
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn items(mut self, items: impl Into<Vec<Item>>) -> Self {
self.items = items.into();
self.list_items = self.items.iter().cloned().map(Item::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 const fn len(&self) -> usize {
self.items.len()
}
pub const fn is_empty(&self) -> bool {
self.items.is_empty()
}
}
impl<'a, Item, Message> Styled for List<'a, Item, Message>
where
Item: Clone + Into<ListItem<'a>>,
{
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, Message> FromIterator<Item> for List<'a, Item, Message>
where
Item: Clone + Into<ListItem<'a>>,
{
fn from_iter<Iter: IntoIterator<Item = Item>>(iter: Iter) -> Self {
let items: Vec<Item> = iter.into_iter().collect();
Self::new(items)
}
}
#[cfg(test)]
mod tests {
use alloc::string::{String, ToString};
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<'_, String> = (0..3).map(|i| format!("Item{i}")).collect();
let expected: List<'_, String> = List::new(vec![
"Item0".to_string(),
"Item1".to_string(),
"Item2".to_string(),
]);
assert_eq!(collected, expected);
}
#[test]
fn can_be_stylized() {
assert_eq!(
List::<&str>::new(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);
}
}