tui_temp_fork/widgets/
table.rs

1use std::collections::HashMap;
2use std::fmt::Display;
3use std::iter::Iterator;
4
5use cassowary::strength::{MEDIUM, REQUIRED, WEAK};
6use cassowary::WeightedRelation::*;
7use cassowary::{Expression, Solver};
8
9use crate::buffer::Buffer;
10use crate::layout::{Constraint, Rect};
11use crate::style::Style;
12use crate::widgets::{Block, Widget};
13
14/// Holds data to be displayed in a Table widget
15pub enum Row<D, I>
16where
17    D: Iterator<Item = I>,
18    I: Display,
19{
20    Data(D),
21    StyledData(D, Style),
22}
23
24/// A widget to display data in formatted columns
25///
26/// # Examples
27///
28/// ```
29/// # use tui_temp_fork::widgets::{Block, Borders, Table, Row};
30/// # use tui_temp_fork::layout::Constraint;
31/// # use tui_temp_fork::style::{Style, Color};
32/// # fn main() {
33/// let row_style = Style::default().fg(Color::White);
34/// Table::new(
35///         ["Col1", "Col2", "Col3"].into_iter(),
36///         vec![
37///             Row::StyledData(["Row11", "Row12", "Row13"].into_iter(), row_style),
38///             Row::StyledData(["Row21", "Row22", "Row23"].into_iter(), row_style),
39///             Row::StyledData(["Row31", "Row32", "Row33"].into_iter(), row_style),
40///             Row::Data(["Row41", "Row42", "Row43"].into_iter())
41///         ].into_iter()
42///     )
43///     .block(Block::default().title("Table"))
44///     .header_style(Style::default().fg(Color::Yellow))
45///     .widths(&[Constraint::Length(5), Constraint::Length(5), Constraint::Length(10)])
46///     .style(Style::default().fg(Color::White))
47///     .column_spacing(1);
48/// # }
49/// ```
50pub struct Table<'a, T, H, I, D, R>
51where
52    T: Display,
53    H: Iterator<Item = T>,
54    I: Display,
55    D: Iterator<Item = I>,
56    R: Iterator<Item = Row<D, I>>,
57{
58    /// A block to wrap the widget in
59    block: Option<Block<'a>>,
60    /// Base style for the widget
61    style: Style,
62    /// Header row for all columns
63    header: H,
64    /// Style for the header
65    header_style: Style,
66    /// Width constraints for each column
67    widths: &'a [Constraint],
68    /// Space between each column
69    column_spacing: u16,
70    /// Data to display in each row
71    rows: R,
72}
73
74impl<'a, T, H, I, D, R> Default for Table<'a, T, H, I, D, R>
75where
76    T: Display,
77    H: Iterator<Item = T> + Default,
78    I: Display,
79    D: Iterator<Item = I>,
80    R: Iterator<Item = Row<D, I>> + Default,
81{
82    fn default() -> Table<'a, T, H, I, D, R> {
83        Table {
84            block: None,
85            style: Style::default(),
86            header: H::default(),
87            header_style: Style::default(),
88            widths: &[],
89            rows: R::default(),
90            column_spacing: 1,
91        }
92    }
93}
94
95impl<'a, T, H, I, D, R> Table<'a, T, H, I, D, R>
96where
97    T: Display,
98    H: Iterator<Item = T>,
99    I: Display,
100    D: Iterator<Item = I>,
101    R: Iterator<Item = Row<D, I>>,
102{
103    pub fn new(header: H, rows: R) -> Table<'a, T, H, I, D, R> {
104        Table {
105            block: None,
106            style: Style::default(),
107            header,
108            header_style: Style::default(),
109            widths: &[],
110            rows,
111            column_spacing: 1,
112        }
113    }
114    pub fn block(mut self, block: Block<'a>) -> Table<'a, T, H, I, D, R> {
115        self.block = Some(block);
116        self
117    }
118
119    pub fn header<II>(mut self, header: II) -> Table<'a, T, H, I, D, R>
120    where
121        II: IntoIterator<Item = T, IntoIter = H>,
122    {
123        self.header = header.into_iter();
124        self
125    }
126
127    pub fn header_style(mut self, style: Style) -> Table<'a, T, H, I, D, R> {
128        self.header_style = style;
129        self
130    }
131
132    pub fn widths(mut self, widths: &'a [Constraint]) -> Table<'a, T, H, I, D, R> {
133        let between_0_and_100 = |&w| match w {
134            Constraint::Percentage(p) => p <= 100,
135            _ => true,
136        };
137        assert!(
138            widths.iter().all(between_0_and_100),
139            "Percentages should be between 0 and 100 inclusively."
140        );
141        self.widths = widths;
142        self
143    }
144
145    pub fn rows<II>(mut self, rows: II) -> Table<'a, T, H, I, D, R>
146    where
147        II: IntoIterator<Item = Row<D, I>, IntoIter = R>,
148    {
149        self.rows = rows.into_iter();
150        self
151    }
152
153    pub fn style(mut self, style: Style) -> Table<'a, T, H, I, D, R> {
154        self.style = style;
155        self
156    }
157
158    pub fn column_spacing(mut self, spacing: u16) -> Table<'a, T, H, I, D, R> {
159        self.column_spacing = spacing;
160        self
161    }
162}
163
164impl<'a, T, H, I, D, R> Widget for Table<'a, T, H, I, D, R>
165where
166    T: Display,
167    H: Iterator<Item = T>,
168    I: Display,
169    D: Iterator<Item = I>,
170    R: Iterator<Item = Row<D, I>>,
171{
172    fn draw(&mut self, area: Rect, buf: &mut Buffer) {
173        // Render block if necessary and get the drawing area
174        let table_area = match self.block {
175            Some(ref mut b) => {
176                b.draw(area, buf);
177                b.inner(area)
178            }
179            None => area,
180        };
181
182        // Set the background
183        self.background(table_area, buf, self.style.bg);
184
185        let mut solver = Solver::new();
186        let mut var_indices = HashMap::new();
187        let mut ccs = Vec::new();
188        let mut variables = Vec::new();
189        for i in 0..self.widths.len() {
190            let var = cassowary::Variable::new();
191            variables.push(var);
192            var_indices.insert(var, i);
193        }
194        for (i, constraint) in self.widths.iter().enumerate() {
195            ccs.push(variables[i] | GE(WEAK) | 0.);
196            ccs.push(match *constraint {
197                Constraint::Length(v) => variables[i] | EQ(MEDIUM) | f64::from(v),
198                Constraint::Percentage(v) => {
199                    variables[i] | EQ(WEAK) | (f64::from(v * area.width) / 100.0)
200                }
201                Constraint::Ratio(n, d) => {
202                    variables[i] | EQ(WEAK) | (f64::from(area.width) * f64::from(n) / f64::from(d))
203                }
204                Constraint::Min(v) => variables[i] | GE(WEAK) | f64::from(v),
205                Constraint::Max(v) => variables[i] | LE(WEAK) | f64::from(v),
206            })
207        }
208        solver
209            .add_constraint(
210                variables
211                    .iter()
212                    .fold(Expression::from_constant(0.), |acc, v| acc + *v)
213                    | LE(REQUIRED)
214                    | f64::from(
215                        area.width - 2 - (self.column_spacing * (variables.len() as u16 - 1)),
216                    ),
217            )
218            .unwrap();
219        solver.add_constraints(&ccs).unwrap();
220        let mut solved_widths = vec![0; variables.len()];
221        for &(var, value) in solver.fetch_changes() {
222            let index = var_indices[&var];
223            let value = if value.is_sign_negative() {
224                0
225            } else {
226                value as u16
227            };
228            solved_widths[index] = value
229        }
230
231        let mut y = table_area.top();
232        let mut x = table_area.left();
233
234        // Draw header
235        if y < table_area.bottom() {
236            for (w, t) in solved_widths.iter().zip(self.header.by_ref()) {
237                buf.set_stringn(x, y, format!("{}", t), *w as usize, self.header_style);
238                x += *w + self.column_spacing;
239            }
240        }
241        y += 2;
242
243        // Draw rows
244        let default_style = Style::default();
245        if y < table_area.bottom() {
246            let remaining = (table_area.bottom() - y) as usize;
247            for (i, row) in self.rows.by_ref().take(remaining).enumerate() {
248                let (data, style) = match row {
249                    Row::Data(d) => (d, default_style),
250                    Row::StyledData(d, s) => (d, s),
251                };
252                x = table_area.left();
253                for (w, elt) in solved_widths.iter().zip(data) {
254                    buf.set_stringn(x, y + i as u16, format!("{}", elt), *w as usize, style);
255                    x += *w + self.column_spacing;
256                }
257            }
258        }
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    #[test]
267    #[should_panic]
268    fn table_invalid_percentages() {
269        Table::new([""].iter(), vec![Row::Data([""].iter())].into_iter())
270            .widths(&[Constraint::Percentage(110)]);
271    }
272}