use super::*;
use crate::rendering::{DisplayTable, TableAlignment};
pub(super) fn layout_table(
table: &DisplayTable,
available: usize,
theme: MissionControlTheme,
role: TranscriptCardRole,
) -> Vec<Vec<AssistantVisualSpan>> {
let rows = styled_table_rows(table, theme, role);
let Some(widths) = table_column_widths(&rows, table.alignments.len(), available) else {
let mut output = Vec::new();
for cells in rows {
if !output.is_empty() {
output.push(Vec::new());
}
for cell in cells {
output.extend(wrap_assistant_spans_to_width(cell, available));
}
}
return output;
};
let style = theme
.display_role(DisplayRole::Plain)
.bg(card_body_background(role, theme));
let mut output = Vec::new();
for (index, cells) in rows.into_iter().enumerate() {
let wrapped: Vec<_> = cells
.into_iter()
.zip(&widths)
.map(|(cell, width)| wrap_assistant_spans_to_width(cell, *width))
.collect();
let height = wrapped.iter().map(Vec::len).max().unwrap_or(1);
for line in 0..height {
let mut row = Vec::new();
for (column, width) in widths.iter().enumerate() {
if column > 0 {
row.push(AssistantVisualSpan::styled(" │ ", style, None));
}
let cell = wrapped
.get(column)
.and_then(|cell| cell.get(line))
.cloned()
.unwrap_or_default();
append_aligned_cell(&mut row, cell, *width, table.alignments[column], style);
}
output.push(row);
}
if index == 0 {
let rule = widths
.iter()
.map(|width| "─".repeat(*width))
.collect::<Vec<_>>()
.join("─┼─");
output.push(vec![AssistantVisualSpan::styled(rule, style, None)]);
}
}
output
}
fn styled_table_rows(
table: &DisplayTable,
theme: MissionControlTheme,
role: TranscriptCardRole,
) -> Vec<Vec<Vec<AssistantVisualSpan>>> {
table
.rows
.iter()
.enumerate()
.map(|(index, cells)| {
cells
.iter()
.map(|cell| {
cell.iter()
.map(|span| {
let mut style = theme
.display_role(span.role)
.bg(card_body_background(role, theme));
if index == 0 {
style = style.add_modifier(Modifier::BOLD);
}
AssistantVisualSpan::styled(span.text.clone(), style, Some(span.role))
})
.collect()
})
.collect()
})
.collect()
}
fn table_column_widths(
rows: &[Vec<Vec<AssistantVisualSpan>>],
columns: usize,
available: usize,
) -> Option<Vec<usize>> {
let mut desired = vec![1usize; columns];
let mut widths = vec![1usize; columns];
for cells in rows {
for (column, cell) in cells.iter().take(columns).enumerate() {
desired[column] = desired[column].max(
cell.iter()
.map(|span| UnicodeWidthStr::width(span.text.as_str()))
.sum(),
);
widths[column] = widths[column].max(
cell.iter()
.flat_map(|span| span.text.graphemes(true))
.map(UnicodeWidthStr::width)
.max()
.unwrap_or(1),
);
}
}
let separator_width = columns.saturating_sub(1) * 3;
let minimum = widths.iter().sum::<usize>() + separator_width;
if columns == 0 || minimum > available {
return None;
}
let mut remaining = available - minimum;
while remaining > 0 {
let mut grew = false;
for (width, desired) in widths.iter_mut().zip(&desired) {
if remaining > 0 && *width < *desired {
*width += 1;
remaining -= 1;
grew = true;
}
}
if !grew {
break;
}
}
Some(widths)
}
fn append_aligned_cell(
row: &mut Vec<AssistantVisualSpan>,
cell: Vec<AssistantVisualSpan>,
width: usize,
alignment: TableAlignment,
style: Style,
) {
let used: usize = cell
.iter()
.map(|span| UnicodeWidthStr::width(span.text.as_str()))
.sum();
let padding = width.saturating_sub(used);
let left = match alignment {
TableAlignment::Right => padding,
TableAlignment::Center => padding / 2,
_ => 0,
};
row.push(AssistantVisualSpan::styled(" ".repeat(left), style, None));
row.extend(cell);
row.push(AssistantVisualSpan::styled(
" ".repeat(padding - left),
style,
None,
));
}
#[cfg(test)]
mod tests {
use super::*;
fn render_table(markdown: &str, width: usize) -> Vec<Vec<AssistantVisualSpan>> {
let lines = crate::rendering::markup::render_markdown(markdown);
let table = lines
.iter()
.find_map(|line| line.table.as_ref())
.expect("table structure");
layout_table(
table,
width,
MissionControlTheme::default(),
TranscriptCardRole::Assistant,
)
}
#[test]
fn table_columns_align_headers_and_preserve_inline_code() {
let rows = render_table(
"| Left | Mid | Right |\n| :--- | :---: | ---: |\n| `x` | y | z |",
40,
);
let text: Vec<_> = rows
.iter()
.map(|row| assistant_wide_copy_text(row))
.collect();
assert_eq!(
text,
[
"Left │ Mid │ Right",
"─────┼─────┼──────",
"x │ y │ z"
]
);
assert!(
rows[0]
.iter()
.filter(|span| span.role.is_some())
.all(|span| span.style.add_modifier.contains(Modifier::BOLD))
);
assert!(
rows[2]
.iter()
.any(|span| span.text == "x" && span.role == Some(DisplayRole::InlineCode))
);
}
#[test]
fn table_cells_wrap_without_moving_column_boundaries() {
let rows = render_table("| A | B |\n| --- | --- |\n| abcdefgh | 界é界 |", 11);
let text: Vec<_> = rows
.iter()
.map(|row| assistant_wide_copy_text(row))
.collect();
assert_eq!(
text,
["A │ B ", "─────┼─────", "abcd │ 界é ", "efgh │ 界 "]
);
assert!(
text.iter()
.all(|row| UnicodeWidthStr::width(row.as_str()) == 11)
);
}
#[test]
fn narrow_table_fallback_keeps_every_cell_and_grapheme() {
for width in [1, 2, 4] {
let rows = render_table("| A | B |\n| --- | --- |\n| 界é | xy |", width);
let text: String = rows
.iter()
.map(|row| assistant_wide_copy_text(row))
.collect();
assert_eq!(text, "AB界éxy", "width {width}");
}
}
}