1 use crate::{
2     buffer::Buffer,
3     layout::{Constraint, Direction, Layout, Rect},
4     style::Style,
5     text::Text,
6     widgets::{Block, StatefulWidget, Widget},
7 };
8 use unicode_width::UnicodeWidthStr;
9 
10 /// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`].
11 ///
12 /// It can be created from anything that can be converted to a [`Text`].
13 /// ```rust
14 /// # use tui::widgets::Cell;
15 /// # use tui::style::{Style, Modifier};
16 /// # use tui::text::{Span, Spans, Text};
17 /// # use std::borrow::Cow;
18 /// Cell::from("simple string");
19 ///
20 /// Cell::from(Span::from("span"));
21 ///
22 /// Cell::from(Spans::from(vec![
23 ///     Span::raw("a vec of "),
24 ///     Span::styled("spans", Style::default().add_modifier(Modifier::BOLD))
25 /// ]));
26 ///
27 /// Cell::from(Text::from("a text"));
28 ///
29 /// Cell::from(Text::from(Cow::Borrowed("hello")));
30 /// ```
31 ///
32 /// You can apply a [`Style`] on the entire [`Cell`] using [`Cell::style`] or rely on the styling
33 /// capabilities of [`Text`].
34 #[derive(Debug, Clone, PartialEq, Default)]
35 pub struct Cell<'a> {
36     content: Text<'a>,
37     style: Style,
38 }
39 
40 impl<'a> Cell<'a> {
41     /// Set the `Style` of this cell.
42     pub fn style(mut self, style: Style) -> Self {
43         self.style = style;
44         self
45     }
46 }
47 
48 impl<'a, T> From<T> for Cell<'a>
49 where
50     T: Into<Text<'a>>,
51 {
52     fn from(content: T) -> Cell<'a> {
53         Cell {
54             content: content.into(),
55             style: Style::default(),
56         }
57     }
58 }
59 
60 /// Holds data to be displayed in a [`Table`] widget.
61 ///
62 /// A [`Row`] is a collection of cells. It can be created from simple strings:
63 /// ```rust
64 /// # use tui::widgets::Row;
65 /// Row::new(vec!["Cell1", "Cell2", "Cell3"]);
66 /// ```
67 ///
68 /// But if you need a bit more control over individual cells, you can explicity create [`Cell`]s:
69 /// ```rust
70 /// # use tui::widgets::{Row, Cell};
71 /// # use tui::style::{Style, Color};
72 /// Row::new(vec![
73 ///     Cell::from("Cell1"),
74 ///     Cell::from("Cell2").style(Style::default().fg(Color::Yellow)),
75 /// ]);
76 /// ```
77 ///
78 /// You can also construct a row from any type that can be converted into [`Text`]:
79 /// ```rust
80 /// # use std::borrow::Cow;
81 /// # use tui::widgets::Row;
82 /// Row::new(vec![
83 ///     Cow::Borrowed("hello"),
84 ///     Cow::Owned("world".to_uppercase()),
85 /// ]);
86 /// ```
87 ///
88 /// By default, a row has a height of 1 but you can change this using [`Row::height`].
89 #[derive(Debug, Clone, PartialEq, Default)]
90 pub struct Row<'a> {
91     cells: Vec<Cell<'a>>,
92     height: u16,
93     style: Style,
94     bottom_margin: u16,
95 }
96 
97 impl<'a> Row<'a> {
98     /// Creates a new [`Row`] from an iterator where items can be converted to a [`Cell`].
99     pub fn new<T>(cells: T) -> Self
100     where
101         T: IntoIterator,
102         T::Item: Into<Cell<'a>>,
103     {
104         Self {
105             height: 1,
106             cells: cells.into_iter().map(|c| c.into()).collect(),
107             style: Style::default(),
108             bottom_margin: 0,
109         }
110     }
111 
112     /// Set the fixed height of the [`Row`]. Any [`Cell`] whose content has more lines than this
113     /// height will see its content truncated.
114     pub fn height(mut self, height: u16) -> Self {
115         self.height = height;
116         self
117     }
118 
119     /// Set the [`Style`] of the entire row. This [`Style`] can be overriden by the [`Style`] of a
120     /// any individual [`Cell`] or event by their [`Text`] content.
121     pub fn style(mut self, style: Style) -> Self {
122         self.style = style;
123         self
124     }
125 
126     /// Set the bottom margin. By default, the bottom margin is `0`.
127     pub fn bottom_margin(mut self, margin: u16) -> Self {
128         self.bottom_margin = margin;
129         self
130     }
131 
132     /// Returns the total height of the row.
133     fn total_height(&self) -> u16 {
134         self.height.saturating_add(self.bottom_margin)
135     }
136 }
137 
138 /// A widget to display data in formatted columns.
139 ///
140 /// It is a collection of [`Row`]s, themselves composed of [`Cell`]s:
141 /// ```rust
142 /// # use tui::widgets::{Block, Borders, Table, Row, Cell};
143 /// # use tui::layout::Constraint;
144 /// # use tui::style::{Style, Color, Modifier};
145 /// # use tui::text::{Text, Spans, Span};
146 /// Table::new(vec![
147 ///     // Row can be created from simple strings.
148 ///     Row::new(vec!["Row11", "Row12", "Row13"]),
149 ///     // You can style the entire row.
150 ///     Row::new(vec!["Row21", "Row22", "Row23"]).style(Style::default().fg(Color::Blue)),
151 ///     // If you need more control over the styling you may need to create Cells directly
152 ///     Row::new(vec![
153 ///         Cell::from("Row31"),
154 ///         Cell::from("Row32").style(Style::default().fg(Color::Yellow)),
155 ///         Cell::from(Spans::from(vec![
156 ///             Span::raw("Row"),
157 ///             Span::styled("33", Style::default().fg(Color::Green))
158 ///         ])),
159 ///     ]),
160 ///     // If a Row need to display some content over multiple lines, you just have to change
161 ///     // its height.
162 ///     Row::new(vec![
163 ///         Cell::from("Row\n41"),
164 ///         Cell::from("Row\n42"),
165 ///         Cell::from("Row\n43"),
166 ///     ]).height(2),
167 /// ])
168 /// // You can set the style of the entire Table.
169 /// .style(Style::default().fg(Color::White))
170 /// // It has an optional header, which is simply a Row always visible at the top.
171 /// .header(
172 ///     Row::new(vec!["Col1", "Col2", "Col3"])
173 ///         .style(Style::default().fg(Color::Yellow))
174 ///         // If you want some space between the header and the rest of the rows, you can always
175 ///         // specify some margin at the bottom.
176 ///         .bottom_margin(1)
177 /// )
178 /// // As any other widget, a Table can be wrapped in a Block.
179 /// .block(Block::default().title("Table"))
180 /// // Columns widths are constrained in the same way as Layout...
181 /// .widths(&[Constraint::Length(5), Constraint::Length(5), Constraint::Length(10)])
182 /// // ...and they can be separated by a fixed spacing.
183 /// .column_spacing(1)
184 /// // If you wish to highlight a row in any specific way when it is selected...
185 /// .highlight_style(Style::default().add_modifier(Modifier::BOLD))
186 /// // ...and potentially show a symbol in front of the selection.
187 /// .highlight_symbol(">>");
188 /// ```
189 #[derive(Debug, Clone, PartialEq)]
190 pub struct Table<'a> {
191     /// A block to wrap the widget in
192     block: Option<Block<'a>>,
193     /// Base style for the widget
194     style: Style,
195     /// Width constraints for each column
196     widths: &'a [Constraint],
197     /// Space between each column
198     column_spacing: u16,
199     /// Style used to render the selected row
200     highlight_style: Style,
201     /// Symbol in front of the selected rom
202     highlight_symbol: Option<&'a str>,
203     /// Optional header
204     header: Option<Row<'a>>,
205     /// Data to display in each row
206     rows: Vec<Row<'a>>,
207 }
208 
209 impl<'a> Table<'a> {
210     pub fn new<T>(rows: T) -> Self
211     where
212         T: IntoIterator<Item = Row<'a>>,
213     {
214         Self {
215             block: None,
216             style: Style::default(),
217             widths: &[],
218             column_spacing: 1,
219             highlight_style: Style::default(),
220             highlight_symbol: None,
221             header: None,
222             rows: rows.into_iter().collect(),
223         }
224     }
225 
226     pub fn block(mut self, block: Block<'a>) -> Self {
227         self.block = Some(block);
228         self
229     }
230 
231     pub fn header(mut self, header: Row<'a>) -> Self {
232         self.header = Some(header);
233         self
234     }
235 
236     pub fn widths(mut self, widths: &'a [Constraint]) -> Self {
237         let between_0_and_100 = |&w| match w {
238             Constraint::Percentage(p) => p <= 100,
239             _ => true,
240         };
241         assert!(
242             widths.iter().all(between_0_and_100),
243             "Percentages should be between 0 and 100 inclusively."
244         );
245         self.widths = widths;
246         self
247     }
248 
249     pub fn style(mut self, style: Style) -> Self {
250         self.style = style;
251         self
252     }
253 
254     pub fn highlight_symbol(mut self, highlight_symbol: &'a str) -> Self {
255         self.highlight_symbol = Some(highlight_symbol);
256         self
257     }
258 
259     pub fn highlight_style(mut self, highlight_style: Style) -> Self {
260         self.highlight_style = highlight_style;
261         self
262     }
263 
264     pub fn column_spacing(mut self, spacing: u16) -> Self {
265         self.column_spacing = spacing;
266         self
267     }
268 
269     fn get_columns_widths(&self, max_width: u16, has_selection: bool) -> Vec<u16> {
270         let mut constraints = Vec::with_capacity(self.widths.len() * 2 + 1);
271         if has_selection {
272             let highlight_symbol_width =
273                 self.highlight_symbol.map(|s| s.width() as u16).unwrap_or(0);
274             constraints.push(Constraint::Length(highlight_symbol_width));
275         }
276         for constraint in self.widths {
277             constraints.push(*constraint);
278             constraints.push(Constraint::Length(self.column_spacing));
279         }
280         if !self.widths.is_empty() {
281             constraints.pop();
282         }
283         let mut chunks = Layout::default()
284             .direction(Direction::Horizontal)
285             .constraints(constraints)
286             .expand_to_fill(false)
287             .split(Rect {
288                 x: 0,
289                 y: 0,
290                 width: max_width,
291                 height: 1,
292             });
293         if has_selection {
294             chunks.remove(0);
295         }
296         chunks.iter().step_by(2).map(|c| c.width).collect()
297     }
298 
299     fn get_row_bounds(
300         &self,
301         selected: Option<usize>,
302         offset: usize,
303         max_height: u16,
304     ) -> (usize, usize) {
305         let offset = offset.min(self.rows.len().saturating_sub(1));
306         let mut start = offset;
307         let mut end = offset;
308         let mut height = 0;
309         for item in self.rows.iter().skip(offset) {
310             if height + item.height > max_height {
311                 break;
312             }
313             height += item.total_height();
314             end += 1;
315         }
316 
317         let selected = selected.unwrap_or(0).min(self.rows.len() - 1);
318         while selected >= end {
319             height = height.saturating_add(self.rows[end].total_height());
320             end += 1;
321             while height > max_height {
322                 height = height.saturating_sub(self.rows[start].total_height());
323                 start += 1;
324             }
325         }
326         while selected < start {
327             start -= 1;
328             height = height.saturating_add(self.rows[start].total_height());
329             while height > max_height {
330                 end -= 1;
331                 height = height.saturating_sub(self.rows[end].total_height());
332             }
333         }
334         (start, end)
335     }
336 }
337 
338 #[derive(Debug, Clone)]
339 pub struct TableState {
340     offset: usize,
341     selected: Option<usize>,
342 }
343 
344 impl Default for TableState {
345     fn default() -> TableState {
346         TableState {
347             offset: 0,
348             selected: None,
349         }
350     }
351 }
352 
353 impl TableState {
354     pub fn selected(&self) -> Option<usize> {
355         self.selected
356     }
357 
358     pub fn select(&mut self, index: Option<usize>) {
359         self.selected = index;
360         if index.is_none() {
361             self.offset = 0;
362         }
363     }
364 }
365 
366 impl<'a> StatefulWidget for Table<'a> {
367     type State = TableState;
368 
369     fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
370         if area.area() == 0 {
371             return;
372         }
373         buf.set_style(area, self.style);
374         let table_area = match self.block.take() {
375             Some(b) => {
376                 let inner_area = b.inner(area);
377                 b.render(area, buf);
378                 inner_area
379             }
380             None => area,
381         };
382 
383         let has_selection = state.selected.is_some();
384         let columns_widths = self.get_columns_widths(table_area.width, has_selection);
385         let highlight_symbol = self.highlight_symbol.unwrap_or("");
386         let blank_symbol = " ".repeat(highlight_symbol.width());
387         let mut current_height = 0;
388         let mut rows_height = table_area.height;
389 
390         // Draw header
391         if let Some(ref header) = self.header {
392             let max_header_height = table_area.height.min(header.total_height());
393             buf.set_style(
394                 Rect {
395                     x: table_area.left(),
396                     y: table_area.top(),
397                     width: table_area.width,
398                     height: table_area.height.min(header.height),
399                 },
400                 header.style,
401             );
402             let mut col = table_area.left();
403             if has_selection {
404                 col += (highlight_symbol.width() as u16).min(table_area.width);
405             }
406             for (width, cell) in columns_widths.iter().zip(header.cells.iter()) {
407                 render_cell(
408                     buf,
409                     cell,
410                     Rect {
411                         x: col,
412                         y: table_area.top(),
413                         width: *width,
414                         height: max_header_height,
415                     },
416                 );
417                 col += *width + self.column_spacing;
418             }
419             current_height += max_header_height;
420             rows_height = rows_height.saturating_sub(max_header_height);
421         }
422 
423         // Draw rows
424         if self.rows.is_empty() {
425             return;
426         }
427         let (start, end) = self.get_row_bounds(state.selected, state.offset, rows_height);
428         state.offset = start;
429         for (i, table_row) in self
430             .rows
431             .iter_mut()
432             .enumerate()
433             .skip(state.offset)
434             .take(end - start)
435         {
436             let (row, col) = (table_area.top() + current_height, table_area.left());
437             current_height += table_row.total_height();
438             let table_row_area = Rect {
439                 x: col,
440                 y: row,
441                 width: table_area.width,
442                 height: table_row.height,
443             };
444             buf.set_style(table_row_area, table_row.style);
445             let is_selected = state.selected.map(|s| s == i).unwrap_or(false);
446             let table_row_start_col = if has_selection {
447                 let symbol = if is_selected {
448                     highlight_symbol
449                 } else {
450                     &blank_symbol
451                 };
452                 let (col, _) =
453                     buf.set_stringn(col, row, symbol, table_area.width as usize, table_row.style);
454                 col
455             } else {
456                 col
457             };
458             let mut col = table_row_start_col;
459             for (width, cell) in columns_widths.iter().zip(table_row.cells.iter()) {
460                 render_cell(
461                     buf,
462                     cell,
463                     Rect {
464                         x: col,
465                         y: row,
466                         width: *width,
467                         height: table_row.height,
468                     },
469                 );
470                 col += *width + self.column_spacing;
471             }
472             if is_selected {
473                 buf.set_style(table_row_area, self.highlight_style);
474             }
475         }
476     }
477 }
478 
479 fn render_cell(buf: &mut Buffer, cell: &Cell, area: Rect) {
480     buf.set_style(area, cell.style);
481     for (i, spans) in cell.content.lines.iter().enumerate() {
482         if i as u16 >= area.height {
483             break;
484         }
485         buf.set_spans(area.x, area.y + i as u16, spans, area.width);
486     }
487 }
488 
489 impl<'a> Widget for Table<'a> {
490     fn render(self, area: Rect, buf: &mut Buffer) {
491         let mut state = TableState::default();
492         StatefulWidget::render(self, area, buf, &mut state);
493     }
494 }
495 
496 #[cfg(test)]
497 mod tests {
498     use super::*;
499 
500     #[test]
501     #[should_panic]
502     fn table_invalid_percentages() {
503         Table::new(vec![]).widths(&[Constraint::Percentage(110)]);
504     }
505 }
506