use crate::core::border::BorderChars;
use crate::core::geometry::Align;
use crate::core::render::Rendered;
use crate::core::style::Style;
use crate::core::text::{Line, Span};
use crate::core::width::{truncate, wrap};
use crate::output::layout::pad_line;
use crate::output::table::plan::{PlacedCell, RowPlan};
use crate::output::table::{Cell, Table};
pub(super) struct Builder<'a> {
table: &'a Table,
widths: &'a [usize],
plan: &'a [RowPlan<'a>],
chars: BorderChars,
}
impl<'a> Builder<'a> {
pub(super) fn new(
table: &'a Table,
widths: &'a [usize],
plan: &'a [RowPlan<'a>],
) -> Self {
Self {
table,
widths,
plan,
chars: table.border.chars(),
}
}
pub(super) fn build(&self) -> Rendered {
let mut lines = Vec::new();
self.push_title(&mut lines);
self.push_border(&mut lines, Edge::Top);
if self.table.header {
self.push_header(&mut lines);
self.push_border(&mut lines, Edge::Middle);
}
self.push_body(&mut lines);
self.push_border(&mut lines, Edge::Bottom);
Rendered::new(lines)
}
fn push_title(&self, lines: &mut Vec<Line>) {
let Some(title) = &self.table.title else {
return;
};
let title_line = title.lines.first().cloned().unwrap_or_default();
let width = self.total_width();
lines.push(pad_line(
title_line,
width,
Align::Center,
self.table.title_style,
));
}
fn push_header(&self, lines: &mut Vec<Line>) {
let cells: Vec<Cell> = self
.table
.columns
.iter()
.map(|column| Cell::new(column.header.clone()).align(column.align))
.collect();
let placed: Vec<PlacedCell> = cells
.iter()
.enumerate()
.map(|(index, cell)| PlacedCell {
cell: Some(cell),
start: index,
colspan: 1,
})
.collect();
self.push_row(lines, &placed, self.table.header_style, false);
}
fn push_body(&self, lines: &mut Vec<Line>) {
let last_body = self.plan.iter().rposition(|r| !r.footer);
let mut body_index = 0;
let mut footer_started = false;
for (index, row) in self.plan.iter().enumerate() {
if row.footer && !footer_started {
self.push_border(lines, Edge::Middle);
footer_started = true;
}
let striped = self.table.striped && body_index % 2 == 1;
let style = if striped {
self.table.stripe_style
} else {
Style::new()
};
self.push_row(lines, &row.cells, style, striped);
if !row.footer {
body_index += 1;
if self.table.row_separators && Some(index) != last_body {
self.push_border(lines, Edge::Middle);
}
}
}
}
fn push_row(
&self,
lines: &mut Vec<Line>,
placed: &[PlacedCell],
style: Style,
fill_bg: bool,
) {
let cell_lines = self.cell_lines(placed, style);
let height = cell_lines.iter().map(Vec::len).max().unwrap_or(1).max(1);
for physical in 0..height {
lines.push(self.row_line(placed, &cell_lines, physical, fill_bg));
}
}
fn cell_lines(
&self,
placed: &[PlacedCell],
style: Style,
) -> Vec<Vec<Line>> {
placed
.iter()
.map(|slot| match slot.cell {
None => vec![Line::default()],
Some(cell) => {
let width = self.span_width(slot.start, slot.colspan);
let wrap = self.column_wraps(slot.start);
format_cell(cell, width, style, wrap)
}
})
.collect()
}
fn column_wraps(&self, index: usize) -> bool {
self.table.columns.get(index).is_some_and(|c| c.wrap)
}
fn row_line(
&self,
placed: &[PlacedCell],
cell_lines: &[Vec<Line>],
physical: usize,
fill_bg: bool,
) -> Line {
let pad = self.table.pad as usize;
let mut spans = vec![self.vbar()];
for (slot_index, slot) in placed.iter().enumerate() {
let width = self.span_width(slot.start, slot.colspan);
let align = slot
.cell
.and_then(|c| c.align)
.unwrap_or(self.align_for(slot.start));
let content = blank_or(cell_lines, slot_index, physical);
let fill = if fill_bg {
self.table.stripe_style
} else {
Style::new()
};
push_cell(&mut spans, content, width, pad, align, fill);
spans.push(self.vbar());
}
Line::new(spans)
}
fn align_for(&self, index: usize) -> Align {
self.table
.columns
.get(index)
.map_or(Align::Left, |column| column.align)
}
fn span_width(&self, start: usize, colspan: usize) -> usize {
let pad = self.table.pad as usize;
let end = (start + colspan).min(self.widths.len());
let content: usize = self.widths[start..end].iter().sum();
let extra = colspan.saturating_sub(1) * (2 * pad + 1);
content + extra
}
fn total_width(&self) -> usize {
let pad = self.table.pad as usize;
let content: usize = self.widths.iter().sum();
let cells = self.widths.len();
content + cells * 2 * pad + cells + 1
}
fn vbar(&self) -> Span {
Span::styled(self.chars.vertical.to_string(), self.table.border_style)
}
fn push_border(&self, lines: &mut Vec<Line>, edge: Edge) {
if self.table.border.is_none() {
return;
}
let (left, mid, right) = edge.corners(&self.chars);
let pad = self.table.pad as usize;
let mut content = String::new();
content.push(left);
for (index, width) in self.widths.iter().enumerate() {
let segment = width + 2 * pad;
content
.push_str(&self.chars.horizontal.to_string().repeat(segment));
if index + 1 < self.widths.len() {
content.push(mid);
}
}
content.push(right);
lines.push(Line::styled(content, self.table.border_style));
}
}
#[derive(Clone, Copy)]
enum Edge {
Top,
Middle,
Bottom,
}
impl Edge {
fn corners(self, chars: &BorderChars) -> (char, char, char) {
match self {
Edge::Top => (chars.top_left, chars.tee_down, chars.top_right),
Edge::Middle => (chars.tee_right, chars.cross, chars.tee_left),
Edge::Bottom => {
(chars.bottom_left, chars.tee_up, chars.bottom_right)
}
}
}
}
fn format_cell(
cell: &Cell,
width: usize,
style: Style,
wrap_cell: bool,
) -> Vec<Line> {
let mut out = Vec::new();
for line in &cell.content.lines {
let plain = line.plain();
if line.width() <= width {
out.push(restyle(line.clone(), style));
} else if wrap_cell {
for chunk in wrap(&plain, width) {
out.push(Line::styled(chunk, style));
}
} else {
let span_style = line.spans.first().map_or(style, |s| s.style);
let cell_style = style.patch(span_style);
out.push(Line::styled(truncate(&plain, width, "…"), cell_style));
}
}
if out.is_empty() {
out.push(Line::default());
}
out
}
fn restyle(line: Line, base: Style) -> Line {
let spans = line
.spans
.into_iter()
.map(|mut span| {
span.style = base.patch(span.style);
span
})
.collect();
Line::new(spans)
}
fn blank_or(cell_lines: &[Vec<Line>], index: usize, physical: usize) -> Line {
cell_lines
.get(index)
.and_then(|lines| lines.get(physical))
.cloned()
.unwrap_or_default()
}
fn push_cell(
spans: &mut Vec<Span>,
content: Line,
width: usize,
pad: usize,
align: Align,
fill: Style,
) {
let truncated = clip(content, width);
let padded = pad_line(truncated, width, align, fill);
if pad > 0 {
spans.push(Span::styled(" ".repeat(pad), fill));
}
spans.extend(padded.spans);
if pad > 0 {
spans.push(Span::styled(" ".repeat(pad), fill));
}
}
fn clip(line: Line, width: usize) -> Line {
if line.width() <= width {
return line;
}
let style = line.spans.first().map(|s| s.style).unwrap_or_default();
Line::styled(truncate(&line.plain(), width, "…"), style)
}