use ratatui::{
style::{Modifier, Style},
text::Span,
};
use unicode_width::UnicodeWidthChar;
use super::tables::{CellFragment, CellInlineStyle};
use super::width::{display_width, expand_tabs};
use super::LINK_MARKER;
pub(super) fn fragments_display_width(frags: &[CellFragment]) -> usize {
frags.iter().map(|f| f.display_width()).sum()
}
pub(super) fn min_table_cell_width(frags: &[CellFragment]) -> usize {
let mut max_width = 4usize;
for frag in frags {
let w = if frag.is_text() {
frag.rendered_text()
.split_whitespace()
.map(display_width)
.max()
.unwrap_or(0)
.min(12)
} else {
display_width(&frag.rendered_text()).min(12) + 2
};
max_width = max_width.max(w);
}
max_width
}
fn table_available_width(col_count: usize, render_width: usize) -> usize {
let border_width = 3 * col_count + 1;
render_width.saturating_sub(border_width).max(col_count)
}
pub(super) fn fit_table_widths(
col_widths: &mut [usize],
min_widths: &[usize],
render_width: usize,
) {
if col_widths.is_empty() {
return;
}
let col_count = col_widths.len();
let available = table_available_width(col_count, render_width);
let min_total: usize = min_widths.iter().sum();
if min_total >= available {
let mut widths = vec![1; col_count];
let mut remaining = available.saturating_sub(col_count);
let mut order: Vec<usize> = (0..col_count).collect();
order.sort_by_key(|&idx| std::cmp::Reverse(min_widths[idx]));
for idx in order {
if remaining == 0 {
break;
}
let extra = (min_widths[idx].saturating_sub(1)).min(remaining);
widths[idx] += extra;
remaining -= extra;
}
col_widths.copy_from_slice(&widths);
return;
}
while col_widths.iter().sum::<usize>() > available {
let Some((idx, _)) = col_widths
.iter()
.enumerate()
.filter(|(idx, width)| **width > min_widths[*idx])
.max_by_key(|(_, width)| **width)
else {
break;
};
col_widths[idx] -= 1;
}
}
pub(super) fn cap_table_widths(col_widths: &mut [usize], render_width: usize) {
let col_count = col_widths.len();
if col_count == 0 {
return;
}
let available = table_available_width(col_count, render_width);
let max_col = (available / col_count).max(1);
let need: Vec<usize> = col_widths.to_vec();
for w in col_widths.iter_mut() {
*w = (*w).min(max_col);
}
let surplus = available.saturating_sub(col_widths.iter().sum());
let demand_count = need
.iter()
.zip(col_widths.iter())
.filter(|(n, w)| n > w)
.count();
if surplus == 0 || demand_count == 0 {
return;
}
let share = surplus / demand_count;
let rem = surplus % demand_count;
let mut k = 0;
for i in 0..col_count {
if need[i] > col_widths[i] {
let want = need[i] - col_widths[i];
col_widths[i] += (share + usize::from(k < rem)).min(want);
k += 1;
}
}
}
fn rebuild_fragment(frag: &CellFragment, text: String) -> CellFragment {
match frag {
CellFragment::InlineMath(_, _) => CellFragment::InlineMath(text, false),
CellFragment::Mark(_, _) => CellFragment::Mark(text, false),
_ => CellFragment::Code(text, false),
}
}
pub(super) fn wrap_table_cell(frags: &[CellFragment], width: usize) -> Vec<Vec<CellFragment>> {
if width == 0 {
return vec![vec![]];
}
if frags.is_empty() {
return vec![vec![]];
}
let mut lines: Vec<Vec<CellFragment>> = Vec::new();
let mut current_line: Vec<CellFragment> = Vec::new();
let mut current_width = 0usize;
let mut glue = false;
let space_frag = || CellFragment::Text(" ".to_string(), CellInlineStyle::default(), false);
for frag in frags {
match frag {
CellFragment::Text(t, style, adj) => {
let expanded = expand_tabs(t, 0);
let style = *style;
let adj = *adj;
let mut first_word = true;
for word in expanded.split_whitespace() {
let word_width = display_width(word);
if word_width > width {
if !current_line.is_empty() || current_width > 0 {
lines.push(std::mem::take(&mut current_line));
current_width = 0;
}
glue = false;
first_word = false;
let mut chunk = String::new();
let mut chunk_width = 0usize;
for ch in word.chars() {
let ch_width = UnicodeWidthChar::width(ch).unwrap_or(0);
if chunk_width + ch_width > width && !chunk.is_empty() {
lines.push(vec![CellFragment::Text(
std::mem::take(&mut chunk),
style,
false,
)]);
chunk_width = 0;
}
chunk.push(ch);
chunk_width += ch_width;
}
if !chunk.is_empty() {
current_line.push(CellFragment::Text(chunk, style, false));
current_width = chunk_width;
}
continue;
}
let suppress = adj && first_word;
first_word = false;
let needs_sep = current_width > 0 && !glue && !suppress;
glue = false;
let sep = if needs_sep { 1 } else { 0 };
if current_width + sep + word_width > width && current_width > 0 {
lines.push(std::mem::take(&mut current_line));
current_width = 0;
} else if needs_sep {
current_line.push(CellFragment::Text(" ".to_string(), style, false));
current_width += 1;
}
current_line.push(CellFragment::Text(word.to_string(), style, false));
current_width += word_width;
}
}
CellFragment::LinkMarker(_) => {
if current_width + 1 > width && current_width > 0 {
lines.push(std::mem::take(&mut current_line));
current_width = 0;
}
if current_width > 0 {
current_line.push(space_frag());
current_width += 1;
}
current_line.push(frag.clone());
current_width += 1;
glue = true;
}
CellFragment::Code(_, adj)
| CellFragment::InlineMath(_, adj)
| CellFragment::Mark(_, adj) => {
let adj = *adj;
let text = frag.rendered_text();
let frag_width = display_width(&text) + 2;
if frag_width > width {
if !current_line.is_empty() || current_width > 0 {
lines.push(std::mem::take(&mut current_line));
current_width = 0;
}
glue = false;
let inner = width.saturating_sub(2).max(1);
let mut chunk = String::new();
let mut chunk_width = 0usize;
for ch in text.chars() {
let ch_width = UnicodeWidthChar::width(ch).unwrap_or(0);
if chunk_width + ch_width > inner && !chunk.is_empty() {
lines.push(vec![rebuild_fragment(frag, std::mem::take(&mut chunk))]);
chunk_width = 0;
}
chunk.push(ch);
chunk_width += ch_width;
}
if !chunk.is_empty() {
current_line.push(rebuild_fragment(frag, chunk));
current_width = chunk_width + 2;
}
continue;
}
let sep = if current_width == 0 || adj { 0 } else { 1 };
if current_width + sep + frag_width > width && current_width > 0 {
lines.push(std::mem::take(&mut current_line));
current_width = 0;
}
if current_width > 0 && !adj {
current_line.push(space_frag());
current_width += 1;
}
current_line.push(frag.clone());
current_width += frag_width;
}
}
}
if !current_line.is_empty() {
lines.push(current_line);
}
if lines.is_empty() {
lines.push(vec![]);
}
lines
}
pub(super) fn align_cell(
frags: &[CellFragment],
width: usize,
align: pulldown_cmark::Alignment,
base_style: Style,
is_header: bool,
theme: &crate::theme::MarkdownTheme,
) -> Vec<Span<'static>> {
let mut spans = Vec::new();
let mut content_width = 0usize;
for frag in frags {
match frag {
CellFragment::Text(t, inline, _) => {
let expanded = expand_tabs(t, 0);
content_width += display_width(&expanded);
let mut style = base_style;
if inline.bold {
style = style.add_modifier(Modifier::BOLD);
if !is_header {
style = style.fg(theme.strong_text);
}
}
if inline.italic {
style = style.add_modifier(Modifier::ITALIC);
}
if inline.strikethrough {
style = style.add_modifier(Modifier::CROSSED_OUT);
}
if inline.link {
style = style.fg(theme.link_text).add_modifier(Modifier::UNDERLINED);
}
spans.push(Span::styled(expanded, style));
}
CellFragment::LinkMarker(inline) => {
let style = Style::default()
.fg(theme.link_icon)
.add_modifier(inline.modifiers());
spans.push(Span::styled(LINK_MARKER, style));
content_width += display_width(LINK_MARKER);
}
CellFragment::Code(_, _)
| CellFragment::InlineMath(_, _)
| CellFragment::Mark(_, _) => {
let styled = format!(" {} ", frag.rendered_text());
content_width += display_width(&styled);
let (fg, bg) = match frag {
CellFragment::Code(..) => (theme.inline_code_fg, theme.inline_code_bg),
CellFragment::Mark(..) => (theme.mark_fg, theme.mark_bg),
_ => (theme.latex_inline_fg, theme.latex_inline_bg),
};
spans.push(Span::styled(styled, Style::default().fg(fg).bg(bg)));
}
}
}
if content_width < width {
let pad = width - content_width;
match align {
pulldown_cmark::Alignment::Right => {
spans.insert(0, Span::styled(" ".repeat(pad), base_style));
}
pulldown_cmark::Alignment::Center => {
let l = pad / 2;
spans.insert(0, Span::styled(" ".repeat(l), base_style));
spans.push(Span::styled(" ".repeat(pad - l), base_style));
}
_ => {
spans.push(Span::styled(" ".repeat(pad), base_style));
}
}
}
spans
}