// Copyright 2018 The Grin Developers // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Copyright (c) 2015-2017 Ivo Wetzel // // Permission is hereby granted, free of charge, to any // person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the // Software without restriction, including without // limitation the rights to use, copy, modify, merge, // publish, distribute, sublicense, and/or sell copies of // the Software, and to permit persons to whom the Software // is furnished to do so, subject to the following // conditions: // // The above copyright notice and this permission notice // shall be included in all copies or substantial portions // of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF // ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED // TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT // SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY // CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. //! Adapted from https://github.com/behnam/rust-cursive-table-view //! A basic table view implementation for [cursive](https://crates.io/crates/cursive). #![deny( missing_docs, missing_copy_implementations, trivial_casts, trivial_numeric_casts, unsafe_code, unused_import_braces, unused_qualifications )] // Crate Dependencies --------------------------------------------------------- extern crate cursive; // STD Dependencies ----------------------------------------------------------- use std::cmp::{self, Ordering}; use std::collections::HashMap; use std::hash::Hash; use std::rc::Rc; // External Dependencies ------------------------------------------------------ use cursive::align::HAlign; use cursive::direction::Direction; use cursive::event::{Callback, Event, EventResult, Key}; use cursive::theme::ColorStyle; use cursive::theme::PaletteColor::{Highlight, HighlightInactive, Primary}; use cursive::vec::Vec2; use cursive::view::{ScrollBase, View}; use cursive::With; use cursive::{Cursive, Printer}; /// A trait for displaying and sorting items inside a /// [`TableView`](struct.TableView.html). pub trait TableViewItem: Clone + Sized where H: Eq + Hash + Copy + Clone + 'static, { /// Method returning a string representation of the item for the /// specified column from type `H`. fn to_column(&self, column: H) -> String; /// Method comparing two items via their specified column from type `H`. fn cmp(&self, other: &Self, column: H) -> Ordering where Self: Sized; } /// View to select an item among a list, supporting multiple columns for /// sorting. /// /// # Examples /// /// ```rust /// # extern crate cursive; /// # extern crate cursive_table_view; /// # use std::cmp::Ordering; /// # use cursive_table_view::{TableView, TableViewItem}; /// # use cursive::align::HAlign; /// # fn main() { /// // Provide a type for the table's columns /// #[derive(Copy, Clone, PartialEq, Eq, Hash)] /// enum BasicColumn { /// Name, /// Count, /// Rate /// } /// /// // Define the item type /// #[derive(Clone, Debug)] /// struct Foo { /// name: String, /// count: usize, /// rate: usize /// } /// /// impl TableViewItem for Foo { /// /// fn to_column(&self, column: BasicColumn) -> String { /// match column { /// BasicColumn::Name => self.name.to_string(), /// BasicColumn::Count => format!("{}", self.count), /// BasicColumn::Rate => format!("{}", self.rate) /// } /// } /// /// fn cmp(&self, other: &Self, column: BasicColumn) -> Ordering where Self: Sized { /// match column { /// BasicColumn::Name => self.name.cmp(&other.name), /// BasicColumn::Count => self.count.cmp(&other.count), /// BasicColumn::Rate => self.rate.cmp(&other.rate) /// } /// } /// /// } /// /// // Configure the actual table /// let table = TableView::::new() /// .column(BasicColumn::Name, "Name", |c| c.width(20)) /// .column(BasicColumn::Count, "Count", |c| c.align(HAlign::Center)) /// .column(BasicColumn::Rate, "Rate", |c| { /// c.ordering(Ordering::Greater).align(HAlign::Right).width(20) /// }) /// .default_column(BasicColumn::Name); /// # } /// ``` pub struct TableView, H: Eq + Hash + Copy + Clone + 'static> { enabled: bool, scrollbase: ScrollBase, last_size: Vec2, column_select: bool, columns: Vec>, column_indices: HashMap, focus: usize, items: Vec, rows_to_items: Vec, on_sort: Option>, // TODO Pass drawing offsets into the handlers so a popup menu // can be created easily? on_submit: Option>, on_select: Option>, } impl, H: Eq + Hash + Copy + Clone + 'static> TableView { /// Creates a new empty `TableView` without any columns. /// /// A TableView should be accompanied by a enum of type `H` representing /// the table columns. pub fn new() -> Self { Self { enabled: true, scrollbase: ScrollBase::new(), last_size: Vec2::new(0, 0), column_select: false, columns: Vec::new(), column_indices: HashMap::new(), focus: 0, items: Vec::new(), rows_to_items: Vec::new(), on_sort: None, on_submit: None, on_select: None, } } /// Adds a column for the specified table column from type `H` along with /// a title for its visual display. /// /// The provided callback can be used to further configure the /// created [`TableColumn`](struct.TableColumn.html). pub fn column, C: FnOnce(TableColumn) -> TableColumn>( mut self, column: H, title: S, callback: C, ) -> Self { self.column_indices.insert(column, self.columns.len()); self.columns .push(callback(TableColumn::new(column, title.into()))); // Make the first column the default one if self.columns.len() == 1 { self.default_column(column) } else { self } } /// Sets the initially active column of the table. pub fn default_column(mut self, column: H) -> Self { if self.column_indices.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 } /// Sorts the table using the specified table `column` and the passed /// `order`. pub fn sort_by(&mut self, column: H, order: Ordering) { if self.column_indices.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); } /// Sorts the table using the currently active column and its /// ordering. pub fn sort(&mut self) { if let Some((column, order)) = self.order() { self.sort_items(column, order); } } /// Returns the currently active column that is used for sorting /// along with its ordering. /// /// Might return `None` if there are currently no items in the table /// and it has not been sorted yet. pub fn order(&self) -> Option<(H, Ordering)> { for c in &self.columns { if c.order != Ordering::Equal { return Some((c.column, c.order)); } } None } /// Disables this view. /// /// A disabled view cannot be selected. pub fn disable(&mut self) { self.enabled = false; } /// Re-enables this view. pub fn enable(&mut self) { self.enabled = true; } /// Enable or disable this view. pub fn set_enabled(&mut self, enabled: bool) { self.enabled = enabled; } /// Returns `true` if this view is enabled. pub fn is_enabled(&self) -> bool { self.enabled } /// Sets a callback to be used when a selected column is sorted by /// pressing ``. /// /// # Example /// /// ```norun /// rt(|siv: &mut Cursive, column: BasicColumn, order: Ordering| {}); /// ``` pub fn set_on_sort(&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))); } /// Sets a callback to be used when a selected column is sorted by /// pressing ``. /// /// Chainable variant. /// /// # Example /// /// ```norun /// siv: &mut Cursive, column: BasicColumn, order: Ordering| {}); /// ``` pub fn on_sort(self, cb: F) -> Self where F: Fn(&mut Cursive, H, Ordering) + 'static, { self.with(|t| t.set_on_sort(cb)) } /// Sets a callback to be used when `` is pressed while an item /// is selected. /// /// Both the currently selected row and the index of the corresponding item /// within the underlying storage vector will be given to the callback. /// /// # Example /// /// ```norun /// bmit(|siv: &mut Cursive, row: usize, index: usize| {}); /// ``` pub fn set_on_submit(&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))); } /// Sets a callback to be used when `` is pressed while an item /// is selected. /// /// Both the currently selected row and the index of the corresponding item /// within the underlying storage vector will be given to the callback. /// /// Chainable variant. /// /// # Example /// /// ```norun /// (|siv: &mut Cursive, row: usize, index: usize| {}); /// ``` pub fn on_submit(self, cb: F) -> Self where F: Fn(&mut Cursive, usize, usize) + 'static, { self.with(|t| t.set_on_submit(cb)) } /// Sets a callback to be used when an item is selected. /// /// Both the currently selected row and the index of the corresponding item /// within the underlying storage vector will be given to the callback. /// /// # Example /// /// ```norun /// lect(|siv: &mut Cursive, row: usize, index: usize| {}); /// ``` pub fn set_on_select(&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))); } /// Sets a callback to be used when an item is selected. /// /// Both the currently selected row and the index of the corresponding item /// within the underlying storage vector will be given to the callback. /// /// Chainable variant. /// /// # Example /// /// ```norun /// (|siv: &mut Cursive, row: usize, index: usize| {}); /// ``` pub fn on_select(self, cb: F) -> Self where F: Fn(&mut Cursive, usize, usize) + 'static, { self.with(|t| t.set_on_select(cb)) } /// Removes all items from this view. pub fn clear(&mut self) { self.items.clear(); self.rows_to_items.clear(); self.focus = 0; } /// Returns the number of items in this table. pub fn len(&self) -> usize { self.items.len() } /// Returns `true` if this table has no items. pub fn is_empty(&self) -> bool { self.items.is_empty() } /// Returns the index of the currently selected table row. pub fn row(&self) -> Option { if self.items.is_empty() { None } else { Some(self.focus) } } /// Selects the row at the specified index. pub fn set_selected_row(&mut self, row_index: usize) { self.focus = row_index; self.scrollbase.scroll_to(row_index); } /// Selects the row at the specified index. /// /// Chainable variant. pub fn selected_row(self, row_index: usize) -> Self { self.with(|t| t.set_selected_row(row_index)) } /// Sets the contained items of the table. /// /// The currently active sort order is preserved and will be applied to all /// items. pub fn set_items(&mut self, items: Vec) { 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); } /// Sets the contained items of the table. /// /// The order of the items will be preserved even when the table is sorted. /// /// Chainable variant. pub fn items(self, items: Vec) -> Self { self.with(|t| t.set_items(items)) } /// Returns a immutable reference to the item at the specified index /// within the underlying storage vector. pub fn borrow_item(&mut self, index: usize) -> Option<&T> { self.items.get(index) } /// Returns a mutable reference to the item at the specified index within /// the underlying storage vector. pub fn borrow_item_mut(&mut self, index: usize) -> Option<&mut T> { self.items.get_mut(index) } /// Returns a immutable reference to the items contained within the table. pub fn borrow_items(&mut self) -> &Vec { &self.items } /// Returns a mutable reference to the items contained within the table. /// /// Can be used to modify the items in place. pub fn borrow_items_mut(&mut self) -> &mut Vec { &mut self.items } /// Returns the index of the currently selected item within the underlying /// storage vector. pub fn item(&self) -> Option { if self.items.is_empty() { None } else { Some(self.rows_to_items[self.focus]) } } /// Selects the item at the specified index within the underlying storage /// vector. pub fn set_selected_item(&mut self, item_index: usize) { // TODO optimize the performance for very large item lists 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; } } } } /// Selects the item at the specified index within the underlying storage /// vector. /// /// Chainable variant. pub fn selected_item(self, item_index: usize) -> Self { self.with(|t| t.set_selected_item(item_index)) } /// Inserts a new item into the table. /// /// The currently active sort order is preserved and will be applied to the /// newly inserted item. pub fn insert_item(&mut self, item: T) { self.items.push(item); self.rows_to_items.push(self.items.len()); 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); } } /// Removes the item at the specified index within the underlying storage /// vector and returns it. pub fn remove_item(&mut self, item_index: usize) -> Option { if item_index < self.items.len() { // Move the selection if the currently selected item gets removed if let Some(selected_index) = self.item() { if selected_index == item_index { self.focus_up(1); } } // Remove the sorted reference to the item self.rows_to_items.retain(|i| *i != item_index); // Adjust remaining references for ref_index in &mut self.rows_to_items { if *ref_index > item_index { *ref_index -= 1; } } // Update scroll height to prevent out of index drawing self.scrollbase .set_heights(self.last_size.y.saturating_sub(2), self.rows_to_items.len()); // Remove actual item from the underlying storage Some(self.items.remove(item_index)) } else { None } } /// Removes all items from the underlying storage and returns them. pub fn take_items(&mut self) -> Vec { 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, H: Eq + Hash + Copy + Clone + 'static> TableView { fn draw_columns)>( &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.sub_printer((column_offset, 0), printer.size, 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().unwrap(); 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; 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 + 'static, H: Eq + Hash + Copy + Clone + 'static> View for TableView { 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 { Highlight } else { HighlightInactive } } else { Primary }; printer.with_color(ColorStyle::from(color), |printer| { column.draw_header(printer); }); }); self.draw_columns( &printer.sub_printer((0, 1), printer.size, true), "┴─", |printer, column| { printer.print_hline((0, 0), column.width + 1, "─"); }, ); let printer = &printer.sub_printer((0, 2), printer.size, true); self.scrollbase.draw(printer, |printer, i| { let color = if i == self.focus { if !self.column_select && self.enabled && printer.focused { Highlight } else { HighlightInactive } } else { Primary }; printer.with_color(ColorStyle::from(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(); // Split up all columns into sized / unsized groups let (mut sized, mut usized): (Vec<&mut TableColumn>, Vec<&mut TableColumn>) = self.columns .iter_mut() .partition(|c| c.requested_width.is_some()); // Subtract one for the separators between our columns (that's column_count - 1) let mut available_width = size.x.saturating_sub(column_count.saturating_sub(1) * 3); // Reduce the with in case we are displaying a scrollbar if size.y.saturating_sub(1) < item_count { available_width = available_width.saturating_sub(2); } // Calculate widths for all requested columns 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); } // Spread the remaining with across the unsized columns 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 && !self.items.is_empty() } 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 } } } /// A type used for the construction of columns in a /// [`TableView`](struct.TableView.html). pub struct TableColumn { column: H, title: String, selected: bool, alignment: HAlign, order: Ordering, width: usize, default_order: Ordering, requested_width: Option, } enum TableColumnWidth { Percent(usize), Absolute(usize), } impl TableColumn { /// Sets the default ordering of the column. pub fn ordering(mut self, order: Ordering) -> Self { self.default_order = order; self } /// Sets the horizontal text alignment of the column. pub fn align(mut self, alignment: HAlign) -> Self { self.alignment = alignment; self } /// Sets how many characters of width this column will try to occupy. pub fn width(mut self, width: usize) -> Self { self.requested_width = Some(TableColumnWidth::Absolute(width)); self } /// Sets what percentage of the width of the entire table this column will /// try to occupy. 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: column, title: 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!( "{: 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!("{: format!("{:>width$} ", value, width = self.width), HAlign::Center => format!("{:^width$} ", value, width = self.width), }; printer.print((0, 0), value.as_str()); } }