use crate::core::buffer::Buffer;
use crate::core::color::Color;
use crate::core::rect::Rect;
use crate::style::Style;
use crate::widgets::paragraph::{Line, Span, Text};
use crate::widgets::Widget;
#[derive(Debug, Clone)]
pub struct ListItem {
pub content: String,
pub text: Text,
pub style: Style,
}
impl ListItem {
pub fn new(content: &str) -> Self {
Self {
content: content.to_string(),
text: Text::raw(content),
style: Style::new(),
}
}
pub fn from_text(text: Text) -> Self {
let content = text
.lines
.iter()
.flat_map(|line| line.spans.iter())
.map(|span| span.content.as_str())
.collect::<Vec<_>>()
.join("");
Self {
content,
text,
style: Style::new(),
}
}
pub fn with_line(line: Line) -> Self {
Self::from_text(Text::new(vec![line]))
}
pub fn with_style(mut self, style: Style) -> Self {
self.style = style;
for line in &mut self.text.lines {
for span in &mut line.spans {
span.style = style.merge(&span.style);
}
}
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct ListState {
pub selected: Option<usize>,
pub offset: usize,
}
impl ListState {
pub fn new() -> Self {
Self::default()
}
pub fn select(&mut self, selected: Option<usize>) {
self.selected = selected;
}
pub fn selected(&self) -> Option<usize> {
self.selected
}
pub fn offset(&self) -> usize {
self.offset
}
}
#[derive(Debug, Clone)]
pub struct List<'a> {
pub items: &'a [ListItem],
pub selected: Option<usize>,
pub highlight_style: Style,
pub highlight_symbol: &'a str,
}
impl<'a> List<'a> {
pub fn new(items: &'a [ListItem]) -> Self {
Self {
items,
selected: None,
highlight_style: Style::new().fg(Color::WHITE).bg(Color::rgb(31, 111, 235)),
highlight_symbol: "▸ ",
}
}
pub fn with_selected(mut self, selected: usize) -> Self {
self.selected = Some(selected);
self
}
pub fn with_highlight_style(mut self, style: Style) -> Self {
self.highlight_style = style;
self
}
pub fn with_highlight_symbol(mut self, symbol: &'a str) -> Self {
self.highlight_symbol = symbol;
self
}
pub fn render_stateful(&self, buffer: &mut Buffer, area: Rect, state: &mut ListState) {
if area.width == 0 || area.height == 0 || self.items.is_empty() {
return;
}
if state.selected.is_none() {
state.selected = self.selected;
}
if let Some(selected) = state.selected {
let selected = selected.min(self.items.len().saturating_sub(1));
state.selected = Some(selected);
if selected < state.offset {
state.offset = selected;
} else if selected >= state.offset + area.height as usize {
state.offset = selected + 1 - area.height as usize;
}
}
for row in 0..area.height as usize {
let item_idx = state.offset + row;
if item_idx >= self.items.len() {
break;
}
let y = area.y as usize + row;
let is_selected = state.selected == Some(item_idx);
let item = &self.items[item_idx];
let style = if is_selected {
item.style.merge(&self.highlight_style)
} else {
item.style
};
let fg = style.fg_or_default();
let bg = style.bg;
if is_selected {
buffer.fill(Rect::new(area.x, y as u16, area.width, 1), ' ', fg, bg);
buffer.set_str(area.x as usize, y, self.highlight_symbol, fg, bg);
}
let x = area.x as usize
+ if is_selected {
self.highlight_symbol.chars().count()
} else {
0
};
let width = area.right() as usize - x.min(area.right() as usize);
render_item_line(buffer, item, x, y, width, style);
}
}
}
impl<'a> Widget for List<'a> {
fn render(&self, buffer: &mut Buffer, area: Rect) {
let mut state = ListState {
selected: self.selected,
offset: 0,
};
self.render_stateful(buffer, area, &mut state);
}
}
fn render_item_line(
buffer: &mut Buffer,
item: &ListItem,
x: usize,
y: usize,
width: usize,
base: Style,
) {
let Some(line) = item.text.lines.first() else {
return;
};
let mut col = 0usize;
for span in &line.spans {
let style = base.merge(&span.style);
let fg = style.fg_or_default();
let bg = style.bg;
for ch in span.content.chars() {
if col >= width {
return;
}
buffer.set(
x + col,
y,
crate::core::buffer::Cell {
ch,
fg,
bg,
bold: style.bold,
italic: style.italic,
underlined: style.underlined,
},
);
col += 1;
}
}
}
pub fn rich_item(spans: Vec<Span>) -> ListItem {
ListItem::with_line(Line::new(spans))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stateful_list_keeps_selected_visible() {
let items = (0..10)
.map(|i| ListItem::new(&format!("item {i}")))
.collect::<Vec<_>>();
let list = List::new(&items);
let mut state = ListState::new();
state.select(Some(6));
let mut buffer = Buffer::new(12, 3);
list.render_stateful(&mut buffer, Rect::new(0, 0, 12, 3), &mut state);
assert_eq!(state.offset, 4);
assert_eq!(buffer.get(0, 2).unwrap().ch, '▸');
}
#[test]
fn list_renders_rich_item_span_color() {
let red = Color::rgb(255, 0, 0);
let items = [rich_item(vec![Span::styled("hot", Style::new().fg(red))])];
let mut buffer = Buffer::new(8, 1);
List::new(&items).render(&mut buffer, Rect::new(0, 0, 8, 1));
assert_eq!(buffer.get(0, 0).unwrap().fg, red);
}
}