├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples ├── basic.rs └── double.rs └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | target 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cursive_table_view" 3 | version = "0.15.0" 4 | edition = "2021" 5 | authors = ["Ivo Wetzel ", "Alexandre Bury "] 6 | description = "A basic table view implementation for cursive." 7 | repository = "https://github.com/BonsaiDen/cursive_table_view.git" 8 | readme = "README.md" 9 | keywords = ["cursive", "ncurses", "TUI", "UI", "table"] 10 | categories = ["command-line-interface", "gui"] 11 | license = "MIT/Apache-2.0" 12 | 13 | [dependencies] 14 | cursive_core = "0.4.0" 15 | 16 | [dev-dependencies] 17 | cursive = "0.21.0" 18 | rand = "0.8" 19 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2017 Ivo Wetzel 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cursive-table-view 2 | 3 | [![cursive-table-view on crates.io][cratesio-image]][cratesio] 4 | [![cursive-table-view on docs.rs][docsrs-image]][docsrs] 5 | 6 | [cratesio-image]: https://img.shields.io/crates/v/cursive_table_view.svg 7 | [cratesio]: https://crates.io/crates/cursive_table_view 8 | [docsrs-image]: https://docs.rs/cursive_table_view/badge.svg 9 | [docsrs]: https://docs.rs/cursive_table_view/ 10 | 11 | A basic table view implementation for [cursive](https://crates.io/crates/cursive). 12 | 13 | ![table](https://cloud.githubusercontent.com/assets/124674/25067632/a6784a56-2249-11e7-8885-50ba7058565f.png) 14 | 15 | ## Usage 16 | 17 | Add this to your `Cargo.toml`: 18 | 19 | ```toml 20 | [dependencies] 21 | cursive_table_view = "0.14" 22 | ``` 23 | 24 | and this to your crate root: 25 | 26 | ```rust 27 | extern crate cursive_table_view; 28 | ``` 29 | 30 | ## License 31 | 32 | Licensed under either of 33 | * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 34 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 35 | at your option. 36 | 37 | 38 | ### Contribution 39 | 40 | Unless you explicitly state otherwise, any contribution intentionally submitted 41 | for inclusion in the work by you shall be dual licensed as above, without any 42 | additional terms or conditions. 43 | -------------------------------------------------------------------------------- /examples/basic.rs: -------------------------------------------------------------------------------- 1 | // Crate Dependencies --------------------------------------------------------- 2 | // ---------------------------------------------------------------------------- 3 | extern crate cursive; 4 | extern crate cursive_table_view; 5 | extern crate rand; 6 | 7 | // STD Dependencies ----------------------------------------------------------- 8 | // ---------------------------------------------------------------------------- 9 | use std::cmp::Ordering; 10 | 11 | // External Dependencies ------------------------------------------------------ 12 | // ---------------------------------------------------------------------------- 13 | use cursive::align::HAlign; 14 | use cursive::traits::*; 15 | use cursive::views::{Dialog, TextView}; 16 | use cursive::Cursive; 17 | use rand::Rng; 18 | 19 | // Modules -------------------------------------------------------------------- 20 | // ---------------------------------------------------------------------------- 21 | use cursive_table_view::{TableView, TableViewItem}; 22 | 23 | #[derive(Copy, Clone, PartialEq, Eq, Hash)] 24 | enum BasicColumn { 25 | Name, 26 | Count, 27 | Rate, 28 | } 29 | 30 | impl BasicColumn { 31 | fn as_str(&self) -> &str { 32 | match *self { 33 | BasicColumn::Name => "Name", 34 | BasicColumn::Count => "Count", 35 | BasicColumn::Rate => "Rate", 36 | } 37 | } 38 | } 39 | 40 | #[derive(Clone, Debug)] 41 | struct Foo { 42 | name: String, 43 | count: usize, 44 | rate: usize, 45 | } 46 | 47 | impl TableViewItem for Foo { 48 | fn to_column(&self, column: BasicColumn) -> String { 49 | match column { 50 | BasicColumn::Name => self.name.to_string(), 51 | BasicColumn::Count => format!("{}", self.count), 52 | BasicColumn::Rate => format!("{}", self.rate), 53 | } 54 | } 55 | 56 | fn cmp(&self, other: &Self, column: BasicColumn) -> Ordering 57 | where 58 | Self: Sized, 59 | { 60 | match column { 61 | BasicColumn::Name => self.name.cmp(&other.name), 62 | BasicColumn::Count => self.count.cmp(&other.count), 63 | BasicColumn::Rate => self.rate.cmp(&other.rate), 64 | } 65 | } 66 | } 67 | 68 | fn main() { 69 | let mut rng = rand::thread_rng(); 70 | 71 | let mut siv = cursive::default(); 72 | let mut table = TableView::::new() 73 | .column(BasicColumn::Name, "Name", |c| c.width_percent(20)) 74 | .column(BasicColumn::Count, "Count", |c| c.align(HAlign::Center)) 75 | .column(BasicColumn::Rate, "Rate", |c| { 76 | c.ordering(Ordering::Greater) 77 | .align(HAlign::Right) 78 | .width_percent(20) 79 | }); 80 | 81 | let mut items = Vec::new(); 82 | for i in 0..50 { 83 | items.push(Foo { 84 | name: format!("Name {}", i), 85 | count: rng.gen_range(0..=255), 86 | rate: rng.gen_range(0..=255), 87 | }); 88 | } 89 | 90 | table.set_items(items); 91 | 92 | table.set_on_sort(|siv: &mut Cursive, column: BasicColumn, order: Ordering| { 93 | siv.add_layer( 94 | Dialog::around(TextView::new(format!("{} / {:?}", column.as_str(), order))) 95 | .title("Sorted by") 96 | .button("Close", |s| { 97 | s.pop_layer(); 98 | }), 99 | ); 100 | }); 101 | 102 | table.set_on_submit(|siv: &mut Cursive, row: usize, index: usize| { 103 | let value = siv 104 | .call_on_name("table", move |table: &mut TableView| { 105 | format!("{:?}", table.borrow_item(index).unwrap()) 106 | }) 107 | .unwrap(); 108 | 109 | siv.add_layer( 110 | Dialog::around(TextView::new(value)) 111 | .title(format!("Removing row # {}", row)) 112 | .button("Close", move |s| { 113 | s.call_on_name("table", |table: &mut TableView| { 114 | table.remove_item(index); 115 | }); 116 | s.pop_layer(); 117 | }), 118 | ); 119 | }); 120 | 121 | siv.add_layer(Dialog::around(table.with_name("table").min_size((50, 20))).title("Table View")); 122 | 123 | siv.run(); 124 | } 125 | -------------------------------------------------------------------------------- /examples/double.rs: -------------------------------------------------------------------------------- 1 | // Crate Dependencies --------------------------------------------------------- 2 | // ---------------------------------------------------------------------------- 3 | extern crate cursive; 4 | extern crate cursive_table_view; 5 | extern crate rand; 6 | 7 | // STD Dependencies ----------------------------------------------------------- 8 | // ---------------------------------------------------------------------------- 9 | use std::cmp::Ordering; 10 | 11 | // External Dependencies ------------------------------------------------------ 12 | // ---------------------------------------------------------------------------- 13 | use cursive::align::HAlign; 14 | use cursive::direction::Orientation; 15 | use cursive::traits::*; 16 | use cursive::views::{Dialog, DummyView, LinearLayout, ResizedView}; 17 | use rand::Rng; 18 | 19 | // Modules -------------------------------------------------------------------- 20 | // ---------------------------------------------------------------------------- 21 | use cursive_table_view::{TableView, TableViewItem}; 22 | 23 | #[derive(Copy, Clone, PartialEq, Eq, Hash)] 24 | enum BasicColumn { 25 | Name, 26 | Count, 27 | Rate, 28 | } 29 | 30 | #[derive(Clone, Debug)] 31 | struct Foo { 32 | name: String, 33 | count: usize, 34 | rate: usize, 35 | } 36 | 37 | impl TableViewItem for Foo { 38 | fn to_column(&self, column: BasicColumn) -> String { 39 | match column { 40 | BasicColumn::Name => self.name.to_string(), 41 | BasicColumn::Count => format!("{}", self.count), 42 | BasicColumn::Rate => format!("{}", self.rate), 43 | } 44 | } 45 | 46 | fn cmp(&self, other: &Self, column: BasicColumn) -> Ordering 47 | where 48 | Self: Sized, 49 | { 50 | match column { 51 | BasicColumn::Name => self.name.cmp(&other.name), 52 | BasicColumn::Count => self.count.cmp(&other.count), 53 | BasicColumn::Rate => self.rate.cmp(&other.rate), 54 | } 55 | } 56 | } 57 | 58 | fn main() { 59 | let mut siv = cursive::default(); 60 | 61 | let mut layout = LinearLayout::new(Orientation::Horizontal); 62 | layout.add_child(create_table().min_size((32, 20))); 63 | layout.add_child(ResizedView::with_fixed_size((4, 0), DummyView)); 64 | layout.add_child(create_table().min_size((32, 20))); 65 | 66 | siv.add_layer(Dialog::around(layout).title("Table View Demo")); 67 | 68 | siv.run(); 69 | } 70 | 71 | fn create_table() -> TableView { 72 | let mut items = Vec::new(); 73 | let mut rng = rand::thread_rng(); 74 | 75 | for i in 0..50 { 76 | items.push(Foo { 77 | name: format!("Name {}", i), 78 | count: rng.gen_range(0..=255), 79 | rate: rng.gen_range(0..=255), 80 | }); 81 | } 82 | 83 | TableView::::new() 84 | .column(BasicColumn::Name, "Name", |c| c.width_percent(20)) 85 | .column(BasicColumn::Count, "Count", |c| c.align(HAlign::Center)) 86 | .column(BasicColumn::Rate, "Rate", |c| { 87 | c.ordering(Ordering::Greater) 88 | .align(HAlign::Right) 89 | .width_percent(20) 90 | }) 91 | .items(items) 92 | } 93 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A basic table view implementation for [cursive](https://crates.io/crates/cursive). 2 | #![deny( 3 | missing_docs, 4 | missing_copy_implementations, 5 | trivial_casts, 6 | trivial_numeric_casts, 7 | unsafe_code, 8 | unused_import_braces, 9 | unused_qualifications 10 | )] 11 | 12 | // Crate Dependencies --------------------------------------------------------- 13 | extern crate cursive_core as cursive; 14 | 15 | // STD Dependencies ----------------------------------------------------------- 16 | use std::cmp::{self, Ordering}; 17 | use std::collections::HashMap; 18 | use std::hash::Hash; 19 | use std::sync::Arc; 20 | 21 | // External Dependencies ------------------------------------------------------ 22 | use cursive::{ 23 | align::HAlign, 24 | direction::Direction, 25 | event::{Callback, Event, EventResult, Key, MouseButton, MouseEvent}, 26 | theme, 27 | vec::Vec2, 28 | view::{scroll, CannotFocus, View}, 29 | Cursive, Printer, Rect, With, 30 | }; 31 | 32 | /// A trait for displaying and sorting items inside a 33 | /// [`TableView`](struct.TableView.html). 34 | pub trait TableViewItem: Clone + Sized 35 | where 36 | H: Eq + Hash + Copy + Clone + Send + Sync + 'static, 37 | { 38 | /// Method returning a string representation of the item for the 39 | /// specified column from type `H`. 40 | fn to_column(&self, column: H) -> String; 41 | 42 | /// Method comparing two items via their specified column from type `H`. 43 | fn cmp(&self, other: &Self, column: H) -> Ordering 44 | where 45 | Self: Sized; 46 | } 47 | 48 | /// Callback used when a column is sorted. 49 | /// 50 | /// It takes the column and the ordering as input. 51 | /// 52 | /// This is a private type to help readability. 53 | type OnSortCallback = Arc; 54 | 55 | /// Callback taking as argument the row and the index of an element. 56 | /// 57 | /// This is a private type to help readability. 58 | type IndexCallback = Arc; 59 | 60 | /// View to select an item among a list, supporting multiple columns for sorting. 61 | /// 62 | /// # Examples 63 | /// 64 | /// ```rust 65 | /// # extern crate cursive; 66 | /// # extern crate cursive_table_view; 67 | /// # use std::cmp::Ordering; 68 | /// # use cursive_table_view::{TableView, TableViewItem}; 69 | /// # use cursive::align::HAlign; 70 | /// # fn main() { 71 | /// // Provide a type for the table's columns 72 | /// #[derive(Copy, Clone, PartialEq, Eq, Hash)] 73 | /// enum BasicColumn { 74 | /// Name, 75 | /// Count, 76 | /// Rate 77 | /// } 78 | /// 79 | /// // Define the item type 80 | /// #[derive(Clone, Debug)] 81 | /// struct Foo { 82 | /// name: String, 83 | /// count: usize, 84 | /// rate: usize 85 | /// } 86 | /// 87 | /// impl TableViewItem for Foo { 88 | /// 89 | /// fn to_column(&self, column: BasicColumn) -> String { 90 | /// match column { 91 | /// BasicColumn::Name => self.name.to_string(), 92 | /// BasicColumn::Count => format!("{}", self.count), 93 | /// BasicColumn::Rate => format!("{}", self.rate) 94 | /// } 95 | /// } 96 | /// 97 | /// fn cmp(&self, other: &Self, column: BasicColumn) -> Ordering where Self: Sized { 98 | /// match column { 99 | /// BasicColumn::Name => self.name.cmp(&other.name), 100 | /// BasicColumn::Count => self.count.cmp(&other.count), 101 | /// BasicColumn::Rate => self.rate.cmp(&other.rate) 102 | /// } 103 | /// } 104 | /// 105 | /// } 106 | /// 107 | /// // Configure the actual table 108 | /// let table = TableView::::new() 109 | /// .column(BasicColumn::Name, "Name", |c| c.width(20)) 110 | /// .column(BasicColumn::Count, "Count", |c| c.align(HAlign::Center)) 111 | /// .column(BasicColumn::Rate, "Rate", |c| { 112 | /// c.ordering(Ordering::Greater).align(HAlign::Right).width(20) 113 | /// }) 114 | /// .default_column(BasicColumn::Name); 115 | /// # } 116 | /// ``` 117 | pub struct TableView { 118 | enabled: bool, 119 | scroll_core: scroll::Core, 120 | needs_relayout: bool, 121 | 122 | column_select: bool, 123 | columns: Vec>, 124 | column_indicies: HashMap, 125 | 126 | focus: usize, 127 | items: Vec, 128 | rows_to_items: Vec, 129 | 130 | on_sort: Option>, 131 | // TODO Pass drawing offsets into the handlers so a popup menu 132 | // can be created easily? 133 | on_submit: Option, 134 | on_select: Option, 135 | } 136 | 137 | cursive::impl_scroller!(TableView < T, H > ::scroll_core); 138 | 139 | impl Default for TableView 140 | where 141 | T: TableViewItem + PartialEq, 142 | H: Eq + Hash + Copy + Clone + Send + Sync + 'static, 143 | { 144 | /// Creates a new empty `TableView` without any columns. 145 | /// 146 | /// See [`TableView::new()`]. 147 | fn default() -> Self { 148 | Self::new() 149 | } 150 | } 151 | 152 | impl TableView 153 | where 154 | T: TableViewItem + PartialEq, 155 | H: Eq + Hash + Copy + Clone + Send + Sync + 'static, 156 | { 157 | /// Sets the contained items of the table. 158 | /// 159 | /// The currently active sort order is preserved and will be applied to all 160 | /// items. 161 | /// 162 | /// Compared to `set_items`, the current selection will be preserved. 163 | /// (But this is only available for `T: PartialEq`.) 164 | pub fn set_items_stable(&mut self, items: Vec) { 165 | // Preserve selection 166 | let new_location = self 167 | .item() 168 | .and_then(|old_item| { 169 | let old_item = &self.items[old_item]; 170 | items.iter().position(|new| new == old_item) 171 | }) 172 | .unwrap_or(0); 173 | 174 | self.set_items_and_focus(items, new_location); 175 | } 176 | } 177 | 178 | impl TableView 179 | where 180 | T: TableViewItem, 181 | H: Eq + Hash + Copy + Clone + Send + Sync + 'static, 182 | { 183 | /// Creates a new empty `TableView` without any columns. 184 | /// 185 | /// A TableView should be accompanied by a enum of type `H` representing 186 | /// the table columns. 187 | pub fn new() -> Self { 188 | Self { 189 | enabled: true, 190 | scroll_core: scroll::Core::new(), 191 | needs_relayout: true, 192 | 193 | column_select: false, 194 | columns: Vec::new(), 195 | column_indicies: HashMap::new(), 196 | 197 | focus: 0, 198 | items: Vec::new(), 199 | rows_to_items: Vec::new(), 200 | 201 | on_sort: None, 202 | on_submit: None, 203 | on_select: None, 204 | } 205 | } 206 | 207 | /// Adds a column for the specified table colum from type `H` along with 208 | /// a title for its visual display. 209 | /// 210 | /// The provided callback can be used to further configure the 211 | /// created [`TableColumn`](struct.TableColumn.html). 212 | pub fn column, C: FnOnce(TableColumn) -> TableColumn>( 213 | mut self, 214 | column: H, 215 | title: S, 216 | callback: C, 217 | ) -> Self { 218 | self.add_column(column, title, callback); 219 | self 220 | } 221 | 222 | /// Adds a column for the specified table colum from type `H` along with 223 | /// a title for its visual display. 224 | /// 225 | /// The provided callback can be used to further configure the 226 | /// created [`TableColumn`](struct.TableColumn.html). 227 | pub fn add_column, C: FnOnce(TableColumn) -> TableColumn>( 228 | &mut self, 229 | column: H, 230 | title: S, 231 | callback: C, 232 | ) { 233 | self.insert_column(self.columns.len(), column, title, callback); 234 | } 235 | 236 | /// Remove a column. 237 | pub fn remove_column(&mut self, i: usize) { 238 | // Update the existing indices 239 | for column in &self.columns[i + 1..] { 240 | *self.column_indicies.get_mut(&column.column).unwrap() -= 1; 241 | } 242 | 243 | let column = self.columns.remove(i); 244 | self.column_indicies.remove(&column.column); 245 | self.needs_relayout = true; 246 | } 247 | 248 | /// Adds a column for the specified table colum from type `H` along with 249 | /// a title for its visual display. 250 | /// 251 | /// The provided callback can be used to further configure the 252 | /// created [`TableColumn`](struct.TableColumn.html). 253 | pub fn insert_column, C: FnOnce(TableColumn) -> TableColumn>( 254 | &mut self, 255 | i: usize, 256 | column: H, 257 | title: S, 258 | callback: C, 259 | ) { 260 | // Update all existing indices 261 | for column in &self.columns[i..] { 262 | *self.column_indicies.get_mut(&column.column).unwrap() += 1; 263 | } 264 | 265 | self.column_indicies.insert(column, i); 266 | self.columns 267 | .insert(i, callback(TableColumn::new(column, title.into()))); 268 | 269 | // Make the first colum the default one 270 | if self.columns.len() == 1 { 271 | self.set_default_column(column); 272 | } 273 | self.needs_relayout = true; 274 | } 275 | 276 | /// Sets the initially active column of the table. 277 | pub fn default_column(mut self, column: H) -> Self { 278 | self.set_default_column(column); 279 | self 280 | } 281 | 282 | /// Sets the initially active column of the table. 283 | pub fn set_default_column(&mut self, column: H) { 284 | if self.column_indicies.contains_key(&column) { 285 | for c in &mut self.columns { 286 | c.selected = c.column == column; 287 | if c.selected { 288 | c.order = c.default_order; 289 | } else { 290 | c.order = Ordering::Equal; 291 | } 292 | } 293 | } 294 | } 295 | 296 | /// Sorts the table using the specified table `column` and the passed 297 | /// `order`. 298 | pub fn sort_by(&mut self, column: H, order: Ordering) { 299 | if self.column_indicies.contains_key(&column) { 300 | for c in &mut self.columns { 301 | // Move selection back to the sorted column. 302 | c.selected = c.column == column; 303 | if c.selected { 304 | c.order = order; 305 | } else { 306 | c.order = Ordering::Equal; 307 | } 308 | } 309 | } 310 | 311 | self.sort_items(column, order); 312 | } 313 | 314 | /// Sorts the table using the currently active column and its 315 | /// ordering. 316 | pub fn sort(&mut self) { 317 | if let Some((column, order)) = self.order() { 318 | self.sort_items(column, order); 319 | } 320 | } 321 | 322 | /// Returns the currently active column that is used for sorting 323 | /// along with its ordering. 324 | /// 325 | /// Might return `None` if there are currently no items in the table 326 | /// and it has not been sorted yet. 327 | pub fn order(&self) -> Option<(H, Ordering)> { 328 | for c in &self.columns { 329 | if c.order != Ordering::Equal { 330 | return Some((c.column, c.order)); 331 | } 332 | } 333 | None 334 | } 335 | 336 | /// Disables this view. 337 | /// 338 | /// A disabled view cannot be selected. 339 | pub fn disable(&mut self) { 340 | self.enabled = false; 341 | } 342 | 343 | /// Re-enables this view. 344 | pub fn enable(&mut self) { 345 | self.enabled = true; 346 | } 347 | 348 | /// Enable or disable this view. 349 | pub fn set_enabled(&mut self, enabled: bool) { 350 | self.enabled = enabled; 351 | } 352 | 353 | /// Returns `true` if this view is enabled. 354 | pub fn is_enabled(&self) -> bool { 355 | self.enabled 356 | } 357 | 358 | /// Sets a callback to be used when a selected column is sorted by 359 | /// pressing ``. 360 | /// 361 | /// # Example 362 | /// 363 | /// ```ignore 364 | /// table.set_on_sort(|siv: &mut Cursive, column: BasicColumn, order: Ordering| { 365 | /// 366 | /// }); 367 | /// ``` 368 | pub fn set_on_sort(&mut self, cb: F) 369 | where 370 | F: Fn(&mut Cursive, H, Ordering) + Send + Sync + 'static, 371 | { 372 | self.on_sort = Some(Arc::new(move |s, h, o| cb(s, h, o))); 373 | } 374 | 375 | /// Sets a callback to be used when a selected column is sorted by 376 | /// pressing ``. 377 | /// 378 | /// Chainable variant. 379 | /// 380 | /// # Example 381 | /// 382 | /// ```ignore 383 | /// table.on_sort(|siv: &mut Cursive, column: BasicColumn, order: Ordering| { 384 | /// 385 | /// }); 386 | /// ``` 387 | pub fn on_sort(self, cb: F) -> Self 388 | where 389 | F: Fn(&mut Cursive, H, Ordering) + Send + Sync + 'static, 390 | { 391 | self.with(|t| t.set_on_sort(cb)) 392 | } 393 | 394 | /// Sets a callback to be used when `` is pressed while an item 395 | /// is selected. 396 | /// 397 | /// Both the currently selected row and the index of the corresponding item 398 | /// within the underlying storage vector will be given to the callback. 399 | /// 400 | /// # Example 401 | /// 402 | /// ```ignore 403 | /// table.set_on_submit(|siv: &mut Cursive, row: usize, index: usize| { 404 | /// 405 | /// }); 406 | /// ``` 407 | pub fn set_on_submit(&mut self, cb: F) 408 | where 409 | F: Fn(&mut Cursive, usize, usize) + Send + Sync + 'static, 410 | { 411 | self.on_submit = Some(Arc::new(move |s, row, index| cb(s, row, index))); 412 | } 413 | 414 | /// Sets a callback to be used when `` is pressed while an item 415 | /// is selected. 416 | /// 417 | /// Both the currently selected row and the index of the corresponding item 418 | /// within the underlying storage vector will be given to the callback. 419 | /// 420 | /// Chainable variant. 421 | /// 422 | /// # Example 423 | /// 424 | /// ```ignore 425 | /// table.on_submit(|siv: &mut Cursive, row: usize, index: usize| { 426 | /// 427 | /// }); 428 | /// ``` 429 | pub fn on_submit(self, cb: F) -> Self 430 | where 431 | F: Fn(&mut Cursive, usize, usize) + Send + Sync + 'static, 432 | { 433 | self.with(|t| t.set_on_submit(cb)) 434 | } 435 | 436 | /// Sets a callback to be used when an item is selected. 437 | /// 438 | /// Both the currently selected row and the index of the corresponding item 439 | /// within the underlying storage vector will be given to the callback. 440 | /// 441 | /// # Example 442 | /// 443 | /// ```ignore 444 | /// table.set_on_select(|siv: &mut Cursive, row: usize, index: usize| { 445 | /// 446 | /// }); 447 | /// ``` 448 | pub fn set_on_select(&mut self, cb: F) 449 | where 450 | F: Fn(&mut Cursive, usize, usize) + Send + Sync + 'static, 451 | { 452 | self.on_select = Some(Arc::new(move |s, row, index| cb(s, row, index))); 453 | } 454 | 455 | /// Sets a callback to be used when an item is selected. 456 | /// 457 | /// Both the currently selected row and the index of the corresponding item 458 | /// within the underlying storage vector will be given to the callback. 459 | /// 460 | /// Chainable variant. 461 | /// 462 | /// # Example 463 | /// 464 | /// ```ignore 465 | /// table.on_select(|siv: &mut Cursive, row: usize, index: usize| { 466 | /// 467 | /// }); 468 | /// ``` 469 | pub fn on_select(self, cb: F) -> Self 470 | where 471 | F: Fn(&mut Cursive, usize, usize) + Send + Sync + 'static, 472 | { 473 | self.with(|t| t.set_on_select(cb)) 474 | } 475 | 476 | /// Removes all items from this view. 477 | pub fn clear(&mut self) { 478 | self.items.clear(); 479 | self.rows_to_items.clear(); 480 | self.focus = 0; 481 | self.needs_relayout = true; 482 | } 483 | 484 | /// Returns the number of items in this table. 485 | pub fn len(&self) -> usize { 486 | self.items.len() 487 | } 488 | 489 | /// Returns `true` if this table has no items. 490 | pub fn is_empty(&self) -> bool { 491 | self.items.is_empty() 492 | } 493 | 494 | /// Returns the index of the currently selected table row. 495 | pub fn row(&self) -> Option { 496 | if self.items.is_empty() { 497 | None 498 | } else { 499 | Some(self.focus) 500 | } 501 | } 502 | 503 | /// Selects the row at the specified index. 504 | pub fn set_selected_row(&mut self, row_index: usize) { 505 | self.focus = row_index; 506 | self.scroll_core.scroll_to_y(row_index); 507 | } 508 | 509 | /// Selects the row at the specified index. 510 | /// 511 | /// Chainable variant. 512 | pub fn selected_row(self, row_index: usize) -> Self { 513 | self.with(|t| t.set_selected_row(row_index)) 514 | } 515 | 516 | /// Sets the contained items of the table. 517 | /// 518 | /// The currently active sort order is preserved and will be applied to all 519 | /// items. 520 | pub fn set_items(&mut self, items: Vec) { 521 | self.set_items_and_focus(items, 0); 522 | } 523 | 524 | fn set_items_and_focus(&mut self, items: Vec, new_location: usize) { 525 | self.items = items; 526 | self.rows_to_items = Vec::with_capacity(self.items.len()); 527 | 528 | for i in 0..self.items.len() { 529 | self.rows_to_items.push(i); 530 | } 531 | 532 | if let Some((column, order)) = self.order() { 533 | // Preserve the selected column if possible. 534 | let selected_column = self.columns.iter().find(|c| c.selected).map(|c| c.column); 535 | self.sort_by(column, order); 536 | if let Some(column) = selected_column { 537 | for c in &mut self.columns { 538 | c.selected = c.column == column; 539 | } 540 | } 541 | } 542 | 543 | self.set_selected_item(new_location); 544 | self.needs_relayout = true; 545 | } 546 | 547 | /// Sets the contained items of the table. 548 | /// 549 | /// The order of the items will be preserved even when the table is sorted. 550 | /// 551 | /// Chainable variant. 552 | pub fn items(self, items: Vec) -> Self { 553 | self.with(|t| t.set_items(items)) 554 | } 555 | 556 | /// Returns a immmutable reference to the item at the specified index 557 | /// within the underlying storage vector. 558 | pub fn borrow_item(&self, index: usize) -> Option<&T> { 559 | self.items.get(index) 560 | } 561 | 562 | /// Returns a mutable reference to the item at the specified index within 563 | /// the underlying storage vector. 564 | pub fn borrow_item_mut(&mut self, index: usize) -> Option<&mut T> { 565 | self.items.get_mut(index) 566 | } 567 | 568 | /// Returns a immmutable reference to the items contained within the table. 569 | pub fn borrow_items(&mut self) -> &[T] { 570 | &self.items 571 | } 572 | 573 | /// Returns a mutable reference to the items contained within the table. 574 | /// 575 | /// Can be used to modify the items in place. 576 | pub fn borrow_items_mut(&mut self) -> &mut [T] { 577 | self.needs_relayout = true; 578 | &mut self.items 579 | } 580 | 581 | /// Returns the index of the currently selected item within the underlying 582 | /// storage vector. 583 | pub fn item(&self) -> Option { 584 | self.rows_to_items.get(self.focus).copied() 585 | } 586 | 587 | /// Selects the item at the specified index within the underlying storage 588 | /// vector. 589 | pub fn set_selected_item(&mut self, item_index: usize) { 590 | // TODO optimize the performance for very large item lists 591 | if item_index < self.items.len() { 592 | for (row, item) in self.rows_to_items.iter().enumerate() { 593 | if *item == item_index { 594 | self.focus = row; 595 | self.scroll_core.scroll_to_y(row); 596 | break; 597 | } 598 | } 599 | } 600 | } 601 | 602 | /// Selects the item at the specified index within the underlying storage 603 | /// vector. 604 | /// 605 | /// Chainable variant. 606 | pub fn selected_item(self, item_index: usize) -> Self { 607 | self.with(|t| t.set_selected_item(item_index)) 608 | } 609 | 610 | /// Inserts a new item into the table. 611 | /// 612 | /// The currently active sort order is preserved and will be applied to the 613 | /// newly inserted item. 614 | /// 615 | /// If no sort option is set, the item will be added to the end of the table. 616 | pub fn insert_item(&mut self, item: T) { 617 | self.insert_item_at(self.items.len(), item); 618 | } 619 | 620 | /// Inserts a new item into the table. 621 | /// 622 | /// The currently active sort order is preserved and will be applied to the 623 | /// newly inserted item. 624 | /// 625 | /// If no sort option is set, the item will be inserted at the given index. 626 | /// 627 | /// # Panics 628 | /// 629 | /// If `index > self.len()`. 630 | pub fn insert_item_at(&mut self, index: usize, item: T) { 631 | self.items.push(item); 632 | 633 | // Here we know self.items.len() > 0 634 | self.rows_to_items.insert(index, self.items.len() - 1); 635 | 636 | if let Some((column, order)) = self.order() { 637 | self.sort_by(column, order); 638 | } 639 | self.needs_relayout = true; 640 | } 641 | 642 | /// Removes the item at the specified index within the underlying storage 643 | /// vector and returns it. 644 | pub fn remove_item(&mut self, item_index: usize) -> Option { 645 | if item_index < self.items.len() { 646 | // Move the selection if the currently selected item gets removed 647 | if let Some(selected_index) = self.item() { 648 | if selected_index == item_index { 649 | self.focus_up(1); 650 | } 651 | } 652 | 653 | // Remove the sorted reference to the item 654 | self.rows_to_items.retain(|i| *i != item_index); 655 | 656 | // Adjust remaining references 657 | for ref_index in &mut self.rows_to_items { 658 | if *ref_index > item_index { 659 | *ref_index -= 1; 660 | } 661 | } 662 | self.needs_relayout = true; 663 | 664 | // Remove actual item from the underlying storage 665 | Some(self.items.remove(item_index)) 666 | } else { 667 | None 668 | } 669 | } 670 | 671 | /// Removes all items from the underlying storage and returns them. 672 | pub fn take_items(&mut self) -> Vec { 673 | self.set_selected_row(0); 674 | self.rows_to_items.clear(); 675 | self.needs_relayout = true; 676 | self.items.drain(0..).collect() 677 | } 678 | } 679 | 680 | impl TableView 681 | where 682 | T: TableViewItem, 683 | H: Eq + Hash + Copy + Clone + Send + Sync + 'static, 684 | { 685 | fn draw_columns)>( 686 | &self, 687 | printer: &Printer, 688 | sep: &str, 689 | callback: C, 690 | ) { 691 | let mut column_offset = 0; 692 | let column_count = self.columns.len(); 693 | for (index, column) in self.columns.iter().enumerate() { 694 | let printer = &printer.offset((column_offset, 0)).focused(true); 695 | 696 | callback(printer, column); 697 | 698 | if 1 + index < column_count { 699 | printer.print((column.width + 1, 0), sep); 700 | } 701 | 702 | column_offset += column.width + 3; 703 | } 704 | } 705 | 706 | fn sort_items(&mut self, column: H, order: Ordering) { 707 | if !self.is_empty() { 708 | let old_item = self.item(); 709 | 710 | let mut rows_to_items = self.rows_to_items.clone(); 711 | rows_to_items.sort_by(|a, b| { 712 | if order == Ordering::Less { 713 | self.items[*a].cmp(&self.items[*b], column) 714 | } else { 715 | self.items[*b].cmp(&self.items[*a], column) 716 | } 717 | }); 718 | self.rows_to_items = rows_to_items; 719 | 720 | if let Some(old_item) = old_item { 721 | self.set_selected_item(old_item); 722 | } 723 | } 724 | } 725 | 726 | fn draw_item(&self, printer: &Printer, i: usize) { 727 | self.draw_columns(printer, "┆ ", |printer, column| { 728 | let value = self.items[self.rows_to_items[i]].to_column(column.column); 729 | column.draw_row(printer, value.as_str()); 730 | }); 731 | } 732 | 733 | fn on_focus_change(&self) -> EventResult { 734 | let row = self.row().unwrap(); 735 | let index = self.item().unwrap(); 736 | EventResult::Consumed( 737 | self.on_select 738 | .clone() 739 | .map(|cb| Callback::from_fn(move |s| cb(s, row, index))), 740 | ) 741 | } 742 | 743 | fn focus_up(&mut self, n: usize) { 744 | self.focus -= cmp::min(self.focus, n); 745 | } 746 | 747 | fn focus_down(&mut self, n: usize) { 748 | self.focus = cmp::min(self.focus + n, self.items.len().saturating_sub(1)); 749 | } 750 | 751 | fn active_column(&self) -> usize { 752 | self.columns.iter().position(|c| c.selected).unwrap_or(0) 753 | } 754 | 755 | fn column_cancel(&mut self) { 756 | self.column_select = false; 757 | for column in &mut self.columns { 758 | column.selected = column.order != Ordering::Equal; 759 | } 760 | } 761 | 762 | fn column_next(&mut self) -> bool { 763 | let column = self.active_column(); 764 | if 1 + column < self.columns.len() { 765 | self.columns[column].selected = false; 766 | self.columns[column + 1].selected = true; 767 | true 768 | } else { 769 | false 770 | } 771 | } 772 | 773 | fn column_prev(&mut self) -> bool { 774 | let column = self.active_column(); 775 | if column > 0 { 776 | self.columns[column].selected = false; 777 | self.columns[column - 1].selected = true; 778 | true 779 | } else { 780 | false 781 | } 782 | } 783 | 784 | fn column_select(&mut self) -> EventResult { 785 | let next = self.active_column(); 786 | let column = self.columns[next].column; 787 | let current = self 788 | .columns 789 | .iter() 790 | .position(|c| c.order != Ordering::Equal) 791 | .unwrap_or(0); 792 | 793 | let order = if current != next { 794 | self.columns[next].default_order 795 | } else if self.columns[current].order == Ordering::Less { 796 | Ordering::Greater 797 | } else { 798 | Ordering::Less 799 | }; 800 | 801 | self.sort_by(column, order); 802 | 803 | if self.on_sort.is_some() { 804 | let c = &self.columns[self.active_column()]; 805 | let column = c.column; 806 | let order = c.order; 807 | 808 | let cb = self.on_sort.clone().unwrap(); 809 | EventResult::with_cb(move |s| cb(s, column, order)) 810 | } else { 811 | EventResult::Consumed(None) 812 | } 813 | } 814 | 815 | fn column_for_x(&self, mut x: usize) -> Option { 816 | for (i, col) in self.columns.iter().enumerate() { 817 | x = match x.checked_sub(col.width) { 818 | None => return Some(i), 819 | Some(x) => x.checked_sub(3)?, 820 | }; 821 | } 822 | 823 | None 824 | } 825 | 826 | fn draw_content(&self, printer: &Printer) { 827 | for i in 0..self.rows_to_items.len() { 828 | let printer = printer.offset((0, i)); 829 | let color = if i == self.focus && self.enabled { 830 | if !self.column_select && self.enabled && printer.focused { 831 | theme::ColorStyle::highlight() 832 | } else { 833 | theme::ColorStyle::highlight_inactive() 834 | } 835 | } else { 836 | theme::ColorStyle::primary() 837 | }; 838 | 839 | if i < self.items.len() { 840 | printer.with_color(color, |printer| { 841 | self.draw_item(printer, i); 842 | }); 843 | } 844 | } 845 | } 846 | 847 | fn layout_content(&mut self, size: Vec2) { 848 | let column_count = self.columns.len(); 849 | 850 | // Split up all columns into sized / unsized groups 851 | let (mut sized, mut usized): (Vec<&mut TableColumn>, Vec<&mut TableColumn>) = self 852 | .columns 853 | .iter_mut() 854 | .partition(|c| c.requested_width.is_some()); 855 | 856 | // Subtract one for the seperators between our columns (that's column_count - 1) 857 | let available_width = size.x.saturating_sub(column_count.saturating_sub(1) * 3); 858 | 859 | // Calculate widths for all requested columns 860 | let mut remaining_width = available_width; 861 | for column in &mut sized { 862 | column.width = match *column.requested_width.as_ref().unwrap() { 863 | TableColumnWidth::Percent(width) => cmp::min( 864 | (size.x as f32 / 100.0 * width as f32).ceil() as usize, 865 | remaining_width, 866 | ), 867 | TableColumnWidth::Absolute(width) => width, 868 | }; 869 | remaining_width = remaining_width.saturating_sub(column.width); 870 | } 871 | 872 | // Spread the remaining with across the unsized columns 873 | let remaining_columns = usized.len(); 874 | for column in &mut usized { 875 | column.width = (remaining_width as f32 / remaining_columns as f32).floor() as usize; 876 | } 877 | 878 | self.needs_relayout = false; 879 | } 880 | 881 | fn content_required_size(&mut self, req: Vec2) -> Vec2 { 882 | Vec2::new(req.x, self.rows_to_items.len()) 883 | } 884 | 885 | fn on_inner_event(&mut self, event: Event) -> EventResult { 886 | let last_focus = self.focus; 887 | match event { 888 | Event::Key(Key::Right) => { 889 | if self.column_select { 890 | if !self.column_next() { 891 | return EventResult::Ignored; 892 | } 893 | } else { 894 | self.column_select = true; 895 | } 896 | } 897 | Event::Key(Key::Left) => { 898 | if self.column_select { 899 | if !self.column_prev() { 900 | return EventResult::Ignored; 901 | } 902 | } else { 903 | self.column_select = true; 904 | } 905 | } 906 | Event::Key(Key::Up) if self.focus > 0 || self.column_select => { 907 | if self.column_select { 908 | self.column_cancel(); 909 | } else { 910 | self.focus_up(1); 911 | } 912 | } 913 | Event::Key(Key::Down) if self.focus + 1 < self.items.len() || self.column_select => { 914 | if self.column_select { 915 | self.column_cancel(); 916 | } else { 917 | self.focus_down(1); 918 | } 919 | } 920 | Event::Key(Key::PageUp) => { 921 | self.column_cancel(); 922 | self.focus_up(10); 923 | } 924 | Event::Key(Key::PageDown) => { 925 | self.column_cancel(); 926 | self.focus_down(10); 927 | } 928 | Event::Key(Key::Home) => { 929 | self.column_cancel(); 930 | self.focus = 0; 931 | } 932 | Event::Key(Key::End) => { 933 | self.column_cancel(); 934 | self.focus = self.items.len().saturating_sub(1); 935 | } 936 | Event::Key(Key::Enter) => { 937 | if self.column_select { 938 | return self.column_select(); 939 | } else if !self.is_empty() && self.on_submit.is_some() { 940 | return self.on_submit_event(); 941 | } 942 | } 943 | Event::Mouse { 944 | position, 945 | offset, 946 | event: MouseEvent::Press(MouseButton::Left), 947 | } if !self.is_empty() 948 | && position 949 | .checked_sub(offset) 950 | .map_or(false, |p| p.y == self.focus) => 951 | { 952 | self.column_cancel(); 953 | return self.on_submit_event(); 954 | } 955 | Event::Mouse { 956 | position, 957 | offset, 958 | event: MouseEvent::Press(_), 959 | } if !self.is_empty() => match position.checked_sub(offset) { 960 | Some(position) if position.y < self.rows_to_items.len() => { 961 | self.column_cancel(); 962 | self.focus = position.y; 963 | } 964 | _ => return EventResult::Ignored, 965 | }, 966 | _ => return EventResult::Ignored, 967 | } 968 | 969 | let focus = self.focus; 970 | 971 | if self.column_select { 972 | EventResult::Consumed(None) 973 | } else if !self.is_empty() && last_focus != focus { 974 | self.on_focus_change() 975 | } else { 976 | EventResult::Ignored 977 | } 978 | } 979 | 980 | fn inner_important_area(&self, size: Vec2) -> Rect { 981 | Rect::from_size((0, self.focus), (size.x, 1)) 982 | } 983 | 984 | fn on_submit_event(&mut self) -> EventResult { 985 | if let Some(ref cb) = &self.on_submit { 986 | let cb = Arc::clone(cb); 987 | let row = self.row().unwrap(); 988 | let index = self.item().unwrap(); 989 | return EventResult::Consumed(Some(Callback::from_fn(move |s| cb(s, row, index)))); 990 | } 991 | EventResult::Ignored 992 | } 993 | } 994 | 995 | impl View for TableView 996 | where 997 | T: TableViewItem + Send + Sync + 'static, 998 | H: Eq + Hash + Copy + Clone + Send + Sync + 'static, 999 | { 1000 | fn draw(&self, printer: &Printer) { 1001 | self.draw_columns(printer, "╷ ", |printer, column| { 1002 | let color = if self.enabled && (column.order != Ordering::Equal || column.selected) { 1003 | if self.column_select && column.selected && self.enabled && printer.focused { 1004 | theme::ColorStyle::highlight() 1005 | } else { 1006 | theme::ColorStyle::highlight_inactive() 1007 | } 1008 | } else { 1009 | theme::ColorStyle::primary() 1010 | }; 1011 | 1012 | printer.with_color(color, |printer| { 1013 | column.draw_header(printer); 1014 | }); 1015 | }); 1016 | 1017 | self.draw_columns( 1018 | &printer.offset((0, 1)).focused(true), 1019 | "┴─", 1020 | |printer, column| { 1021 | printer.print_hline((0, 0), column.width + 1, "─"); 1022 | }, 1023 | ); 1024 | 1025 | // Extend the vertical bars to the end of the view 1026 | for y in 2..printer.size.y { 1027 | self.draw_columns(&printer.offset((0, y)), "┆ ", |_, _| ()); 1028 | } 1029 | 1030 | let printer = &printer.offset((0, 2)).focused(true); 1031 | scroll::draw(self, printer, Self::draw_content); 1032 | } 1033 | 1034 | fn layout(&mut self, size: Vec2) { 1035 | scroll::layout( 1036 | self, 1037 | size.saturating_sub((0, 2)), 1038 | self.needs_relayout, 1039 | Self::layout_content, 1040 | Self::content_required_size, 1041 | ); 1042 | } 1043 | 1044 | fn take_focus(&mut self, _: Direction) -> Result { 1045 | self.enabled.then(EventResult::consumed).ok_or(CannotFocus) 1046 | } 1047 | 1048 | fn on_event(&mut self, event: Event) -> EventResult { 1049 | if !self.enabled { 1050 | return EventResult::Ignored; 1051 | } 1052 | 1053 | match event { 1054 | Event::Mouse { 1055 | position, 1056 | offset, 1057 | event: MouseEvent::Press(MouseButton::Left), 1058 | } if position.checked_sub(offset).map_or(false, |p| p.y == 0) => { 1059 | if let Some(position) = position.checked_sub(offset) { 1060 | if let Some(col) = self.column_for_x(position.x) { 1061 | if self.column_select && self.columns[col].selected { 1062 | return self.column_select(); 1063 | } else { 1064 | let active = self.active_column(); 1065 | self.columns[active].selected = false; 1066 | self.columns[col].selected = true; 1067 | self.column_select = true; 1068 | } 1069 | } 1070 | } 1071 | EventResult::Ignored 1072 | } 1073 | event => scroll::on_event( 1074 | self, 1075 | event.relativized((0, 2)), 1076 | Self::on_inner_event, 1077 | Self::inner_important_area, 1078 | ), 1079 | } 1080 | } 1081 | 1082 | fn important_area(&self, size: Vec2) -> Rect { 1083 | self.inner_important_area(size.saturating_sub((0, 2))) + (0, 2) 1084 | } 1085 | } 1086 | 1087 | /// A type used for the construction of columns in a 1088 | /// [`TableView`](struct.TableView.html). 1089 | pub struct TableColumn { 1090 | column: H, 1091 | title: String, 1092 | selected: bool, 1093 | alignment: HAlign, 1094 | order: Ordering, 1095 | width: usize, 1096 | default_order: Ordering, 1097 | requested_width: Option, 1098 | } 1099 | 1100 | enum TableColumnWidth { 1101 | Percent(usize), 1102 | Absolute(usize), 1103 | } 1104 | 1105 | impl TableColumn { 1106 | /// Sets the default ordering of the column. 1107 | pub fn ordering(mut self, order: Ordering) -> Self { 1108 | self.default_order = order; 1109 | self 1110 | } 1111 | 1112 | /// Sets the horizontal text alignment of the column. 1113 | pub fn align(mut self, alignment: HAlign) -> Self { 1114 | self.alignment = alignment; 1115 | self 1116 | } 1117 | 1118 | /// Sets how many characters of width this column will try to occupy. 1119 | pub fn width(mut self, width: usize) -> Self { 1120 | self.requested_width = Some(TableColumnWidth::Absolute(width)); 1121 | self 1122 | } 1123 | 1124 | /// Sets what percentage of the width of the entire table this column will 1125 | /// try to occupy. 1126 | pub fn width_percent(mut self, width: usize) -> Self { 1127 | self.requested_width = Some(TableColumnWidth::Percent(width)); 1128 | self 1129 | } 1130 | 1131 | fn new(column: H, title: String) -> Self { 1132 | Self { 1133 | column, 1134 | title, 1135 | selected: false, 1136 | alignment: HAlign::Left, 1137 | order: Ordering::Equal, 1138 | width: 0, 1139 | default_order: Ordering::Less, 1140 | requested_width: None, 1141 | } 1142 | } 1143 | 1144 | fn draw_header(&self, printer: &Printer) { 1145 | let order = match self.order { 1146 | Ordering::Less => "^", 1147 | Ordering::Greater => "v", 1148 | Ordering::Equal => " ", 1149 | }; 1150 | 1151 | let header = match self.alignment { 1152 | HAlign::Left => format!( 1153 | "{: format!( 1159 | "{:>width$} [{}]", 1160 | self.title, 1161 | order, 1162 | width = self.width.saturating_sub(4) 1163 | ), 1164 | HAlign::Center => format!( 1165 | "{:^width$} [{}]", 1166 | self.title, 1167 | order, 1168 | width = self.width.saturating_sub(4) 1169 | ), 1170 | }; 1171 | 1172 | printer.print((0, 0), header.as_str()); 1173 | } 1174 | 1175 | fn draw_row(&self, printer: &Printer, value: &str) { 1176 | let value = match self.alignment { 1177 | HAlign::Left => format!("{: format!("{:>width$} ", value, width = self.width), 1179 | HAlign::Center => format!("{:^width$} ", value, width = self.width), 1180 | }; 1181 | 1182 | printer.print((0, 0), value.as_str()); 1183 | } 1184 | } 1185 | 1186 | #[cfg(test)] 1187 | mod tests { 1188 | use super::*; 1189 | 1190 | #[derive(Copy, Clone, PartialEq, Eq, Hash)] 1191 | enum SimpleColumn { 1192 | Name, 1193 | } 1194 | 1195 | #[allow(dead_code)] 1196 | impl SimpleColumn { 1197 | fn as_str(&self) -> &str { 1198 | match *self { 1199 | SimpleColumn::Name => "Name", 1200 | } 1201 | } 1202 | } 1203 | 1204 | #[derive(Clone, Debug)] 1205 | struct SimpleItem { 1206 | name: String, 1207 | } 1208 | 1209 | impl TableViewItem for SimpleItem { 1210 | fn to_column(&self, column: SimpleColumn) -> String { 1211 | match column { 1212 | SimpleColumn::Name => self.name.to_string(), 1213 | } 1214 | } 1215 | 1216 | fn cmp(&self, other: &Self, column: SimpleColumn) -> Ordering 1217 | where 1218 | Self: Sized, 1219 | { 1220 | match column { 1221 | SimpleColumn::Name => self.name.cmp(&other.name), 1222 | } 1223 | } 1224 | } 1225 | 1226 | fn setup_test_table() -> TableView { 1227 | TableView::::new() 1228 | .column(SimpleColumn::Name, "Name", |c| c.width_percent(20)) 1229 | } 1230 | 1231 | #[test] 1232 | fn should_insert_into_existing_table() { 1233 | let mut simple_table = setup_test_table(); 1234 | 1235 | let mut simple_items = Vec::new(); 1236 | 1237 | for i in 1..=10 { 1238 | simple_items.push(SimpleItem { 1239 | name: format!("{} - Name", i), 1240 | }); 1241 | } 1242 | 1243 | // Insert First Batch of Items 1244 | simple_table.set_items(simple_items); 1245 | 1246 | // Test for Additional item insertion 1247 | simple_table.insert_item(SimpleItem { 1248 | name: format!("{} Name", 11), 1249 | }); 1250 | 1251 | assert!(simple_table.len() == 11); 1252 | } 1253 | 1254 | #[test] 1255 | fn should_insert_into_empty_table() { 1256 | let mut simple_table = setup_test_table(); 1257 | 1258 | // Test for First item insertion 1259 | simple_table.insert_item(SimpleItem { 1260 | name: format!("{} Name", 1), 1261 | }); 1262 | 1263 | assert!(simple_table.len() == 1); 1264 | } 1265 | } 1266 | --------------------------------------------------------------------------------