use ratatui::style::{Modifier, Stylize};
use ratatui::text::{Line, Span};
use unicode_width::UnicodeWidthStr;
use crate::wrap::wrap;
pub(super) type Cell = Vec<Vec<Span<'static>>>;
#[derive(Default)]
pub(super) struct Table {
pub(super) rows: Vec<Vec<Cell>>,
pub(super) header_rows: usize,
}
const GAP: &str = " ";
const LONGEST_WORD: usize = 30;
const NARROWEST: usize = 6;
pub(super) fn lines(
table: Table,
(first, rest): (Vec<Span<'static>>, Vec<Span<'static>>),
width: Option<usize>,
) -> Vec<Line<'static>> {
let Table { rows, header_rows } = table;
let columns = rows.iter().map(Vec::len).max().unwrap_or(0);
let natural: Vec<usize> = (0..columns)
.map(|at| column(&rows, at).map(cell_width).max().unwrap_or(0))
.collect();
let indent = Line::from(first.clone()).width();
let gaps = GAP.len() * columns.saturating_sub(1);
let room = width.map(|width| width.saturating_sub(indent + gaps));
let fitted = match room {
None => Some(natural.clone()),
Some(room) => fit(&rows, &natural, room),
};
let mut prefixes = std::iter::once(first).chain(std::iter::repeat(rest));
match fitted {
Some(widths) => grid(&rows, header_rows, &widths, &mut prefixes),
None if rows.len() > header_rows => {
let room = width.unwrap_or(0).saturating_sub(indent);
records(&rows, header_rows, room, &mut prefixes)
}
None => grid(&rows, header_rows, &natural, &mut prefixes),
}
}
fn column(rows: &[Vec<Cell>], at: usize) -> impl Iterator<Item = &Cell> {
rows.iter().filter_map(move |row| row.get(at))
}
fn cell_width(cell: &Cell) -> usize {
let line = |spans: &Vec<Span<'static>>| spans.iter().map(|span| span.content.width()).sum();
cell.iter().map(line).max().unwrap_or(0)
}
fn longest_word(cell: &Cell) -> usize {
let words = cell
.iter()
.flatten()
.flat_map(|span| span.content.split_whitespace());
words.map(str::width).max().unwrap_or(0).min(LONGEST_WORD)
}
fn fit(rows: &[Vec<Cell>], natural: &[usize], room: usize) -> Option<Vec<usize>> {
if natural.iter().sum::<usize>() <= room {
return Some(natural.to_vec());
}
let least: Vec<usize> = (0..natural.len())
.map(|at| {
let word = column(rows, at).map(longest_word).max().unwrap_or(0);
let wide = natural.get(at).copied().unwrap_or(0);
word.max(NARROWEST).min(wide)
})
.collect();
let spare = room.checked_sub(least.iter().sum())?;
let wants: Vec<usize> = natural.iter().zip(&least).map(|(n, l)| n - l).collect();
let wanted: usize = wants.iter().sum();
let mut widths: Vec<usize> = least
.iter()
.zip(&wants)
.map(|(least, wants)| least + (wants * spare).checked_div(wanted).unwrap_or(0))
.collect();
let mut left = room.saturating_sub(widths.iter().sum());
while left > 0 {
let mut grew = false;
for (width, natural) in widths.iter_mut().zip(natural) {
if left > 0 && *width < *natural {
*width += 1;
left -= 1;
grew = true;
}
}
if !grew {
break;
}
}
Some(widths)
}
fn wrapped(cell: Option<&Cell>, width: usize) -> Vec<Line<'static>> {
let lines = cell.into_iter().flatten();
let rows = lines.flat_map(|spans| wrap(&Line::from(spans.clone()), width));
rows.map(|(row, _)| row).collect()
}
fn grid(
rows: &[Vec<Cell>],
header_rows: usize,
widths: &[usize],
prefixes: &mut impl Iterator<Item = Vec<Span<'static>>>,
) -> Vec<Line<'static>> {
let laid: Vec<Vec<Vec<Line<'static>>>> = rows
.iter()
.map(|row| {
let cells = widths.iter().enumerate();
cells
.map(|(at, width)| wrapped(row.get(at), *width))
.collect()
})
.collect();
let tall = laid
.iter()
.skip(header_rows)
.any(|row| row.iter().any(|cell| cell.len() > 1));
let mut lines = Vec::new();
for (index, row) in laid.into_iter().enumerate() {
let header = index < header_rows;
if tall && index > header_rows {
lines.push(Line::default());
}
let height = row.iter().map(Vec::len).max().unwrap_or(0).max(1);
for at in 0..height {
let mut spans = prefixes.next().unwrap_or_default();
for (column, (cell, width)) in row.iter().zip(widths).enumerate() {
if column > 0 {
spans.push(Span::from(GAP));
}
let line = cell.get(at);
for span in line.into_iter().flat_map(|line| line.spans.iter()) {
let mut span = span.clone();
if header {
span.style = span
.style
.add_modifier(Modifier::BOLD | Modifier::UNDERLINED);
}
spans.push(span);
}
if column + 1 < widths.len() {
let used = line.map_or(0, Line::width);
spans.push(Span::from(" ".repeat(width.saturating_sub(used))));
}
}
lines.push(Line::from(spans));
}
}
lines
}
fn records(
rows: &[Vec<Cell>],
header_rows: usize,
room: usize,
prefixes: &mut impl Iterator<Item = Vec<Span<'static>>>,
) -> Vec<Line<'static>> {
let labels: Vec<String> = rows
.first()
.filter(|_| header_rows > 0)
.map(|header| {
let text = |cell: &Cell| {
let lines = cell.iter().map(|spans| {
spans
.iter()
.map(|span| span.content.as_ref())
.collect::<String>()
});
lines.collect::<Vec<_>>().join(" ")
};
header.iter().map(text).collect()
})
.unwrap_or_default();
let label_width = labels.iter().map(|label| label.width()).max().unwrap_or(0);
let beside = label_width + 2 + 2 * NARROWEST <= room;
let mut lines = Vec::new();
for (index, row) in rows.iter().skip(header_rows).enumerate() {
if index > 0 {
lines.push(Line::default());
}
for (at, cell) in row.iter().enumerate() {
let label = labels.get(at).cloned().unwrap_or_default();
let mut spans = prefixes.next().unwrap_or_default();
if beside {
let value = wrapped(Some(cell), room - label_width - 2);
let pad = " ".repeat(label_width - label.width() + 2);
spans.push(Span::from(label).bold());
spans.push(Span::from(pad));
let mut value = value.into_iter();
spans.extend(value.next().map(|line| line.spans).unwrap_or_default());
lines.push(Line::from(spans));
for line in value {
let mut spans = prefixes.next().unwrap_or_default();
spans.push(Span::from(" ".repeat(label_width + 2)));
spans.extend(line.spans);
lines.push(Line::from(spans));
}
} else {
spans.push(Span::from(label).bold());
lines.push(Line::from(spans));
for line in wrapped(Some(cell), room.saturating_sub(2).max(1)) {
let mut spans = prefixes.next().unwrap_or_default();
spans.push(Span::from(" "));
spans.extend(line.spans);
lines.push(Line::from(spans));
}
}
}
}
lines
}
#[cfg(test)]
mod tests;