1 use std::{
2     cmp::Ordering,
3     io::{stdout, Write},
4 };
5 
6 use crossterm::{
7     cursor::MoveTo,
8     queue,
9     style::{Color, SetBackgroundColor},
10     terminal::{Clear, ClearType},
11 };
12 
13 use crate::{
14     compute_scrollbar, errors::Result, gray, Alignment, Area, CompoundStyle, MadSkin, Spacing,
15 };
16 
17 pub struct ListViewCell<'t> {
18     con: String,
19     style: &'t CompoundStyle,
20     width: usize, // length of content in chars
21 }
22 
23 pub struct Title {
24     columns: Vec<usize>, // the column(s) below this title
25 }
26 
27 pub struct ListViewColumn<'t, T> {
28     title: String,
29     min_width: usize,
30     max_width: usize,
31     spacing: Spacing,
32     extract: Box<dyn Fn(&T) -> ListViewCell<'t>>, // a function building cells from the rows
33 }
34 
35 struct Row<T> {
36     data: T,
37     displayed: bool,
38 }
39 
40 /// A filterable list whose columns can be automatically resized.
41 ///
42 ///
43 /// Notes:
44 /// * another version will allow more than one style per cell
45 /// (i.e. make the cells composites rather than compounds). Shout
46 /// out if you need that now.
47 /// * this version doesn't allow cell wrapping
48 pub struct ListView<'t, T> {
49     titles: Vec<Title>,
50     columns: Vec<ListViewColumn<'t, T>>,
51     rows: Vec<Row<T>>,
52     pub area: Area,
53     scroll: usize,
54     pub skin: &'t MadSkin,
55     filter: Option<Box<dyn Fn(&T) -> bool>>, // a function determining if the row must be displayed
56     displayed_rows_count: usize,
57     row_order: Option<Box<dyn Fn(&T, &T) -> Ordering>>,
58     selection: Option<usize>, // index of the selected line
59     selection_background: Color,
60 }
61 
62 impl<'t> ListViewCell<'t> {
new(con: String, style: &'t CompoundStyle) -> Self63     pub fn new(con: String, style: &'t CompoundStyle) -> Self {
64         let width = con.chars().count();
65         Self { con, style, width }
66     }
67 }
68 
69 impl<'t, T> ListViewColumn<'t, T> {
new( title: &str, min_width: usize, max_width: usize, extract: Box<dyn Fn(&T) -> ListViewCell<'t>>, ) -> Self70     pub fn new(
71         title: &str,
72         min_width: usize,
73         max_width: usize,
74         extract: Box<dyn Fn(&T) -> ListViewCell<'t>>,
75     ) -> Self {
76         Self {
77             title: title.to_owned(),
78             min_width,
79             max_width,
80             spacing: Spacing {
81                 width: min_width,
82                 align: Alignment::Center,
83             },
84             extract,
85         }
86     }
with_align(mut self, align: Alignment) -> Self87     pub const fn with_align(mut self, align: Alignment) -> Self {
88         self.spacing.align = align;
89         self
90     }
91 }
92 
93 impl<'t, T> ListView<'t, T> {
94     /// Create a new list view with the passed columns.
95     ///
96     /// The columns can't be changed afterwards but the area can be modified.
97     /// When two columns have the same title, those titles are merged (but
98     /// the columns below stay separated).
new(area: Area, columns: Vec<ListViewColumn<'t, T>>, skin: &'t MadSkin) -> Self99     pub fn new(area: Area, columns: Vec<ListViewColumn<'t, T>>, skin: &'t MadSkin) -> Self {
100         let mut titles: Vec<Title> = Vec::new();
101         for (column_idx, column) in columns.iter().enumerate() {
102             if let Some(last_title) = titles.last_mut() {
103                 if columns[last_title.columns[0]].title == column.title {
104                     // we merge those columns titles
105                     last_title.columns.push(column_idx);
106                     continue;
107                 }
108             }
109             // this is a new title
110             titles.push(Title {
111                 columns: vec![column_idx],
112             });
113         }
114         Self {
115             titles,
116             columns,
117             rows: Vec::new(),
118             area,
119             scroll: 0,
120             skin,
121             filter: None,
122             displayed_rows_count: 0,
123             row_order: None,
124             selection: None,
125             selection_background: gray(5),
126         }
127     }
128     /// set a comparator for row sorting
sort(&mut self, sort: Box<dyn Fn(&T, &T) -> Ordering>)129     pub fn sort(&mut self, sort: Box<dyn Fn(&T, &T) -> Ordering>) {
130         self.row_order = Some(sort);
131     }
132     /// return the height which is available for rows
133     #[inline(always)]
tbody_height(&self) -> u16134     pub const fn tbody_height(&self) -> u16 {
135         if self.area.height > 2 {
136             self.area.height - 2
137         } else {
138             self.area.height
139         }
140     }
141     /// return an option which when filled contains
142     ///  a tupple with the top and bottom of the vertical
143     ///  scrollbar. Return none when the content fits
144     ///  the available space.
145     #[inline(always)]
scrollbar(&self) -> Option<(u16, u16)>146     pub fn scrollbar(&self) -> Option<(u16, u16)> {
147         compute_scrollbar(
148             self.scroll as u16,
149             self.displayed_rows_count as u16,
150             self.tbody_height(),
151             self.area.top,
152         )
153     }
add_row(&mut self, data: T)154     pub fn add_row(&mut self, data: T) {
155         let stick_to_bottom = self.row_order.is_none() && self.do_scroll_show_bottom();
156         let displayed = match &self.filter {
157             Some(fun) => fun(&data),
158             None => true,
159         };
160         if displayed {
161             self.displayed_rows_count += 1;
162         }
163         if stick_to_bottom {
164             self.scroll_to_bottom();
165         }
166         self.rows.push(Row { data, displayed });
167         if let Some(row_order) = &self.row_order {
168             self.rows.sort_by(|a, b| row_order(&a.data, &b.data));
169         }
170     }
171     /// remove all rows (and selection).
172     ///
173     /// Keep the columns and the sort function, if any.
clear_rows(&mut self)174     pub fn clear_rows(&mut self) {
175         self.rows.clear();
176         self.scroll = 0;
177         self.displayed_rows_count = 0;
178         self.selection = None;
179     }
180     /// return both the number of displayed rows and the total number
row_counts(&self) -> (usize, usize)181     pub fn row_counts(&self) -> (usize, usize) {
182         (self.displayed_rows_count, self.rows.len())
183     }
184     /// recompute the widths of all columns.
185     /// This should be called when the area size is modified
update_dimensions(&mut self)186     pub fn update_dimensions(&mut self) {
187         let available_width: i32 =
188             i32::from(self.area.width)
189             - (self.columns.len() as i32 - 1) // we remove the separator
190             - 1; // we remove 1 to let space for the scrollbar
191         let sum_min_widths: i32 = self.columns.iter().map(|c| c.min_width as i32).sum();
192         if sum_min_widths >= available_width {
193             for i in 0..self.columns.len() {
194                 self.columns[i].spacing.width = self.columns[i].min_width;
195             }
196         } else {
197             let mut excess = available_width - sum_min_widths;
198             for i in 0..self.columns.len() {
199                 let d =
200                     ((self.columns[i].max_width - self.columns[i].min_width) as i32).min(excess);
201                 excess -= d;
202                 self.columns[i].spacing.width = self.columns[i].min_width + d as usize;
203             }
204             // there might be some excess, but it's better to have some space at right rather
205             //  than a too wide table
206         }
207     }
set_filter(&mut self, filter: Box<dyn Fn(&T) -> bool>)208     pub fn set_filter(&mut self, filter: Box<dyn Fn(&T) -> bool>) {
209         let mut count = 0;
210         for row in self.rows.iter_mut() {
211             row.displayed = filter(&row.data);
212             if row.displayed {
213                 count += 1;
214             }
215         }
216         self.scroll = 0; // something better should be done... later
217         self.displayed_rows_count = count;
218         self.filter = Some(filter);
219     }
remove_filter(&mut self)220     pub fn remove_filter(&mut self) {
221         for row in self.rows.iter_mut() {
222             row.displayed = true;
223         }
224         self.displayed_rows_count = self.rows.len();
225         self.filter = None;
226     }
227     /// write the list view on the given writer
write_on<W>(&self, w: &mut W) -> Result<()> where W: std::io::Write,228     pub fn write_on<W>(&self, w: &mut W) -> Result<()>
229     where
230         W: std::io::Write,
231     {
232         let sx = self.area.left + self.area.width;
233         let vbar = self.skin.table.compound_style.style_char('│');
234         let tee = self.skin.table.compound_style.style_char('┬');
235         let cross = self.skin.table.compound_style.style_char('┼');
236         let hbar = self.skin.table.compound_style.style_char('─');
237         // title line
238         queue!(w, MoveTo(self.area.left, self.area.top))?;
239         for (title_idx, title) in self.titles.iter().enumerate() {
240             if title_idx != 0 {
241                 vbar.queue(w)?;
242             }
243             let width = title
244                 .columns
245                 .iter()
246                 .map(|ci| self.columns[*ci].spacing.width)
247                 .sum::<usize>()
248                 + title.columns.len()
249                 - 1;
250             let spacing = Spacing {
251                 width,
252                 align: Alignment::Center,
253             };
254             spacing.write_str(
255                 w,
256                 &self.columns[title.columns[0]].title,
257                 &self.skin.headers[0].compound_style,
258             )?;
259         }
260         // separator line
261         queue!(w, MoveTo(self.area.left, self.area.top + 1))?;
262         for (title_idx, title) in self.titles.iter().enumerate() {
263             if title_idx != 0 {
264                 cross.queue(w)?;
265             }
266             for (col_idx_idx, col_idx) in title.columns.iter().enumerate() {
267                 if col_idx_idx > 0 {
268                     tee.queue(w)?;
269                 }
270                 for _ in 0..self.columns[*col_idx].spacing.width {
271                     hbar.queue(w)?;
272                 }
273             }
274         }
275         // rows, maybe scrolled
276         let mut row_idx = self.scroll as usize;
277         let scrollbar = self.scrollbar();
278         for y in 2..self.area.height {
279             queue!(w, MoveTo(self.area.left, self.area.top + y))?;
280             loop {
281                 if row_idx == self.rows.len() {
282                     queue!(w, Clear(ClearType::UntilNewLine))?;
283                     break;
284                 }
285                 if self.rows[row_idx].displayed {
286                     let selected = Some(row_idx) == self.selection;
287                     for (col_idx, col) in self.columns.iter().enumerate() {
288                         if col_idx != 0 {
289                             if selected {
290                                 queue!(w, SetBackgroundColor(self.selection_background))?;
291                             }
292                             vbar.queue(w)?;
293                         }
294                         let cell = (col.extract)(&self.rows[row_idx].data);
295                         if selected {
296                             let mut style = cell.style.clone();
297                             style.set_bg(self.selection_background);
298                             col.spacing
299                                 .write_counted_str(w, &cell.con, cell.width, &style)?;
300                         } else {
301                             col.spacing
302                                 .write_counted_str(w, &cell.con, cell.width, cell.style)?;
303                         }
304                     }
305                     row_idx += 1;
306                     break;
307                 }
308                 row_idx += 1;
309             }
310             if let Some((sctop, scbottom)) = scrollbar {
311                 queue!(w, MoveTo(sx, self.area.top + y))?;
312                 let y = y - 2;
313                 if sctop <= y && y <= scbottom {
314                     self.skin.scrollbar.thumb.queue(w)?;
315                 } else {
316                     self.skin.scrollbar.track.queue(w)?;
317                 }
318             }
319         }
320         Ok(())
321     }
322     /// display the whole list in its area
write(&self) -> Result<()>323     pub fn write(&self) -> Result<()> {
324         let mut stdout = stdout();
325         self.write_on(&mut stdout)?;
326         stdout.flush()?;
327         Ok(())
328     }
329     /// return true if the last line of the list is visible
do_scroll_show_bottom(&self) -> bool330     pub const fn do_scroll_show_bottom(&self) -> bool {
331         self.scroll + self.tbody_height() as usize >= self.displayed_rows_count
332     }
333     /// ensure the last line is visible
scroll_to_bottom(&mut self)334     pub fn scroll_to_bottom(&mut self) {
335         let body_height = self.tbody_height() as usize;
336         self.scroll = if self.displayed_rows_count > body_height {
337             self.displayed_rows_count - body_height
338         } else {
339             0
340         }
341     }
342     /// set the scroll amount.
343     /// lines_count can be negative
try_scroll_lines(&mut self, lines_count: i32)344     pub fn try_scroll_lines(&mut self, lines_count: i32) {
345         if lines_count < 0 {
346             let lines_count = -lines_count as usize;
347                 self.scroll = if lines_count >= self.scroll {
348                 0
349             } else {
350                 self.scroll - lines_count
351             };
352         } else {
353             self.scroll = (self.scroll + lines_count as usize)
354                 .min(self.displayed_rows_count - self.tbody_height() as usize + 1);
355         }
356         self.make_selection_visible();
357     }
358     /// set the scroll amount.
359     /// pages_count can be negative
try_scroll_pages(&mut self, pages_count: i32)360     pub fn try_scroll_pages(&mut self, pages_count: i32) {
361         self.try_scroll_lines(pages_count * self.tbody_height() as i32)
362     }
363     /// try to select the next visible line
try_select_next(&mut self, up: bool)364     pub fn try_select_next(&mut self, up: bool) {
365         if self.displayed_rows_count == 0 {
366             return;
367         }
368         if self.displayed_rows_count == 1 || self.selection.is_none() {
369             for i in 0..self.rows.len() {
370                 let i = (i + self.scroll as usize) % self.rows.len();
371                 if self.rows[i].displayed {
372                     self.selection = Some(i);
373                     self.make_selection_visible();
374                     return;
375                 }
376             }
377         }
378         for i in 0..self.rows.len() {
379             let delta_idx = if up { self.rows.len() - 1 - i } else { i + 1 };
380             let row_idx = (delta_idx + self.selection.unwrap()) % self.rows.len();
381             if self.rows[row_idx].displayed {
382                 self.selection = Some(row_idx);
383                 self.make_selection_visible();
384                 return;
385             }
386         }
387     }
388     /// select the first visible line (unless there's nothing).
select_first_line(&mut self)389     pub fn select_first_line(&mut self) {
390         for i in 0..self.rows.len() {
391             if self.rows[i].displayed {
392                 self.selection = Some(i);
393                 self.make_selection_visible();
394                 return;
395             }
396         }
397         self.selection = None;
398     }
399     /// select the last visible line (unless there's nothing).
select_last_line(&mut self)400     pub fn select_last_line(&mut self) {
401         for i in (0..self.rows.len()).rev() {
402             if self.rows[i].displayed {
403                 self.selection = Some(i);
404                 self.make_selection_visible();
405                 return;
406             }
407         }
408         self.selection = None;
409     }
410     /// scroll to ensure the selected line (if any) is visible.
411     ///
412     /// This is automatically called by try_scroll
413     ///  and try select functions
make_selection_visible(&mut self)414     pub fn make_selection_visible(&mut self) {
415         let tbody_height = self.tbody_height() as usize;
416         if self.displayed_rows_count <= tbody_height {
417             return; // there's no scroll
418         }
419         if let Some(sel) = self.selection {
420             if sel <= self.scroll {
421                 self.scroll = if sel > 2 { sel - 2 } else { 0 };
422             } else if sel + 1 >= self.scroll + tbody_height {
423                 self.scroll = sel - tbody_height + 2;
424             }
425         }
426     }
get_selection(&self) -> Option<&T>427     pub fn get_selection(&self) -> Option<&T> {
428         self.selection.map(|sel| &self.rows[sel].data)
429     }
has_selection(&self) -> bool430     pub const fn has_selection(&self) -> bool {
431         self.selection.is_some()
432     }
unselect(&mut self)433     pub fn unselect(&mut self) {
434         self.selection = None;
435     }
436 }
437