use std::collections::HashMap;
use std::fmt::Display;
use std::iter::Iterator;
use cassowary::strength::{MEDIUM, REQUIRED, WEAK};
use cassowary::WeightedRelation::*;
use cassowary::{Expression, Solver};
use crate::buffer::Buffer;
use crate::layout::{Constraint, Rect};
use crate::style::Style;
use crate::widgets::{Block, Widget};
pub enum Row<D, I>
where
D: Iterator<Item = I>,
I: Display,
{
Data(D),
StyledData(D, Style),
}
pub struct Table<'a, T, H, I, D, R>
where
T: Display,
H: Iterator<Item = T>,
I: Display,
D: Iterator<Item = I>,
R: Iterator<Item = Row<D, I>>,
{
block: Option<Block<'a>>,
style: Style,
header: H,
header_style: Style,
widths: &'a [Constraint],
column_spacing: u16,
rows: R,
}
impl<'a, T, H, I, D, R> Default for Table<'a, T, H, I, D, R>
where
T: Display,
H: Iterator<Item = T> + Default,
I: Display,
D: Iterator<Item = I>,
R: Iterator<Item = Row<D, I>> + Default,
{
fn default() -> Table<'a, T, H, I, D, R> {
Table {
block: None,
style: Style::default(),
header: H::default(),
header_style: Style::default(),
widths: &[],
rows: R::default(),
column_spacing: 1,
}
}
}
impl<'a, T, H, I, D, R> Table<'a, T, H, I, D, R>
where
T: Display,
H: Iterator<Item = T>,
I: Display,
D: Iterator<Item = I>,
R: Iterator<Item = Row<D, I>>,
{
pub fn new(header: H, rows: R) -> Table<'a, T, H, I, D, R> {
Table {
block: None,
style: Style::default(),
header,
header_style: Style::default(),
widths: &[],
rows,
column_spacing: 1,
}
}
pub fn block(mut self, block: Block<'a>) -> Table<'a, T, H, I, D, R> {
self.block = Some(block);
self
}
pub fn header<II>(mut self, header: II) -> Table<'a, T, H, I, D, R>
where
II: IntoIterator<Item = T, IntoIter = H>,
{
self.header = header.into_iter();
self
}
pub fn header_style(mut self, style: Style) -> Table<'a, T, H, I, D, R> {
self.header_style = style;
self
}
pub fn widths(mut self, widths: &'a [Constraint]) -> Table<'a, T, H, I, D, R> {
let between_0_and_100 = |&w| match w {
Constraint::Percentage(p) => p <= 100,
_ => true,
};
assert!(
widths.iter().all(between_0_and_100),
"Percentages should be between 0 and 100 inclusively."
);
self.widths = widths;
self
}
pub fn rows<II>(mut self, rows: II) -> Table<'a, T, H, I, D, R>
where
II: IntoIterator<Item = Row<D, I>, IntoIter = R>,
{
self.rows = rows.into_iter();
self
}
pub fn style(mut self, style: Style) -> Table<'a, T, H, I, D, R> {
self.style = style;
self
}
pub fn column_spacing(mut self, spacing: u16) -> Table<'a, T, H, I, D, R> {
self.column_spacing = spacing;
self
}
}
impl<'a, T, H, I, D, R> Widget for Table<'a, T, H, I, D, R>
where
T: Display,
H: Iterator<Item = T>,
I: Display,
D: Iterator<Item = I>,
R: Iterator<Item = Row<D, I>>,
{
fn draw(&mut self, area: Rect, buf: &mut Buffer) {
let table_area = match self.block {
Some(ref mut b) => {
b.draw(area, buf);
b.inner(area)
}
None => area,
};
self.background(table_area, buf, self.style.bg);
let mut solver = Solver::new();
let mut var_indices = HashMap::new();
let mut ccs = Vec::new();
let mut variables = Vec::new();
for i in 0..self.widths.len() {
let var = cassowary::Variable::new();
variables.push(var);
var_indices.insert(var, i);
}
for (i, constraint) in self.widths.iter().enumerate() {
ccs.push(variables[i] | GE(WEAK) | 0.);
ccs.push(match *constraint {
Constraint::Length(v) => variables[i] | EQ(MEDIUM) | f64::from(v),
Constraint::Percentage(v) => {
variables[i] | EQ(WEAK) | (f64::from(v * area.width) / 100.0)
}
Constraint::Ratio(n, d) => {
variables[i] | EQ(WEAK) | (f64::from(area.width) * f64::from(n) / f64::from(d))
}
Constraint::Min(v) => variables[i] | GE(WEAK) | f64::from(v),
Constraint::Max(v) => variables[i] | LE(WEAK) | f64::from(v),
})
}
solver
.add_constraint(
variables
.iter()
.fold(Expression::from_constant(0.), |acc, v| acc + *v)
| LE(REQUIRED)
| f64::from(
area.width - 2 - (self.column_spacing * (variables.len() as u16 - 1)),
),
)
.unwrap();
solver.add_constraints(&ccs).unwrap();
let mut solved_widths = vec![0; variables.len()];
for &(var, value) in solver.fetch_changes() {
let index = var_indices[&var];
let value = if value.is_sign_negative() {
0
} else {
value as u16
};
solved_widths[index] = value
}
let mut y = table_area.top();
let mut x = table_area.left();
if y < table_area.bottom() {
for (w, t) in solved_widths.iter().zip(self.header.by_ref()) {
buf.set_stringn(x, y, format!("{}", t), *w as usize, self.header_style);
x += *w + self.column_spacing;
}
}
y += 2;
let default_style = Style::default();
if y < table_area.bottom() {
let remaining = (table_area.bottom() - y) as usize;
for (i, row) in self.rows.by_ref().take(remaining).enumerate() {
let (data, style) = match row {
Row::Data(d) => (d, default_style),
Row::StyledData(d, s) => (d, s),
};
x = table_area.left();
for (w, elt) in solved_widths.iter().zip(data) {
buf.set_stringn(x, y + i as u16, format!("{}", elt), *w as usize, style);
x += *w + self.column_spacing;
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn table_invalid_percentages() {
Table::new([""].iter(), vec![Row::Data([""].iter())].into_iter())
.widths(&[Constraint::Percentage(110)]);
}
}