#![deny(
missing_docs,
missing_copy_implementations,
trivial_casts,
trivial_numeric_casts,
unsafe_code,
unused_import_braces,
unused_qualifications
)]
extern crate cursive;
use std::cmp::{self, Ordering};
use std::collections::HashMap;
use std::hash::Hash;
use std::rc::Rc;
use cursive::align::HAlign;
use cursive::direction::Direction;
use cursive::event::{Callback, Event, EventResult, Key};
use cursive::theme;
use cursive::vec::Vec2;
use cursive::view::{ScrollBase, View};
use cursive::With;
use cursive::{Cursive, Printer};
pub trait TableViewItem<H>: Clone + Sized
where
H: Eq + Hash + Copy + Clone + 'static,
{
fn to_column(&self, column: H) -> String;
fn cmp(&self, other: &Self, column: H) -> Ordering
where
Self: Sized;
}
type OnSortCallback<H> = Rc<Fn(&mut Cursive, H, Ordering)>;
type IndexCallback = Rc<Fn(&mut Cursive, usize, usize)>;
pub struct TableView<T: TableViewItem<H>, H: Eq + Hash + Copy + Clone + 'static> {
enabled: bool,
scrollbase: ScrollBase,
last_size: Vec2,
column_select: bool,
columns: Vec<TableColumn<H>>,
column_indicies: HashMap<H, usize>,
focus: usize,
items: Vec<T>,
rows_to_items: Vec<usize>,
on_sort: Option<OnSortCallback<H>>,
on_submit: Option<IndexCallback>,
on_select: Option<IndexCallback>,
}
impl<T: TableViewItem<H>, H: Eq + Hash + Copy + Clone + 'static> Default for TableView<T, H> {
fn default() -> Self {
Self::new()
}
}
impl<T: TableViewItem<H>, H: Eq + Hash + Copy + Clone + 'static> TableView<T, H> {
pub fn new() -> Self {
Self {
enabled: true,
scrollbase: ScrollBase::new(),
last_size: Vec2::new(0, 0),
column_select: false,
columns: Vec::new(),
column_indicies: HashMap::new(),
focus: 0,
items: Vec::new(),
rows_to_items: Vec::new(),
on_sort: None,
on_submit: None,
on_select: None,
}
}
pub fn column<S: Into<String>, C: FnOnce(TableColumn<H>) -> TableColumn<H>>(
mut self,
column: H,
title: S,
callback: C,
) -> Self {
self.column_indicies.insert(column, self.columns.len());
self.columns
.push(callback(TableColumn::new(column, title.into())));
if self.columns.len() == 1 {
self.default_column(column)
} else {
self
}
}
pub fn default_column(mut self, column: H) -> Self {
if self.column_indicies.contains_key(&column) {
for c in &mut self.columns {
c.selected = c.column == column;
if c.selected {
c.order = c.default_order;
} else {
c.order = Ordering::Equal;
}
}
}
self
}
pub fn sort_by(&mut self, column: H, order: Ordering) {
if self.column_indicies.contains_key(&column) {
for c in &mut self.columns {
c.selected = c.column == column;
if c.selected {
c.order = order;
} else {
c.order = Ordering::Equal;
}
}
}
self.sort_items(column, order);
}
pub fn sort(&mut self) {
if let Some((column, order)) = self.order() {
self.sort_items(column, order);
}
}
pub fn order(&self) -> Option<(H, Ordering)> {
for c in &self.columns {
if c.order != Ordering::Equal {
return Some((c.column, c.order));
}
}
None
}
pub fn disable(&mut self) {
self.enabled = false;
}
pub fn enable(&mut self) {
self.enabled = true;
}
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
pub fn is_enabled(&self) -> bool {
self.enabled
}
pub fn set_on_sort<F>(&mut self, cb: F)
where
F: Fn(&mut Cursive, H, Ordering) + 'static,
{
self.on_sort = Some(Rc::new(move |s, h, o| cb(s, h, o)));
}
pub fn on_sort<F>(self, cb: F) -> Self
where
F: Fn(&mut Cursive, H, Ordering) + 'static,
{
self.with(|t| t.set_on_sort(cb))
}
pub fn set_on_submit<F>(&mut self, cb: F)
where
F: Fn(&mut Cursive, usize, usize) + 'static,
{
self.on_submit = Some(Rc::new(move |s, row, index| cb(s, row, index)));
}
pub fn on_submit<F>(self, cb: F) -> Self
where
F: Fn(&mut Cursive, usize, usize) + 'static,
{
self.with(|t| t.set_on_submit(cb))
}
pub fn set_on_select<F>(&mut self, cb: F)
where
F: Fn(&mut Cursive, usize, usize) + 'static,
{
self.on_select = Some(Rc::new(move |s, row, index| cb(s, row, index)));
}
pub fn on_select<F>(self, cb: F) -> Self
where
F: Fn(&mut Cursive, usize, usize) + 'static,
{
self.with(|t| t.set_on_select(cb))
}
pub fn clear(&mut self) {
self.items.clear();
self.rows_to_items.clear();
self.focus = 0;
}
pub fn len(&self) -> usize {
self.items.len()
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
pub fn row(&self) -> Option<usize> {
if self.items.is_empty() {
None
} else {
Some(self.focus)
}
}
pub fn set_selected_row(&mut self, row_index: usize) {
self.focus = row_index;
self.scrollbase.scroll_to(row_index);
}
pub fn selected_row(self, row_index: usize) -> Self {
self.with(|t| t.set_selected_row(row_index))
}
pub fn set_items(&mut self, items: Vec<T>) {
self.items = items;
self.rows_to_items = Vec::with_capacity(self.items.len());
for i in 0..self.items.len() {
self.rows_to_items.push(i);
}
if let Some((column, order)) = self.order() {
self.sort_by(column, order);
}
self.scrollbase
.set_heights(self.last_size.y.saturating_sub(2), self.rows_to_items.len());
self.set_selected_row(0);
}
pub fn items(self, items: Vec<T>) -> Self {
self.with(|t| t.set_items(items))
}
pub fn borrow_item(&mut self, index: usize) -> Option<&T> {
self.items.get(index)
}
pub fn borrow_item_mut(&mut self, index: usize) -> Option<&mut T> {
self.items.get_mut(index)
}
pub fn borrow_items(&mut self) -> &Vec<T> {
&self.items
}
pub fn borrow_items_mut(&mut self) -> &mut Vec<T> {
&mut self.items
}
pub fn item(&self) -> Option<usize> {
if self.items.is_empty() {
None
} else {
Some(self.rows_to_items[self.focus])
}
}
pub fn set_selected_item(&mut self, item_index: usize) {
if item_index < self.items.len() {
for (row, item) in self.rows_to_items.iter().enumerate() {
if *item == item_index {
self.focus = row;
self.scrollbase.scroll_to(row);
break;
}
}
}
}
pub fn selected_item(self, item_index: usize) -> Self {
self.with(|t| t.set_selected_item(item_index))
}
pub fn insert_item(&mut self, item: T) {
self.items.push(item);
self.rows_to_items.push(self.items.len() - 1);
self.scrollbase
.set_heights(self.last_size.y.saturating_sub(2), self.rows_to_items.len());
if let Some((column, order)) = self.order() {
self.sort_by(column, order);
}
}
pub fn remove_item(&mut self, item_index: usize) -> Option<T> {
if item_index < self.items.len() {
if let Some(selected_index) = self.item() {
if selected_index == item_index {
self.focus_up(1);
}
}
self.rows_to_items.retain(|i| *i != item_index);
for ref_index in &mut self.rows_to_items {
if *ref_index > item_index {
*ref_index -= 1;
}
}
self.scrollbase
.set_heights(self.last_size.y.saturating_sub(2), self.rows_to_items.len());
Some(self.items.remove(item_index))
} else {
None
}
}
pub fn take_items(&mut self) -> Vec<T> {
self.scrollbase
.set_heights(self.last_size.y.saturating_sub(2), 0);
self.set_selected_row(0);
self.rows_to_items.clear();
self.items.drain(0..).collect()
}
}
impl<T: TableViewItem<H>, H: Eq + Hash + Copy + Clone + 'static> TableView<T, H> {
fn draw_columns<C: Fn(&Printer, &TableColumn<H>)>(
&self,
printer: &Printer,
sep: &str,
callback: C,
) {
let mut column_offset = 0;
let column_count = self.columns.len();
for (index, column) in self.columns.iter().enumerate() {
let printer = &printer.offset((column_offset, 0)).focused(true);
callback(printer, column);
if index < column_count - 1 {
printer.print((column.width + 1, 0), sep);
}
column_offset += column.width + 3;
}
}
fn sort_items(&mut self, column: H, order: Ordering) {
if !self.is_empty() {
let old_item = self.item();
let mut rows_to_items = self.rows_to_items.clone();
rows_to_items.sort_by(|a, b| {
if order == Ordering::Less {
self.items[*a].cmp(&self.items[*b], column)
} else {
self.items[*b].cmp(&self.items[*a], column)
}
});
self.rows_to_items = rows_to_items;
if let Some(old_item) = old_item {
self.set_selected_item(old_item);
}
}
}
fn draw_item(&self, printer: &Printer, i: usize) {
self.draw_columns(printer, "┆ ", |printer, column| {
let value = self.items[self.rows_to_items[i]].to_column(column.column);
column.draw_row(printer, value.as_str());
});
}
fn focus_up(&mut self, n: usize) {
self.focus -= cmp::min(self.focus, n);
}
fn focus_down(&mut self, n: usize) {
self.focus = cmp::min(self.focus + n, self.items.len() - 1);
}
fn active_column(&self) -> usize {
self.columns.iter().position(|c| c.selected).unwrap_or(0)
}
fn column_cancel(&mut self) {
self.column_select = false;
for column in &mut self.columns {
column.selected = column.order != Ordering::Equal;
}
}
fn column_next(&mut self) -> bool {
let column = self.active_column();
if column < self.columns.len() - 1 {
self.columns[column].selected = false;
self.columns[column + 1].selected = true;
true
} else {
false
}
}
fn column_prev(&mut self) -> bool {
let column = self.active_column();
if column > 0 {
self.columns[column].selected = false;
self.columns[column - 1].selected = true;
true
} else {
false
}
}
fn column_select(&mut self) {
let next = self.active_column();
let column = self.columns[next].column;
let current = self
.columns
.iter()
.position(|c| c.order != Ordering::Equal)
.unwrap_or(0);
let order = if current != next {
self.columns[next].default_order
} else if self.columns[current].order == Ordering::Less {
Ordering::Greater
} else {
Ordering::Less
};
self.sort_by(column, order);
}
}
impl<T: TableViewItem<H> + 'static, H: Eq + Hash + Copy + Clone + 'static> View
for TableView<T, H>
{
fn draw(&self, printer: &Printer) {
self.draw_columns(printer, "╷ ", |printer, column| {
let color = if column.order != Ordering::Equal || column.selected {
if self.column_select && column.selected && self.enabled && printer.focused {
theme::ColorStyle::highlight()
} else {
theme::ColorStyle::highlight_inactive()
}
} else {
theme::ColorStyle::primary()
};
printer.with_color(color, |printer| {
column.draw_header(printer);
});
});
self.draw_columns(
&printer.offset((0, 1)).focused(true),
"┴─",
|printer, column| {
printer.print_hline((0, 0), column.width + 1, "─");
},
);
let printer = &printer.offset((0, 2)).focused(true);
self.scrollbase.draw(printer, |printer, i| {
let color = if i == self.focus {
if !self.column_select && self.enabled && printer.focused {
theme::ColorStyle::highlight()
} else {
theme::ColorStyle::highlight_inactive()
}
} else {
theme::ColorStyle::primary()
};
if i < self.items.len() {
printer.with_color(color, |printer| {
self.draw_item(printer, i);
});
}
});
}
fn layout(&mut self, size: Vec2) {
if size == self.last_size {
return;
}
let item_count = self.items.len();
let column_count = self.columns.len();
let (mut sized, mut usized): (Vec<&mut TableColumn<H>>, Vec<&mut TableColumn<H>>) = self
.columns
.iter_mut()
.partition(|c| c.requested_width.is_some());
let mut available_width = size.x.saturating_sub(column_count.saturating_sub(1) * 3);
if size.y.saturating_sub(1) < item_count {
available_width = available_width.saturating_sub(2);
}
let mut remaining_width = available_width;
for column in &mut sized {
column.width = match *column.requested_width.as_ref().unwrap() {
TableColumnWidth::Percent(width) => cmp::min(
(size.x as f32 / 100.0 * width as f32).ceil() as usize,
remaining_width,
),
TableColumnWidth::Absolute(width) => width,
};
remaining_width = remaining_width.saturating_sub(column.width);
}
let remaining_columns = usized.len();
for column in &mut usized {
column.width = (remaining_width as f32 / remaining_columns as f32).floor() as usize;
}
self.scrollbase
.set_heights(size.y.saturating_sub(2), item_count);
self.last_size = size;
}
fn take_focus(&mut self, _: Direction) -> bool {
self.enabled
}
fn on_event(&mut self, event: Event) -> EventResult {
if !self.enabled {
return EventResult::Ignored;
}
let last_focus = self.focus;
match event {
Event::Key(Key::Right) => {
if self.column_select {
if !self.column_next() {
return EventResult::Ignored;
}
} else {
self.column_select = true;
}
}
Event::Key(Key::Left) => {
if self.column_select {
if !self.column_prev() {
return EventResult::Ignored;
}
} else {
self.column_select = true;
}
}
Event::Key(Key::Up) if self.focus > 0 || self.column_select => {
if self.column_select {
self.column_cancel();
} else {
self.focus_up(1);
}
}
Event::Key(Key::Down) if self.focus + 1 < self.items.len() || self.column_select => {
if self.column_select {
self.column_cancel();
} else {
self.focus_down(1);
}
}
Event::Key(Key::PageUp) => {
self.column_cancel();
self.focus_up(10);
}
Event::Key(Key::PageDown) => {
self.column_cancel();
self.focus_down(10);
}
Event::Key(Key::Home) => {
self.column_cancel();
self.focus = 0;
}
Event::Key(Key::End) => {
self.column_cancel();
self.focus = self.items.len() - 1;
}
Event::Key(Key::Enter) => {
if self.column_select {
self.column_select();
if self.on_sort.is_some() {
let c = &self.columns[self.active_column()];
let column = c.column;
let order = c.order;
let cb = self.on_sort.clone().unwrap();
return EventResult::Consumed(Some(Callback::from_fn(move |s| {
cb(s, column, order)
})));
}
} else if !self.is_empty() && self.on_submit.is_some() {
let cb = self.on_submit.clone().unwrap();
let row = self.row().unwrap();
let index = self.item().unwrap();
return EventResult::Consumed(Some(Callback::from_fn(move |s| {
cb(s, row, index)
})));
}
}
_ => return EventResult::Ignored,
}
let focus = self.focus;
self.scrollbase.scroll_to(focus);
if self.column_select {
EventResult::Consumed(None)
} else if !self.is_empty() && last_focus != focus {
let row = self.row().unwrap();
let index = self.item().unwrap();
EventResult::Consumed(
self.on_select
.clone()
.map(|cb| Callback::from_fn(move |s| cb(s, row, index))),
)
} else {
EventResult::Ignored
}
}
}
pub struct TableColumn<H: Copy + Clone + 'static> {
column: H,
title: String,
selected: bool,
alignment: HAlign,
order: Ordering,
width: usize,
default_order: Ordering,
requested_width: Option<TableColumnWidth>,
}
enum TableColumnWidth {
Percent(usize),
Absolute(usize),
}
impl<H: Copy + Clone + 'static> TableColumn<H> {
pub fn ordering(mut self, order: Ordering) -> Self {
self.default_order = order;
self
}
pub fn align(mut self, alignment: HAlign) -> Self {
self.alignment = alignment;
self
}
pub fn width(mut self, width: usize) -> Self {
self.requested_width = Some(TableColumnWidth::Absolute(width));
self
}
pub fn width_percent(mut self, width: usize) -> Self {
self.requested_width = Some(TableColumnWidth::Percent(width));
self
}
fn new(column: H, title: String) -> Self {
Self {
column,
title,
selected: false,
alignment: HAlign::Left,
order: Ordering::Equal,
width: 0,
default_order: Ordering::Less,
requested_width: None,
}
}
fn draw_header(&self, printer: &Printer) {
let order = match self.order {
Ordering::Less => "^",
Ordering::Greater => "v",
Ordering::Equal => " ",
};
let header = match self.alignment {
HAlign::Left => format!(
"{:<width$} [{}]",
self.title,
order,
width = self.width.saturating_sub(4)
),
HAlign::Right => format!(
"{:>width$} [{}]",
self.title,
order,
width = self.width.saturating_sub(4)
),
HAlign::Center => format!(
"{:^width$} [{}]",
self.title,
order,
width = self.width.saturating_sub(4)
),
};
printer.print((0, 0), header.as_str());
}
fn draw_row(&self, printer: &Printer, value: &str) {
let value = match self.alignment {
HAlign::Left => format!("{:<width$} ", value, width = self.width),
HAlign::Right => format!("{:>width$} ", value, width = self.width),
HAlign::Center => format!("{:^width$} ", value, width = self.width),
};
printer.print((0, 0), value.as_str());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
enum SimpleColumn {
Name,
}
#[allow(dead_code)]
impl SimpleColumn {
fn as_str(&self) -> &str {
match *self {
SimpleColumn::Name => "Name",
}
}
}
#[derive(Clone, Debug)]
struct SimpleItem {
name: String,
}
impl TableViewItem<SimpleColumn> for SimpleItem {
fn to_column(&self, column: SimpleColumn) -> String {
match column {
SimpleColumn::Name => self.name.to_string(),
}
}
fn cmp(&self, other: &Self, column: SimpleColumn) -> Ordering
where
Self: Sized,
{
match column {
SimpleColumn::Name => self.name.cmp(&other.name),
}
}
}
fn setup_test_table() -> TableView<SimpleItem, SimpleColumn> {
TableView::<SimpleItem, SimpleColumn>::new()
.column(SimpleColumn::Name, "Name", |c| c.width_percent(20))
}
#[test]
fn should_insert_into_existing_table() {
let mut simple_table = setup_test_table();
let mut simple_items = Vec::new();
for i in 1..=10 {
simple_items.push(SimpleItem {
name: format!("{} - Name", i),
});
}
simple_table.set_items(simple_items);
simple_table.insert_item(SimpleItem {
name: format!("{} Name", 11),
});
assert!(simple_table.len() == 11);
}
#[test]
fn should_insert_into_empty_table() {
let mut simple_table = setup_test_table();
simple_table.insert_item(SimpleItem {
name: format!("{} Name", 1),
});
assert!(simple_table.len() == 1);
}
}