1use crate::{
2 buffer::Buffer,
3 layout::{Constraint, Rect},
4 style::Style,
5 widgets::{Block, StatefulWidget, Widget},
6};
7use cassowary::{
8 strength::{MEDIUM, REQUIRED, WEAK},
9 WeightedRelation::*,
10 {Expression, Solver},
11};
12use std::{
13 collections::HashMap,
14 fmt::Display,
15 iter::{self, Iterator},
16};
17use unicode_width::UnicodeWidthStr;
18
19pub struct TableState {
20 offset: usize,
21 selected: Option<usize>,
22}
23
24impl Default for TableState {
25 fn default() -> TableState {
26 TableState {
27 offset: 0,
28 selected: None,
29 }
30 }
31}
32
33impl TableState {
34 pub fn selected(&self) -> Option<usize> {
35 self.selected
36 }
37
38 pub fn select(&mut self, index: Option<usize>) {
39 self.selected = index;
40 if index.is_none() {
41 self.offset = 0;
42 }
43 }
44}
45
46pub enum Row<D>
48where
49 D: Iterator,
50 D::Item: Display,
51{
52 Data(D),
53 StyledData(D, Style),
54}
55
56pub struct Table<'a, H, R> {
81 block: Option<Block<'a>>,
83 style: Style,
85 header: H,
87 header_style: Style,
89 widths: &'a [Constraint],
91 column_spacing: u16,
93 header_gap: u16,
95 highlight_style: Style,
97 highlight_symbol: Option<&'a str>,
99 rows: R,
101}
102
103impl<'a, H, R> Default for Table<'a, H, R>
104where
105 H: Iterator + Default,
106 R: Iterator + Default,
107{
108 fn default() -> Table<'a, H, R> {
109 Table {
110 block: None,
111 style: Style::default(),
112 header: H::default(),
113 header_style: Style::default(),
114 widths: &[],
115 column_spacing: 1,
116 header_gap: 1,
117 highlight_style: Style::default(),
118 highlight_symbol: None,
119 rows: R::default(),
120 }
121 }
122}
123impl<'a, H, D, R> Table<'a, H, R>
124where
125 H: Iterator,
126 D: Iterator,
127 D::Item: Display,
128 R: Iterator<Item = Row<D>>,
129{
130 pub fn new(header: H, rows: R) -> Table<'a, H, R> {
131 Table {
132 block: None,
133 style: Style::default(),
134 header,
135 header_style: Style::default(),
136 widths: &[],
137 column_spacing: 1,
138 header_gap: 1,
139 highlight_style: Style::default(),
140 highlight_symbol: None,
141 rows,
142 }
143 }
144 pub fn block(mut self, block: Block<'a>) -> Table<'a, H, R> {
145 self.block = Some(block);
146 self
147 }
148
149 pub fn header<II>(mut self, header: II) -> Table<'a, H, R>
150 where
151 II: IntoIterator<Item = H::Item, IntoIter = H>,
152 {
153 self.header = header.into_iter();
154 self
155 }
156
157 pub fn header_style(mut self, style: Style) -> Table<'a, H, R> {
158 self.header_style = style;
159 self
160 }
161
162 pub fn widths(mut self, widths: &'a [Constraint]) -> Table<'a, H, R> {
163 let between_0_and_100 = |&w| match w {
164 Constraint::Percentage(p) => p <= 100,
165 _ => true,
166 };
167 assert!(
168 widths.iter().all(between_0_and_100),
169 "Percentages should be between 0 and 100 inclusively."
170 );
171 self.widths = widths;
172 self
173 }
174
175 pub fn rows<II>(mut self, rows: II) -> Table<'a, H, R>
176 where
177 II: IntoIterator<Item = Row<D>, IntoIter = R>,
178 {
179 self.rows = rows.into_iter();
180 self
181 }
182
183 pub fn style(mut self, style: Style) -> Table<'a, H, R> {
184 self.style = style;
185 self
186 }
187
188 pub fn highlight_symbol(mut self, highlight_symbol: &'a str) -> Table<'a, H, R> {
189 self.highlight_symbol = Some(highlight_symbol);
190 self
191 }
192
193 pub fn highlight_style(mut self, highlight_style: Style) -> Table<'a, H, R> {
194 self.highlight_style = highlight_style;
195 self
196 }
197
198 pub fn column_spacing(mut self, spacing: u16) -> Table<'a, H, R> {
199 self.column_spacing = spacing;
200 self
201 }
202
203 pub fn header_gap(mut self, gap: u16) -> Table<'a, H, R> {
204 self.header_gap = gap;
205 self
206 }
207}
208
209impl<'a, H, D, R> StatefulWidget for Table<'a, H, R>
210where
211 H: Iterator,
212 H::Item: Display,
213 D: Iterator,
214 D::Item: Display,
215 R: Iterator<Item = Row<D>>,
216{
217 type State = TableState;
218
219 fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
220 let table_area = match self.block {
222 Some(ref mut b) => {
223 b.render(area, buf);
224 b.inner(area)
225 }
226 None => area,
227 };
228
229 buf.set_background(table_area, self.style.bg);
230
231 let mut solver = Solver::new();
232 let mut var_indices = HashMap::new();
233 let mut ccs = Vec::new();
234 let mut variables = Vec::new();
235 for i in 0..self.widths.len() {
236 let var = cassowary::Variable::new();
237 variables.push(var);
238 var_indices.insert(var, i);
239 }
240 for (i, constraint) in self.widths.iter().enumerate() {
241 ccs.push(variables[i] | GE(WEAK) | 0.);
242 ccs.push(match *constraint {
243 Constraint::Length(v) => variables[i] | EQ(MEDIUM) | f64::from(v),
244 Constraint::Percentage(v) => {
245 variables[i] | EQ(WEAK) | (f64::from(v * area.width) / 100.0)
246 }
247 Constraint::Ratio(n, d) => {
248 variables[i] | EQ(WEAK) | (f64::from(area.width) * f64::from(n) / f64::from(d))
249 }
250 Constraint::Min(v) => variables[i] | GE(WEAK) | f64::from(v),
251 Constraint::Max(v) => variables[i] | LE(WEAK) | f64::from(v),
252 })
253 }
254 solver
255 .add_constraint(
256 variables
257 .iter()
258 .fold(Expression::from_constant(0.), |acc, v| acc + *v)
259 | LE(REQUIRED)
260 | f64::from(
261 area.width - 2 - (self.column_spacing * (variables.len() as u16 - 1)),
262 ),
263 )
264 .unwrap();
265 solver.add_constraints(&ccs).unwrap();
266 let mut solved_widths = vec![0; variables.len()];
267 for &(var, value) in solver.fetch_changes() {
268 let index = var_indices[&var];
269 let value = if value.is_sign_negative() {
270 0
271 } else {
272 value as u16
273 };
274 solved_widths[index] = value
275 }
276
277 let mut y = table_area.top();
278 let mut x = table_area.left();
279
280 if y < table_area.bottom() {
282 for (w, t) in solved_widths.iter().zip(self.header.by_ref()) {
283 buf.set_stringn(x, y, format!("{}", t), *w as usize, self.header_style);
284 x += *w + self.column_spacing;
285 }
286 }
287 y += 1 + self.header_gap;
288
289 let (selected, highlight_style) = match state.selected {
291 Some(i) => (Some(i), self.highlight_style),
292 None => (None, self.style),
293 };
294 let highlight_symbol = self.highlight_symbol.unwrap_or("");
295 let blank_symbol = iter::repeat(" ")
296 .take(highlight_symbol.width())
297 .collect::<String>();
298
299 let default_style = Style::default();
301 if y < table_area.bottom() {
302 let remaining = (table_area.bottom() - y) as usize;
303
304 state.offset = if let Some(selected) = selected {
306 if selected >= remaining + state.offset - 1 {
307 selected + 1 - remaining
308 } else if selected < state.offset {
309 selected
310 } else {
311 state.offset
312 }
313 } else {
314 0
315 };
316 for (i, row) in self.rows.skip(state.offset).take(remaining).enumerate() {
317 let (data, style, symbol) = match row {
318 Row::Data(d) | Row::StyledData(d, _)
319 if Some(i) == state.selected.map(|s| s - state.offset) =>
320 {
321 (d, highlight_style, highlight_symbol)
322 }
323 Row::Data(d) => (d, default_style, blank_symbol.as_ref()),
324 Row::StyledData(d, s) => (d, s, blank_symbol.as_ref()),
325 };
326 x = table_area.left();
327 for (c, (w, elt)) in solved_widths.iter().zip(data).enumerate() {
328 let s = if c == 0 {
329 format!("{}{}", symbol, elt)
330 } else {
331 format!("{}", elt)
332 };
333 buf.set_stringn(x, y + i as u16, s, *w as usize, style);
334 x += *w + self.column_spacing;
335 }
336 }
337 }
338 }
339}
340
341impl<'a, H, D, R> Widget for Table<'a, H, R>
342where
343 H: Iterator,
344 H::Item: Display,
345 D: Iterator,
346 D::Item: Display,
347 R: Iterator<Item = Row<D>>,
348{
349 fn render(self, area: Rect, buf: &mut Buffer) {
350 let mut state = TableState::default();
351 StatefulWidget::render(self, area, buf, &mut state);
352 }
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358
359 #[test]
360 #[should_panic]
361 fn table_invalid_percentages() {
362 Table::new([""].iter(), vec![Row::Data([""].iter())].into_iter())
363 .widths(&[Constraint::Percentage(110)]);
364 }
365}