use super::keys::ListKeyMap;
use super::style::ListStyles;
use super::types::{FilterState, FilteredItem, Item, ItemDelegate};
use crate::{help, paginator, spinner, textinput};
pub struct Model<I: Item> {
pub(super) title: String,
pub(super) items: Vec<I>,
pub(super) delegate: Box<dyn ItemDelegate<I> + Send + Sync>,
pub(super) paginator: paginator::Model,
pub(super) per_page: usize,
pub(super) show_title: bool,
#[allow(dead_code)]
pub(super) spinner: spinner::Model,
pub(super) show_spinner: bool,
pub(super) width: usize,
pub(super) height: usize,
pub(super) styles: ListStyles,
pub(super) show_status_bar: bool,
#[allow(dead_code)]
pub(super) status_message_lifetime: usize,
pub(super) status_item_singular: Option<String>,
pub(super) status_item_plural: Option<String>,
pub(super) show_pagination: bool,
pub(super) help: help::Model,
pub(super) show_help: bool,
pub(super) keymap: ListKeyMap,
pub(super) filter_state: FilterState,
pub(super) filtered_items: Vec<FilteredItem<I>>,
pub(super) cursor: usize,
pub(super) viewport_start: usize,
pub(super) filter_input: textinput::Model,
}
impl<I: Item + Send + Sync + 'static> Model<I> {
pub fn new<D>(items: Vec<I>, delegate: D, width: usize, height: usize) -> Self
where
D: ItemDelegate<I> + Send + Sync + 'static,
{
let styles = ListStyles::default();
let mut paginator = paginator::Model::new();
let per_page = 10;
paginator.set_per_page(per_page);
paginator.set_total_items(items.len());
paginator.paginator_type = paginator::Type::Dots;
paginator.active_dot = styles.active_pagination_dot.render("");
paginator.inactive_dot = styles.inactive_pagination_dot.render("");
let mut list = Self {
title: "List".to_string(),
items,
delegate: Box::new(delegate),
paginator,
per_page,
show_title: true,
spinner: spinner::new(&[]),
show_spinner: false,
width,
height,
styles,
show_status_bar: true,
status_message_lifetime: 1,
status_item_singular: None,
status_item_plural: None,
show_pagination: true,
help: help::Model::new(),
show_help: true,
keymap: ListKeyMap::default(),
filter_state: FilterState::Unfiltered,
filtered_items: vec![],
cursor: 0,
viewport_start: 0,
filter_input: textinput::new(),
};
list.update_pagination();
list
}
pub fn set_items(&mut self, items: Vec<I>) {
self.items = items;
self.cursor = 0;
self.update_pagination();
}
pub fn visible_items(&self) -> Vec<I> {
if self.filter_state == FilterState::Unfiltered {
self.items.clone()
} else {
self.filtered_items
.iter()
.map(|fi| fi.item.clone())
.collect()
}
}
pub fn set_filter_text(&mut self, s: &str) {
self.filter_input.set_value(s);
}
pub fn set_filter_state(&mut self, st: FilterState) {
self.filter_state = st;
}
pub fn set_status_bar_item_name(&mut self, singular: &str, plural: &str) {
self.status_item_singular = Some(singular.to_string());
self.status_item_plural = Some(plural.to_string());
}
pub fn set_size(&mut self, width: usize, height: usize) {
self.width = width;
self.height = height;
self.update_pagination(); }
pub fn width(&self) -> usize {
self.width
}
pub fn height(&self) -> usize {
self.height
}
pub fn per_page(&self) -> usize {
self.per_page
}
pub fn total_pages(&self) -> usize {
self.paginator.total_pages
}
pub fn calculate_element_height(&self, element: &str) -> usize {
match element {
"title" => {
2
}
"status_bar" => {
2
}
"pagination" => {
1
}
"help" => {
2
}
_ => 1, }
}
pub(super) fn update_pagination(&mut self) {
let total = self.len();
self.paginator.set_total_items(total);
if self.height > 0 {
let item_height = self.delegate.height() + self.delegate.spacing();
let mut header_height = 0;
if self.show_title {
header_height += self.calculate_element_height("title");
}
if self.show_status_bar {
header_height += self.calculate_element_height("status_bar");
}
let mut footer_height = 0;
if self.show_help {
footer_height += self.calculate_element_height("help");
}
if self.show_pagination {
footer_height += self.calculate_element_height("pagination");
}
let available_height = self.height.saturating_sub(header_height + footer_height);
let items_per_page = if item_height > 0 {
(available_height / item_height).max(1)
} else {
5 };
self.per_page = items_per_page;
self.paginator.set_per_page(items_per_page);
self.paginator.set_total_items(self.len());
}
}
pub fn len(&self) -> usize {
if self.filter_state == FilterState::Unfiltered {
self.items.len()
} else {
self.filtered_items.len()
}
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn selected_item(&self) -> Option<&I> {
if self.filter_state == FilterState::Unfiltered {
self.items.get(self.cursor)
} else {
self.filtered_items.get(self.cursor).map(|fi| &fi.item)
}
}
pub fn cursor(&self) -> usize {
self.cursor
}
pub fn matches_for_original_item(&self, original_index: usize) -> Option<&Vec<usize>> {
self.filtered_items
.iter()
.find(|fi| fi.index == original_index)
.map(|fi| &fi.matches)
}
pub fn with_title(mut self, title: &str) -> Self {
self.title = title.to_string();
self
}
pub fn with_show_pagination(mut self, show: bool) -> Self {
self.show_pagination = show;
self
}
pub fn with_pagination_type(mut self, pagination_type: paginator::Type) -> Self {
self.paginator.paginator_type = pagination_type;
self
}
pub fn with_show_title(mut self, show: bool) -> Self {
self.show_title = show;
self
}
pub fn with_show_status_bar(mut self, show: bool) -> Self {
self.show_status_bar = show;
self
}
pub fn with_show_spinner(mut self, show: bool) -> Self {
self.show_spinner = show;
self
}
pub fn with_show_help(mut self, show: bool) -> Self {
self.show_help = show;
self
}
pub fn with_styles(mut self, styles: ListStyles) -> Self {
self.styles = styles;
self
}
pub fn show_pagination(&self) -> bool {
self.show_pagination
}
pub fn set_show_pagination(&mut self, show: bool) {
self.show_pagination = show;
}
pub fn toggle_pagination(&mut self) -> bool {
self.show_pagination = !self.show_pagination;
self.show_pagination
}
pub fn pagination_type(&self) -> paginator::Type {
self.paginator.paginator_type
}
pub fn set_pagination_type(&mut self, pagination_type: paginator::Type) {
self.paginator.paginator_type = pagination_type;
}
pub fn insert_item(&mut self, index: usize, item: I) {
self.items.insert(index, item);
if self.filter_state != FilterState::Unfiltered {
self.filter_state = FilterState::Unfiltered;
self.filtered_items.clear();
}
if index <= self.cursor {
self.cursor = self
.cursor
.saturating_add(1)
.min(self.items.len().saturating_sub(1));
}
self.update_pagination();
}
pub fn remove_item(&mut self, index: usize) -> I {
if index >= self.items.len() {
panic!("Index out of bounds");
}
if !self.delegate.can_remove(index, &self.items[index]) {
panic!("Item cannot be removed");
}
let item_ref = &self.items[index];
let _ = self.delegate.on_remove(index, item_ref);
let item = self.items.remove(index);
if self.filter_state != FilterState::Unfiltered {
self.filter_state = FilterState::Unfiltered;
self.filtered_items.clear();
}
if !self.items.is_empty() {
if index < self.cursor {
self.cursor = self.cursor.saturating_sub(1);
} else if self.cursor >= self.items.len() {
self.cursor = self.items.len().saturating_sub(1);
}
} else {
self.cursor = 0;
}
self.update_pagination();
item
}
pub fn move_item(&mut self, from_index: usize, to_index: usize) {
if from_index >= self.items.len() || to_index >= self.items.len() {
panic!("Index out of bounds");
}
if from_index == to_index {
return; }
let item = self.items.remove(from_index);
self.items.insert(to_index, item);
if self.filter_state != FilterState::Unfiltered {
self.filter_state = FilterState::Unfiltered;
self.filtered_items.clear();
}
if self.cursor == from_index {
self.cursor = to_index;
} else if from_index < self.cursor && to_index >= self.cursor {
self.cursor = self.cursor.saturating_sub(1);
} else if from_index > self.cursor && to_index <= self.cursor {
self.cursor = self.cursor.saturating_add(1);
}
self.update_pagination();
}
pub fn push_item(&mut self, item: I) {
self.items.push(item);
if self.filter_state != FilterState::Unfiltered {
self.filter_state = FilterState::Unfiltered;
self.filtered_items.clear();
}
self.update_pagination();
}
pub fn pop_item(&mut self) -> Option<I> {
if self.items.is_empty() {
return None;
}
let item = self.items.pop();
if self.filter_state != FilterState::Unfiltered {
self.filter_state = FilterState::Unfiltered;
self.filtered_items.clear();
}
if self.cursor >= self.items.len() && !self.items.is_empty() {
self.cursor = self.items.len() - 1;
} else if self.items.is_empty() {
self.cursor = 0;
}
self.update_pagination();
item
}
pub fn items(&self) -> &[I] {
&self.items
}
pub fn items_mut(&mut self) -> &mut Vec<I> {
if self.filter_state != FilterState::Unfiltered {
self.filter_state = FilterState::Unfiltered;
self.filtered_items.clear();
}
&mut self.items
}
pub fn items_len(&self) -> usize {
self.items.len()
}
pub fn items_empty(&self) -> bool {
self.items.is_empty()
}
pub fn show_title(&self) -> bool {
self.show_title
}
pub fn set_show_title(&mut self, show: bool) {
self.show_title = show;
}
pub fn toggle_title(&mut self) -> bool {
self.show_title = !self.show_title;
self.show_title
}
pub fn show_status_bar(&self) -> bool {
self.show_status_bar
}
pub fn set_show_status_bar(&mut self, show: bool) {
self.show_status_bar = show;
}
pub fn toggle_status_bar(&mut self) -> bool {
self.show_status_bar = !self.show_status_bar;
self.show_status_bar
}
pub fn show_spinner(&self) -> bool {
self.show_spinner
}
pub fn set_show_spinner(&mut self, show: bool) {
self.show_spinner = show;
}
pub fn toggle_spinner(&mut self) -> bool {
self.show_spinner = !self.show_spinner;
self.show_spinner
}
pub fn spinner(&self) -> &spinner::Model {
&self.spinner
}
pub fn spinner_mut(&mut self) -> &mut spinner::Model {
&mut self.spinner
}
pub fn show_help(&self) -> bool {
self.show_help
}
pub fn set_show_help(&mut self, show: bool) {
self.show_help = show;
}
pub fn toggle_help(&mut self) -> bool {
self.show_help = !self.show_help;
self.show_help
}
pub fn help(&self) -> &help::Model {
&self.help
}
pub fn help_mut(&mut self) -> &mut help::Model {
&mut self.help
}
pub fn styles(&self) -> &ListStyles {
&self.styles
}
pub fn styles_mut(&mut self) -> &mut ListStyles {
&mut self.styles
}
pub fn set_styles(&mut self, styles: ListStyles) {
self.paginator.active_dot = styles.active_pagination_dot.render("");
self.paginator.inactive_dot = styles.inactive_pagination_dot.render("");
self.styles = styles;
}
pub fn status_view(&self) -> String {
if !self.show_status_bar {
return String::new();
}
let mut footer = String::new();
if !self.is_empty() {
let singular = self.status_item_singular.as_deref().unwrap_or("item");
let plural = self.status_item_plural.as_deref().unwrap_or("items");
let noun = if self.len() == 1 { singular } else { plural };
footer.push_str(&format!("{}/{} {}", self.cursor + 1, self.len(), noun));
}
let help_view = self.help.view(self);
if !help_view.is_empty() {
footer.push('\n');
footer.push_str(&help_view);
}
footer
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::list::{DefaultDelegate, DefaultItem};
#[test]
fn test_pagination_calculation_fix() {
let items: Vec<DefaultItem> = (0..23)
.map(|i| DefaultItem::new(&format!("Item {}", i), "Description"))
.collect();
let delegate = DefaultDelegate::new();
for terminal_height in [24, 30, 34] {
let doc_margin = 2; let list_height = terminal_height - doc_margin;
let list = Model::new(items.clone(), delegate.clone(), 80, list_height)
.with_title("Test List");
let title_height = list.calculate_element_height("title"); let status_height = list.calculate_element_height("status_bar"); let pagination_height = list.calculate_element_height("pagination"); let help_height = list.calculate_element_height("help");
let header_height = title_height + status_height; let footer_height = pagination_height + help_height; let total_reserved = header_height + footer_height; let available_height = list_height - total_reserved;
let delegate_item_height = 3;
let expected_per_page = available_height / delegate_item_height;
let expected_total_pages =
(items.len() as f32 / expected_per_page as f32).ceil() as usize;
println!("Terminal height {}: list_height={}, available={}, expected_per_page={}, expected_pages={}, actual_per_page={}, actual_pages={}",
terminal_height, list_height, available_height, expected_per_page, expected_total_pages, list.per_page(), list.total_pages());
assert_eq!(
list.per_page(),
expected_per_page,
"Items per page mismatch for terminal height {}",
terminal_height
);
assert_eq!(
list.total_pages(),
expected_total_pages,
"Total pages mismatch for terminal height {}",
terminal_height
);
}
}
}