use crate::components::navigation::SelectionState;
use crate::components::{Box as RnkBox, Line, Span, Text};
use crate::core::{Color, Element, Style};
#[derive(Debug, Clone)]
pub struct ListItem {
pub content: Line,
pub style: Option<Style>,
}
impl ListItem {
pub fn new(content: impl Into<String>) -> Self {
Self {
content: Line::raw(content),
style: None,
}
}
pub fn from_line(line: Line) -> Self {
Self {
content: line,
style: None,
}
}
pub fn from_spans(spans: Vec<Span>) -> Self {
Self {
content: Line::from_spans(spans),
style: None,
}
}
pub fn style(mut self, style: Style) -> Self {
self.style = Some(style);
self
}
}
impl<T: Into<String>> From<T> for ListItem {
fn from(s: T) -> Self {
ListItem::new(s)
}
}
#[derive(Debug, Clone, Default)]
pub struct ListState {
pub selected: Option<usize>,
pub offset: usize,
}
impl ListState {
pub fn new() -> Self {
Self::default()
}
pub fn with_selected(selected: Option<usize>) -> Self {
Self {
selected,
offset: 0,
}
}
}
impl SelectionState for ListState {
fn selected(&self) -> Option<usize> {
self.selected
}
fn select(&mut self, index: Option<usize>) {
self.selected = index;
}
fn offset(&self) -> usize {
self.offset
}
fn set_offset(&mut self, offset: usize) {
self.offset = offset;
}
}
#[derive(Debug, Clone)]
pub struct List {
items: Vec<ListItem>,
highlight_style: Style,
highlight_symbol: Option<String>,
show_selection: bool,
key: Option<String>,
}
impl List {
pub fn new() -> Self {
Self {
items: Vec::new(),
highlight_style: Style::new(),
highlight_symbol: None,
show_selection: true,
key: None,
}
}
pub fn from_items<I, T>(items: I) -> Self
where
I: IntoIterator<Item = T>,
T: Into<ListItem>,
{
Self {
items: items.into_iter().map(|i| i.into()).collect(),
highlight_style: Style::new(),
highlight_symbol: None,
show_selection: true,
key: None,
}
}
pub fn item(mut self, item: impl Into<ListItem>) -> Self {
self.items.push(item.into());
self
}
pub fn items<I, T>(mut self, items: I) -> Self
where
I: IntoIterator<Item = T>,
T: Into<ListItem>,
{
self.items = items.into_iter().map(|i| i.into()).collect();
self
}
pub fn highlight_style(mut self, style: Style) -> Self {
self.highlight_style = style;
self
}
pub fn highlight_color(mut self, color: Color) -> Self {
self.highlight_style.color = Some(color);
self
}
pub fn highlight_bg(mut self, color: Color) -> Self {
self.highlight_style.background_color = Some(color);
self
}
pub fn highlight_symbol(mut self, symbol: impl Into<String>) -> Self {
self.highlight_symbol = Some(symbol.into());
self
}
pub fn show_selection(mut self, show: bool) -> Self {
self.show_selection = show;
self
}
pub fn key(mut self, key: impl Into<String>) -> Self {
self.key = Some(key.into());
self
}
pub fn len(&self) -> usize {
self.items.len()
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
pub fn render(self, state: &ListState) -> Element {
self.render_with_height(state, None)
}
pub fn render_with_height(self, state: &ListState, viewport_height: Option<usize>) -> Element {
let selected = state.selected;
let offset = state.offset;
let height = viewport_height.unwrap_or(self.items.len());
let symbol_width = self.highlight_symbol.as_ref().map(|s| s.len()).unwrap_or(0);
let visible_items: Vec<_> = self
.items
.iter()
.enumerate()
.skip(offset)
.take(height)
.collect();
let mut container = RnkBox::new().flex_direction(crate::core::FlexDirection::Column);
if let Some(key) = self.key {
container = container.key(key);
}
for (idx, item) in visible_items {
let is_selected = self.show_selection && selected == Some(idx);
let mut spans = Vec::new();
if let Some(ref symbol) = self.highlight_symbol {
if is_selected {
spans.push(Span::new(symbol.clone()));
} else {
spans.push(Span::new(" ".repeat(symbol_width)));
}
}
spans.extend(item.content.spans.iter().cloned());
let line = Line::from_spans(spans);
let mut text = Text::line(line);
if is_selected {
if let Some(color) = self.highlight_style.color {
text = text.color(color);
}
if let Some(bg) = self.highlight_style.background_color {
text = text.background(bg);
}
if self.highlight_style.bold {
text = text.bold();
}
if self.highlight_style.inverse {
text = text.inverse();
}
} else if let Some(ref item_style) = item.style
&& let Some(color) = item_style.color
{
text = text.color(color);
}
container = container.child(text.into_element());
}
container.into_element()
}
pub fn into_element(self) -> Element {
self.render(&ListState::new())
}
}
impl Default for List {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_list_item_creation() {
let item = ListItem::new("Test item");
assert_eq!(item.content.spans[0].content, "Test item");
}
#[test]
fn test_list_creation() {
let list = List::from_items(vec!["Item 1", "Item 2", "Item 3"]);
assert_eq!(list.len(), 3);
}
#[test]
fn test_list_state_navigation() {
let mut state = ListState::new();
state.select_next(5);
assert_eq!(state.selected, Some(0));
state.select_next(5);
assert_eq!(state.selected, Some(1));
state.select_previous(5);
assert_eq!(state.selected, Some(0));
state.select_previous(5);
assert_eq!(state.selected, Some(0)); }
#[test]
fn test_list_state_first_last() {
let mut state = ListState::new();
state.select_last(10);
assert_eq!(state.selected, Some(9));
state.select_first(10);
assert_eq!(state.selected, Some(0));
}
#[test]
fn test_scroll_to_selected() {
let mut state = ListState::with_selected(Some(15));
state.scroll_to_selected(10);
assert_eq!(state.offset, 6); }
}