use ctl_text::{Context, Line, Measurement, Overflow, Renderable, Text};
use crate::column::Column;
use crate::frame::{Frame, RowSeparation};
use crate::glyphs::Glyphs;
use crate::lines::{literal, push_cell, rule_line};
use crate::widths::{Extent, Framing, solve};
const GAP: u16 = 2;
#[derive(Clone, Debug)]
pub struct Table {
columns: Vec<Column>,
header: Option<Vec<Text>>,
rows: Vec<Vec<Text>>,
frame: Frame,
separation: RowSeparation,
}
impl Table {
#[must_use]
pub fn new(columns: impl IntoIterator<Item = Column>) -> Self {
Self::from_columns(columns.into_iter().collect())
}
fn from_columns(mut columns: Vec<Column>) -> Self {
let titled = columns.iter().any(|column| column.header.is_some());
let header: Option<Vec<Text>> = titled.then(|| {
columns
.iter_mut()
.map(|column| {
column
.header
.take()
.unwrap_or_default()
.overflow(column.overflow)
})
.collect()
});
Self {
columns,
header,
rows: Vec::new(),
frame: Frame::default(),
separation: RowSeparation::default(),
}
}
#[must_use]
pub fn frame(mut self, frame: Frame) -> Self {
self.frame = frame;
self
}
#[must_use]
pub fn row_separation(mut self, separation: RowSeparation) -> Self {
self.separation = separation;
self
}
#[must_use]
pub fn row<C: Into<Text>>(self, cells: impl IntoIterator<Item = C>) -> Self {
self.push_row(cells.into_iter().map(Into::into).collect())
}
#[must_use]
pub fn rows<C, R>(self, rows: impl IntoIterator<Item = R>) -> Self
where
R: IntoIterator<Item = C>,
C: Into<Text>,
{
rows.into_iter().fold(self, Table::row)
}
fn push_row(mut self, mut cells: Vec<Text>) -> Self {
cells.resize_with(self.columns.len(), Text::new);
let cells = cells
.into_iter()
.zip(&self.columns)
.map(|(cell, column)| cell.overflow(column.overflow))
.collect();
self.rows.push(cells);
self
}
pub(crate) fn into_rows(self) -> Vec<Vec<Text>> {
self.rows
}
fn is_blank(&self) -> bool {
self.columns.is_empty() || (self.header.is_none() && self.rows.is_empty())
}
fn frame_width(&self) -> u16 {
let count = u16::try_from(self.columns.len()).unwrap_or(u16::MAX);
match self.frame.framing() {
Framing::Framed => count.saturating_mul(3).saturating_add(1),
Framing::Unframed => count.saturating_sub(1).saturating_mul(GAP),
}
}
fn grid(&self) -> impl Iterator<Item = &[Text]> {
self.header.iter().chain(&self.rows).map(Vec::as_slice)
}
fn cell_overflow(&self, column: &Column) -> Overflow {
match self.frame.framing() {
Framing::Framed if column.overflow == Overflow::KeepWords => Overflow::Fold,
Framing::Framed | Framing::Unframed => column.overflow,
}
}
fn extents(&self, context: &Context) -> Vec<Extent> {
let framing = self.frame.framing();
self.columns
.iter()
.enumerate()
.map(|(index, column)| {
let mut extent = Extent {
natural: 0,
minimum: 0,
floor: 0,
};
for cell in self.grid().filter_map(|row| row.get(index)) {
let measured = cell.measure(context);
let floor = match framing {
Framing::Framed if column.overflow != Overflow::Fold => cell
.measure_as(context, Overflow::Fold)
.minimum()
.min(measured.minimum()),
Framing::Framed | Framing::Unframed => measured.minimum(),
};
extent.natural = extent.natural.max(measured.maximum());
extent.minimum = extent.minimum.max(measured.minimum());
extent.floor = extent.floor.max(floor);
}
extent
})
.collect()
}
fn render_cells(&self, widths: &[u16], context: &Context) -> Vec<Vec<Vec<Line>>> {
self.grid()
.map(|row| {
row.iter()
.zip(widths)
.zip(&self.columns)
.map(|((cell, width), column)| {
let context = context.at_width(*width);
cell.render_as(&context, self.cell_overflow(column))
})
.collect()
})
.collect()
}
fn row_lines(&self, cells: &[Vec<Line>], widths: &[u16], glyphs: &Glyphs) -> Vec<Line> {
let height = cells.iter().map(Vec::len).max().unwrap_or(0).max(1);
let empty = Line::new();
let framing = self.frame.framing();
let (edge, gap, padding) = match framing {
Framing::Framed => (
glyphs.outer.to_string(),
glyphs.inner.to_string(),
" ".to_owned(),
),
Framing::Unframed => (String::new(), " ".repeat(usize::from(GAP)), String::new()),
};
(0..height)
.map(|index| {
let mut line = Line::from(literal(&edge));
for (column, (width, spec)) in widths.iter().zip(&self.columns).enumerate() {
if column > 0 {
line.push(literal(&gap));
}
line.push(literal(&padding));
let cell = cells
.get(column)
.and_then(|lines| lines.get(index))
.unwrap_or(&empty);
push_cell(&mut line, cell, *width, spec.align);
line.push(literal(&padding));
}
match framing {
Framing::Framed => line.push(literal(&edge)),
Framing::Unframed => line.trim_end(),
}
line
})
.collect()
}
fn compose(&self, cells: &[Vec<Vec<Line>>], widths: &[u16], glyphs: &Glyphs) -> Vec<Line> {
let header = self.header.is_some();
let framing = self.frame.framing();
let total = widths
.iter()
.fold(self.frame_width(), |sum, width| sum.saturating_add(*width));
let blank_cells = vec![Vec::new(); widths.len()];
let mut lines = Vec::new();
match framing {
Framing::Framed => lines.push(glyphs.top.line(widths)),
Framing::Unframed => {}
}
for (index, row) in cells.iter().enumerate() {
if header && index == 1 {
match self.frame {
Frame::Grid => lines.push(glyphs.header.line(widths)),
Frame::HeaderRule => lines.push(rule_line(glyphs.rule, total)),
Frame::Plain => {}
}
} else if index > 0 {
match (self.separation, framing) {
(RowSeparation::Rule, Framing::Framed) => lines.push(glyphs.row.line(widths)),
(RowSeparation::Rule, Framing::Unframed) => {
lines.push(rule_line(glyphs.rule, total));
}
(RowSeparation::Blank, Framing::Framed | Framing::Unframed) => {
lines.extend(self.row_lines(&blank_cells, widths, glyphs));
}
(RowSeparation::None, Framing::Framed | Framing::Unframed) => {}
}
}
lines.extend(self.row_lines(row, widths, glyphs));
}
match framing {
Framing::Framed => lines.push(glyphs.bottom.line(widths)),
Framing::Unframed => {}
}
lines
}
}
fn narrowed(cells: &[Vec<Vec<Line>>], widths: &[u16]) -> Vec<u16> {
widths
.iter()
.enumerate()
.map(|(index, width)| {
cells
.iter()
.filter_map(|row| row.get(index))
.flatten()
.map(Line::width)
.max()
.unwrap_or(0)
.min(*width)
})
.collect()
}
impl Renderable for Table {
fn measure(&self, context: &Context) -> Measurement {
if self.is_blank() {
return Measurement::exact(0);
}
let extents = self.extents(context);
let frame = u64::from(self.frame_width());
let sum = |value: fn(&Extent) -> u16| {
extents.iter().fold(frame, |total, extent| {
total.saturating_add(u64::from(value(extent)))
})
};
let narrowest = match self.frame.framing() {
Framing::Framed => sum(|extent| extent.floor),
Framing::Unframed => sum(|extent| extent.minimum),
};
let natural = sum(|extent| extent.natural);
Measurement::new(
u16::try_from(narrowest).unwrap_or(u16::MAX),
u16::try_from(natural).unwrap_or(u16::MAX),
)
}
fn render(&self, context: &Context) -> Vec<Line> {
if self.is_blank() {
return Vec::new();
}
let extents = self.extents(context);
let available = context.width().saturating_sub(self.frame_width());
let widths = solve(&extents, available, self.frame.framing());
let cells = self.render_cells(&widths, context);
let widths = narrowed(&cells, &widths);
let glyphs = Glyphs::for_chars(context.box_chars());
self.compose(&cells, &widths, &glyphs)
}
}