use crate::widgets::get_scrolled_line;
use ratatui::style::Style;
use ratatui::widgets::{List, ListItem, ListState, StatefulWidget};
use std::borrow::Cow;
pub const DEFAULT_TICKER_GAP: u16 = 6;
#[derive(Debug, Default, Clone)]
pub struct ScrollingListState {
list_state: ListState,
last_scrolled_tick: u64,
}
impl ScrollingListState {
pub fn select(&mut self, index: Option<usize>, cur_tick: u64) {
if self.list_state.selected() != index {
self.last_scrolled_tick = cur_tick;
}
self.list_state.select(index);
}
}
pub struct ScrollingList<'a, I> {
items: I,
style: Style,
highlight_style: Style,
highlight_symbol: Option<&'a str>,
cur_tick: u64,
ticker_gap: u16,
max_times_to_scroll: Option<u16>,
}
impl<'a, I> ScrollingList<'a, I> {
pub fn new<II>(items: I, cur_tick: u64) -> ScrollingList<'a, I>
where
I: IntoIterator<Item = II> + 'a,
II: Into<Cow<'a, str>>,
{
Self {
items,
cur_tick,
ticker_gap: DEFAULT_TICKER_GAP,
max_times_to_scroll: None,
style: Default::default(),
highlight_style: Default::default(),
highlight_symbol: Default::default(),
}
}
#[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 fn ticker_gap(mut self, ticker_gap: u16) -> Self {
self.ticker_gap = ticker_gap;
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn max_times_to_scroll(mut self, max_times_to_scroll: Option<u16>) -> Self {
self.max_times_to_scroll = max_times_to_scroll;
self
}
}
impl<'a, I, II> StatefulWidget for ScrollingList<'a, I>
where
I: IntoIterator<Item = II> + 'a,
II: Into<Cow<'a, str>>,
{
type State = ScrollingListState;
fn render(
self,
area: ratatui::prelude::Rect,
buf: &mut ratatui::prelude::Buffer,
state: &mut Self::State,
) {
let Self {
items,
style,
highlight_style,
highlight_symbol,
cur_tick,
ticker_gap,
max_times_to_scroll,
} = self;
let cur_selected = state.list_state.selected();
let adj_tick = cur_tick.saturating_sub(state.last_scrolled_tick);
let items = items.into_iter().enumerate().map(|(idx, item)| {
let item: Cow<_> = item.into();
if Some(idx) == cur_selected {
return get_scrolled_line(
item,
adj_tick,
ticker_gap,
area.width,
max_times_to_scroll,
)
.into();
}
ListItem::from(item)
});
let list = List::new(items)
.style(style)
.highlight_style(highlight_style);
let list = if let Some(highlight_symbol) = highlight_symbol {
list.highlight_symbol(highlight_symbol)
} else {
list
};
list.render(area, buf, &mut state.list_state);
}
}
#[cfg(test)]
mod tests {
use crate::widgets::{ScrollingList, ScrollingListState};
use pretty_assertions::assert_eq;
use ratatui::layout::Rect;
use ratatui::widgets::StatefulWidget;
#[test]
fn test_basic_scrolling_list() {
let list_items = ["AA", "ABCD"];
let mut list_state = ScrollingListState::default();
list_state.select(Some(1), 0);
let area = Rect::new(0, 0, 3, 2);
let mut buf = ratatui::buffer::Buffer::empty(area);
let list_frame_1 = ScrollingList::new(list_items, 0).ticker_gap(1);
list_frame_1.render(area, &mut buf, &mut list_state);
let frame_1_cells_as_string = buf
.content
.iter()
.map(|cell| cell.symbol())
.collect::<String>();
let expected_frame_1_cells_as_string = "AA ABC".to_string();
assert_eq!(frame_1_cells_as_string, expected_frame_1_cells_as_string);
let list_frame_2 = ScrollingList::new(list_items, 1).ticker_gap(1);
list_frame_2.render(area, &mut buf, &mut list_state);
let frame_2_cells_as_string = buf
.content
.iter()
.map(|cell| cell.symbol())
.collect::<String>();
let expected_frame_2_cells_as_string = "AA BCD".to_string();
assert_eq!(frame_2_cells_as_string, expected_frame_2_cells_as_string);
let list_frame_3 = ScrollingList::new(list_items, 2).ticker_gap(1);
list_frame_3.render(area, &mut buf, &mut list_state);
let frame_3_cells_as_string = buf
.content
.iter()
.map(|cell| cell.symbol())
.collect::<String>();
let expected_frame_3_cells_as_string = "AA CD ".to_string();
assert_eq!(frame_3_cells_as_string, expected_frame_3_cells_as_string);
let list_frame_4 = ScrollingList::new(list_items, 3).ticker_gap(1);
list_frame_4.render(area, &mut buf, &mut list_state);
let frame_4_cells_as_string = buf
.content
.iter()
.map(|cell| cell.symbol())
.collect::<String>();
let expected_frame_4_cells_as_string = "AA D A".to_string();
assert_eq!(frame_4_cells_as_string, expected_frame_4_cells_as_string);
}
#[test]
fn test_max_times_to_scroll() {
let list_items = ["AA", "ABCD"];
let mut list_state = ScrollingListState::default();
list_state.select(Some(1), 0);
let area = Rect::new(0, 0, 3, 2);
let mut buf = ratatui::buffer::Buffer::empty(area);
let list_frame_6 = ScrollingList::new(list_items, 6)
.ticker_gap(1)
.max_times_to_scroll(Some(1));
list_frame_6.render(area, &mut buf, &mut list_state);
let frame_6_cells_as_string = buf
.content
.iter()
.map(|cell| cell.symbol())
.collect::<String>();
let expected_frame_6_cells_as_string = "AA ABC".to_string();
assert_eq!(frame_6_cells_as_string, expected_frame_6_cells_as_string);
let list_frame_6 = ScrollingList::new(list_items, 6).ticker_gap(1);
list_frame_6.render(area, &mut buf, &mut list_state);
let frame_6_cells_as_string = buf
.content
.iter()
.map(|cell| cell.symbol())
.collect::<String>();
let expected_frame_6_cells_as_string = "AA BCD".to_string();
assert_eq!(frame_6_cells_as_string, expected_frame_6_cells_as_string);
}
}