use ratatui_core::style::Style;
use std::{
any::Any,
cmp::Ordering,
collections::{BTreeMap, BTreeSet},
rc::Rc,
};
type RowKey<T> = Box<dyn Fn(&T) -> Box<dyn Any>>;
type KeyMatches<T> = Box<dyn Fn(&T, &dyn Any) -> bool>;
struct RowIdentity<T> {
key: RowKey<T>,
matches: KeyMatches<T>,
}
type Formatter<T> = Box<dyn Fn(&T) -> String>;
type CellStyle<T> = Box<dyn Fn(&T) -> Style>;
type RowFilter<T> = Box<dyn Fn(&T) -> bool>;
type Comparator<T> = Box<dyn Fn(&T, &T) -> Ordering>;
pub struct Column<T> {
pub(crate) title: String,
pub(crate) width: u16,
pub(crate) format: Formatter<T>,
pub(crate) compare: Option<Comparator<T>>,
pub(crate) external_sortable: bool,
pub(crate) cell_style: Option<CellStyle<T>>,
}
impl<T> Column<T> {
pub fn new(
title: impl Into<String>,
width: u16,
format: impl Fn(&T) -> String + 'static,
) -> Self {
Self {
title: title.into(),
width: width.max(4),
format: Box::new(format),
compare: None,
external_sortable: false,
cell_style: None,
}
}
pub fn cell_style(mut self, style: impl Fn(&T) -> Style + 'static) -> Self {
self.cell_style = Some(Box::new(style));
self
}
pub fn sortable(mut self, compare: impl Fn(&T, &T) -> Ordering + 'static) -> Self {
self.compare = Some(Box::new(compare));
self
}
pub fn sortable_external(mut self) -> Self {
self.external_sortable = true;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SortDirection {
Ascending,
Descending,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Sort {
pub column: usize,
pub direction: SortDirection,
}
pub struct GridModel<T> {
pub(crate) columns: Vec<Column<T>>,
rows: Vec<T>,
pub(crate) order: Vec<usize>,
selected: Option<usize>,
selected_position: Option<usize>,
sort: Option<Sort>,
identity: Option<RowIdentity<T>>,
selected_id: Option<Box<dyn Any>>,
marked: BTreeSet<usize>,
marked_ids: BTreeMap<usize, Box<dyn Any>>,
filter: Option<RowFilter<T>>,
search: String,
}
impl<T> GridModel<T> {
pub fn new(columns: Vec<Column<T>>, rows: Vec<T>) -> Self {
let order = (0..rows.len()).collect();
Self {
columns,
rows,
order,
selected: None,
selected_position: None,
sort: None,
identity: None,
selected_id: None,
marked: BTreeSet::new(),
marked_ids: BTreeMap::new(),
filter: None,
search: String::new(),
}
}
pub fn with_row_id<K: Eq + 'static>(mut self, row_id: impl Fn(&T) -> K + 'static) -> Self {
let row_id = Rc::new(row_id);
let extract = Rc::clone(&row_id);
self.identity = Some(RowIdentity {
key: Box::new(move |row| Box::new(extract(row))),
matches: Box::new(move |row, key| {
key.downcast_ref::<K>()
.is_some_and(|key| row_id(row) == *key)
}),
});
self.refresh_selected_id();
self.refresh_marked_ids();
self
}
fn refresh_selected_id(&mut self) {
self.selected_id = self.selected.and_then(|index| {
self.identity
.as_ref()
.map(|identity| (identity.key)(&self.rows[index]))
});
}
fn restore_selection(&mut self) {
self.selected = None;
self.selected_position = None;
if let (Some(identity), Some(key)) = (&self.identity, &self.selected_id) {
let mut matches = self
.rows
.iter()
.enumerate()
.filter(|(_, row)| (identity.matches)(row, key.as_ref()));
if let Some((index, _)) = matches.next()
&& matches.next().is_none()
{
self.selected = Some(index);
}
}
if self.selected.is_none() {
self.selected_id = None;
}
self.marked.clear();
if let Some(identity) = &self.identity {
for key in self.marked_ids.values() {
let mut matches = self
.rows
.iter()
.enumerate()
.filter(|(_, row)| (identity.matches)(row, key.as_ref()));
if let Some((index, _)) = matches.next()
&& matches.next().is_none()
{
self.marked.insert(index);
}
}
}
self.refresh_marked_ids();
}
fn refresh_marked_ids(&mut self) {
self.marked_ids.clear();
if let Some(identity) = &self.identity {
for &index in &self.marked {
self.marked_ids
.insert(index, (identity.key)(&self.rows[index]));
}
}
}
pub fn selected_indices(&self) -> impl Iterator<Item = usize> + '_ {
self.marked.iter().copied()
}
pub fn is_row_selected(&self, index: usize) -> bool {
self.marked.contains(&index)
}
pub fn set_selected_indices(&mut self, indices: impl IntoIterator<Item = usize>) -> bool {
let marked = indices
.into_iter()
.filter(|&i| i < self.rows.len())
.collect();
if self.marked == marked {
return false;
}
self.marked = marked;
self.refresh_marked_ids();
true
}
pub fn toggle_row_selection(&mut self, index: usize) -> bool {
if index >= self.rows.len() {
return false;
}
if !self.marked.remove(&index) {
self.marked.insert(index);
}
if let Some(identity) = &self.identity {
if self.marked.contains(&index) {
self.marked_ids
.insert(index, (identity.key)(&self.rows[index]));
} else {
self.marked_ids.remove(&index);
}
}
true
}
pub fn clear_row_selection(&mut self) -> bool {
let changed = !self.marked.is_empty();
self.marked.clear();
self.marked_ids.clear();
changed
}
pub fn visible_len(&self) -> usize {
self.order.len()
}
pub fn set_filter(&mut self, filter: impl Fn(&T) -> bool + 'static) {
self.filter = Some(Box::new(filter));
self.apply_sort();
}
pub fn clear_filter(&mut self) {
self.filter = None;
self.apply_sort();
}
pub fn set_search(&mut self, query: impl Into<String>) {
self.search = query
.into()
.chars()
.filter(|c| !c.is_control())
.collect::<String>()
.to_lowercase();
self.apply_sort();
}
fn matches_filter(&self, row: &T) -> bool {
self.filter.as_ref().is_none_or(|filter| filter(row))
&& (self.search.is_empty()
|| self.columns.iter().any(|column| {
(column.format)(row)
.chars()
.filter(|c| !c.is_control())
.collect::<String>()
.to_lowercase()
.contains(&self.search)
}))
}
pub(crate) fn clear_rows_for_request(&mut self) {
self.rows.clear();
self.order.clear();
self.selected = None;
self.selected_position = None;
self.marked.clear();
}
pub fn rows(&self) -> &[T] {
&self.rows
}
pub fn sort(&self) -> Option<Sort> {
self.sort
}
pub fn selected(&self) -> Option<&T> {
self.selected.map(|i| &self.rows[i])
}
pub fn selected_index(&self) -> Option<usize> {
self.selected
}
pub fn selected_position(&self) -> Option<usize> {
self.selected_position
}
pub fn row_at(&self, position: usize) -> Option<&T> {
self.index_at(position).map(|i| &self.rows[i])
}
pub fn index_at(&self, position: usize) -> Option<usize> {
self.order.get(position).copied()
}
pub fn update_row(&mut self, index: usize, update: impl FnOnce(&mut T)) -> bool {
if !self.update_row_in_place(index, update) {
return false;
}
if self.filter.is_some() || !self.search.is_empty() {
self.apply_sort();
return true;
}
let Some(sort) = self.sort else {
return true;
};
let old = if self.selected == Some(index) {
self.selected_position.expect("selected position")
} else {
self.order
.iter()
.position(|&i| i == index)
.expect("resident row")
};
let compare = self.columns[sort.column]
.compare
.as_ref()
.expect("sortable column");
let ordered = |a: usize, b: usize| {
let result = compare(&self.rows[a], &self.rows[b]);
let result = match sort.direction {
SortDirection::Ascending => result,
SortDirection::Descending => result.reverse(),
};
result.then(a.cmp(&b))
};
if (old == 0 || ordered(self.order[old - 1], index) != Ordering::Greater)
&& (old + 1 == self.order.len()
|| ordered(index, self.order[old + 1]) != Ordering::Greater)
{
return true;
}
self.order.remove(old);
let new = self
.order
.partition_point(|&i| ordered(i, index) == Ordering::Less);
self.order.insert(new, index);
self.selected_position = self.selected_position.map(|position| {
if self.selected == Some(index) {
new
} else if old < position && new >= position {
position - 1
} else if new <= position && old > position {
position + 1
} else {
position
}
});
true
}
pub(crate) fn update_row_in_place(
&mut self,
index: usize,
update: impl FnOnce(&mut T),
) -> bool {
let Some(row) = self.rows.get_mut(index) else {
return false;
};
update(row);
if self.selected == Some(index) {
self.refresh_selected_id();
}
if self.marked.contains(&index)
&& let Some(identity) = &self.identity
{
self.marked_ids
.insert(index, (identity.key)(&self.rows[index]));
}
true
}
pub fn select(&mut self, position: usize) -> bool {
let Some(&index) = self.order.get(position) else {
return false;
};
let changed = self.selected != Some(index);
self.selected = Some(index);
self.selected_position = Some(position);
self.refresh_selected_id();
changed
}
pub fn toggle_sort(&mut self, column: usize) -> bool {
if self
.columns
.get(column)
.and_then(|c| c.compare.as_ref())
.is_none()
{
return false;
}
if !self.request_sort(column) {
return false;
}
self.apply_sort();
true
}
pub(crate) fn request_sort(&mut self, column: usize) -> bool {
if !self
.columns
.get(column)
.is_some_and(|c| c.compare.is_some() || c.external_sortable)
{
return false;
}
self.sort = match self.sort {
Some(Sort {
column: c,
direction: SortDirection::Ascending,
}) if c == column => Some(Sort {
column,
direction: SortDirection::Descending,
}),
Some(Sort {
column: c,
direction: SortDirection::Descending,
}) if c == column => None,
_ => Some(Sort {
column,
direction: SortDirection::Ascending,
}),
};
true
}
pub fn replace_rows(&mut self, rows: Vec<T>) {
self.rows = rows;
self.restore_selection();
self.apply_sort();
}
pub(crate) fn replace_rows_in_order(&mut self, rows: Vec<T>) {
self.rows = rows;
self.restore_selection();
self.order = (0..self.rows.len()).collect();
self.selected_position = self.selected;
}
fn apply_sort(&mut self) {
self.order = (0..self.rows.len())
.filter(|&index| self.matches_filter(&self.rows[index]))
.collect();
if let Some(sort) = self.sort {
let compare = self.columns[sort.column]
.compare
.as_ref()
.expect("sortable column");
let rows = &self.rows;
self.order.sort_by(|&a, &b| {
let result = compare(&rows[a], &rows[b]);
match sort.direction {
SortDirection::Ascending => result,
SortDirection::Descending => result.reverse(),
}
});
}
self.selected_position = self
.selected
.and_then(|i| self.order.iter().position(|&r| r == i));
}
}