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
14pub enum Row<D, I>
16where
17 D: Iterator<Item = I>,
18 I: Display,
19{
20 Data(D),
21 StyledData(D, Style),
22}
23
24pub 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 block: Option<Block<'a>>,
60 style: Style,
62 header: H,
64 header_style: Style,
66 widths: &'a [Constraint],
68 column_spacing: u16,
70 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 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 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 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 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}