use crate::ast::{Node, NodeKind, Style, TextAlign, computed_style_to_text_style};
use crate::generator::text::{annotate_runs_with_urls, collect_inline_segments};
use crate::text::{
FONT_CONTEXT, LAYOUT_CONTEXT, TextAlign as TextAlign2, TextStyle, layout_text_with_contexts,
};
use crate::visual::{FillStrokeStyle, StrokeStyle, VisualElement};
use vello_cpu::kurbo::{Point, Rect};
struct CellMeasure {
ideal_width: f32,
min_width: f32,
}
pub struct TableLayoutInfo {
pub num_rows: usize,
pub num_cols: usize,
pub col_widths: Vec<f32>,
pub row_heights: Vec<f32>,
pub row_align: Vec<TextAlign>,
}
pub fn compute_layout_info(node: &Node, content_width: f32) -> TableLayoutInfo {
let rows = collect_rows(node);
let num_cols = rows.iter().map(|r| r.len()).max().unwrap_or(0);
if num_cols == 0 || rows.is_empty() {
return TableLayoutInfo {
num_rows: 0,
num_cols: 0,
col_widths: vec![],
row_heights: vec![],
row_align: vec![],
};
}
let align = build_alignment(node, num_cols);
let cell_measures = measure_cells(&rows, num_cols);
let col_widths = calculate_column_widths(&cell_measures, num_cols, content_width);
let row_heights = calculate_row_heights(&rows, &col_widths, num_cols);
TableLayoutInfo {
num_rows: rows.len(),
num_cols,
col_widths,
row_heights,
row_align: align,
}
}
pub fn generate_rows(
node: &Node,
layout: &TableLayoutInfo,
row_start: usize,
row_end: usize,
style: &Style,
) -> Vec<VisualElement> {
if row_start >= row_end || row_start >= layout.num_rows {
return vec![];
}
let end = row_end.min(layout.num_rows);
let rows = collect_rows(node);
let col_starts = compute_column_starts(&layout.col_widths);
let row_starts = compute_row_starts_range(&layout.row_heights, row_start, end);
let mut elements = Vec::new();
let has_header = !rows.is_empty();
let chunk_table_width: f32 = col_starts.last().copied().unwrap_or(0.0)
+ layout.col_widths.last().copied().unwrap_or(0.0);
for (chunk_idx, abs_row_idx) in (row_start..end).enumerate() {
let row_y = row_starts[chunk_idx];
let row_h = layout.row_heights[abs_row_idx];
if has_header && abs_row_idx == 0 {
if let Some(bg) = style.table_header_bg {
elements.push(VisualElement::Rect {
rect: Rect::new(
0.0,
row_y as f64,
chunk_table_width as f64,
(row_y + row_h) as f64,
),
style: FillStrokeStyle {
fill: Some(bg),
stroke: None,
},
});
}
} else if abs_row_idx > 0 && abs_row_idx % 2 == 0 {
if let Some(bg) = style.table_alt_row_bg {
elements.push(VisualElement::Rect {
rect: Rect::new(
0.0,
row_y as f64,
chunk_table_width as f64,
(row_y + row_h) as f64,
),
style: FillStrokeStyle {
fill: Some(bg),
stroke: None,
},
});
}
}
}
layout_cell_texts(
&rows,
&col_starts,
&layout.col_widths,
&row_starts,
&layout.row_heights,
&layout.row_align,
layout.num_cols,
row_start,
end,
style,
&mut elements,
);
draw_table_borders_range(
&col_starts,
&layout.col_widths,
&row_starts,
&layout.row_heights,
row_start,
end,
style,
&mut elements,
);
elements
}
fn collect_rows(node: &Node) -> Vec<Vec<&Node>> {
let mut rows = Vec::new();
if let NodeKind::Table { children, .. } = &node.kind {
for child in children {
if let NodeKind::TableRow { children: cells } = &child.kind {
let row: Vec<&Node> = cells.iter().collect();
rows.push(row);
}
}
}
rows
}
fn build_alignment(node: &Node, num_cols: usize) -> Vec<TextAlign> {
let mut a = match &node.kind {
NodeKind::Table { align, .. } => align.clone(),
_ => vec![],
};
while a.len() < num_cols {
a.push(TextAlign::Left);
}
a.truncate(num_cols);
a
}
fn measure_cells(rows: &[Vec<&Node>], num_cols: usize) -> Vec<Vec<CellMeasure>> {
let padding_h = 4.0; let mut measures = Vec::with_capacity(rows.len());
for row in rows {
let mut row_meas = Vec::with_capacity(num_cols);
for cell in row.iter().take(num_cols) {
row_meas.push(measure_cell(cell, padding_h));
}
while row_meas.len() < num_cols {
row_meas.push(CellMeasure {
ideal_width: padding_h * 2.0,
min_width: padding_h * 2.0,
});
}
measures.push(row_meas);
}
measures
}
fn measure_cell(cell: &Node, padding_h: f32) -> CellMeasure {
let segments = collect_inline_segments_from_cell(cell);
if segments.is_empty() {
return CellMeasure {
ideal_width: padding_h * 2.0,
min_width: padding_h * 2.0,
};
}
let texts: Vec<(&str, &TextStyle)> = segments.iter().map(|(t, s)| (t.as_str(), s)).collect();
let ideal_width = FONT_CONTEXT.with(|font_cx| {
LAYOUT_CONTEXT.with(|layout_cx| {
let mut fcx = font_cx.borrow_mut();
let mut lcx = layout_cx.borrow_mut();
let layout =
layout_text_with_contexts(&texts, None, TextAlign2::Left, &mut fcx, &mut lcx);
layout.width as f32
})
});
let min_width = segments
.iter()
.flat_map(|(text, style)| {
let words = split_words(text);
words.into_iter().map(move |w| (w, style))
})
.filter(|(w, _)| !w.is_empty())
.map(|(word, style)| {
FONT_CONTEXT.with(|font_cx| {
LAYOUT_CONTEXT.with(|layout_cx| {
let mut fcx = font_cx.borrow_mut();
let mut lcx = layout_cx.borrow_mut();
let layout = layout_text_with_contexts(
&[(word, style)],
None,
TextAlign2::Left,
&mut fcx,
&mut lcx,
);
layout.width as f32
})
})
})
.fold(0.0_f32, f32::max);
CellMeasure {
ideal_width: ideal_width + padding_h * 2.0,
min_width: (min_width + padding_h * 2.0).max(padding_h * 2.0),
}
}
fn collect_inline_segments_from_cell(cell: &Node) -> Vec<(String, TextStyle)> {
match &cell.kind {
NodeKind::Paragraph { children } => collect_inline_segments(children),
_ => {
let text = cell.text_content();
if text.is_empty() {
vec![]
} else {
vec![(text, computed_style_to_text_style(&cell.style))]
}
}
}
}
fn calculate_column_widths(
cell_measures: &[Vec<CellMeasure>],
num_cols: usize,
available_width: f32,
) -> Vec<f32> {
let mut ideal_w = vec![0.0_f32; num_cols];
let mut min_w = vec![0.0_f32; num_cols];
for row in cell_measures {
for (c, m) in row.iter().enumerate().take(num_cols) {
if m.ideal_width > ideal_w[c] {
ideal_w[c] = m.ideal_width;
}
if m.min_width > min_w[c] {
min_w[c] = m.min_width;
}
}
}
let total_ideal: f32 = ideal_w.iter().sum();
if total_ideal <= available_width {
return ideal_w;
}
let total_min: f32 = min_w.iter().sum();
if total_min >= available_width {
return min_w;
}
let extra = available_width - total_min;
let ideal_extra: f32 = ideal_w.iter().zip(min_w.iter()).map(|(i, m)| i - m).sum();
let mut final_w = Vec::with_capacity(num_cols);
for c in 0..num_cols {
let ratio = if ideal_extra > 0.0 {
(ideal_w[c] - min_w[c]) / ideal_extra
} else {
1.0 / num_cols as f32
};
final_w.push(min_w[c] + extra * ratio);
}
final_w
}
fn calculate_row_heights(rows: &[Vec<&Node>], col_widths: &[f32], num_cols: usize) -> Vec<f32> {
let padding_h = 4.0;
let padding_v = 2.0;
let mut heights = Vec::with_capacity(rows.len());
for row in rows {
let mut max_height = 0.0_f32;
for (c, cell) in row.iter().enumerate().take(num_cols) {
let cell_width = col_widths[c] - padding_h * 2.0;
if cell_width <= 0.0 {
continue;
}
let segments = collect_inline_segments_from_cell(cell);
if segments.is_empty() {
max_height = max_height.max(padding_v * 2.0);
continue;
}
let texts: Vec<(&str, &TextStyle)> =
segments.iter().map(|(t, s)| (t.as_str(), s)).collect();
let height = FONT_CONTEXT.with(|font_cx| {
LAYOUT_CONTEXT.with(|layout_cx| {
let mut fcx = font_cx.borrow_mut();
let mut lcx = layout_cx.borrow_mut();
let layout = layout_text_with_contexts(
&texts,
Some(cell_width as f64),
TextAlign2::Left,
&mut fcx,
&mut lcx,
);
let mut h = 0.0_f32;
for line in &layout.lines {
h += line.line_height;
}
h
})
});
max_height = max_height.max(height + padding_v * 2.0);
}
heights.push(max_height.max(padding_v * 2.0));
}
heights
}
fn compute_column_starts(col_widths: &[f32]) -> Vec<f32> {
let mut starts = Vec::with_capacity(col_widths.len());
let mut x = 0.0_f32;
for w in col_widths {
starts.push(x);
x += w;
}
starts
}
fn compute_row_starts_range(row_heights: &[f32], row_start: usize, row_end: usize) -> Vec<f32> {
let mut starts = Vec::with_capacity(row_end - row_start);
let mut y = 0.0_f32;
for i in row_start..row_end {
starts.push(y);
y += row_heights[i];
}
starts
}
fn layout_cell_texts(
rows: &[Vec<&Node>],
col_starts: &[f32],
col_widths: &[f32],
row_starts: &[f32],
_row_heights: &[f32],
align: &[TextAlign],
num_cols: usize,
row_start: usize,
row_end: usize,
style: &Style,
elements: &mut Vec<VisualElement>,
) {
let padding_h = style.table_cell_padding_h_pt;
let padding_v = style.table_cell_padding_v_pt;
for (chunk_idx, abs_row_idx) in (row_start..row_end).enumerate() {
if abs_row_idx >= rows.len() {
break;
}
let row = &rows[abs_row_idx];
let cell_y = row_starts[chunk_idx];
for (c, cell) in row.iter().enumerate().take(num_cols) {
let cell_x = col_starts[c];
let cell_w = col_widths[c];
let segments = collect_inline_segments_from_cell(cell);
if segments.is_empty() {
continue;
}
let text_align = map_text_align(align.get(c).copied().unwrap_or(TextAlign::Left));
let cell_text_width = cell_w - padding_h * 2.0;
if cell_text_width <= 0.0 {
continue;
}
let texts: Vec<(&str, &TextStyle)> =
segments.iter().map(|(t, s)| (t.as_str(), s)).collect();
FONT_CONTEXT.with(|font_cx| {
LAYOUT_CONTEXT.with(|layout_cx| {
let mut fcx = font_cx.borrow_mut();
let mut lcx = layout_cx.borrow_mut();
let total_text: String = segments.iter().map(|(t, _)| t.as_str()).collect();
let layout = layout_text_with_contexts(
&texts,
Some(cell_text_width as f64),
text_align,
&mut fcx,
&mut lcx,
);
let mut lines = layout.lines;
annotate_runs_with_urls(&mut lines, &total_text, &segments);
for line in &lines {
let line_abs_x = cell_x + padding_h + line.bounds.x0 as f32;
let line_abs_y = cell_y + padding_v + line.bounds.y0 as f32;
let line_width = line.bounds.width() as f32;
let bounds = Rect::new(
line_abs_x as f64,
line_abs_y as f64,
(line_abs_x + line_width) as f64,
(line_abs_y + line.line_height) as f64,
);
elements.push(VisualElement::TextLine {
runs: line.runs.clone(),
bounds,
line_height: line.line_height,
});
}
})
});
}
}
}
fn draw_table_borders_range(
col_starts: &[f32],
col_widths: &[f32],
row_starts: &[f32],
row_heights: &[f32],
row_start: usize,
row_end: usize,
style: &Style,
elements: &mut Vec<VisualElement>,
) {
let border = StrokeStyle {
color: style.table_border_color,
width: style.table_border_width_pt as f64,
};
let table_width: f32 =
col_starts.last().copied().unwrap_or(0.0) + col_widths.last().copied().unwrap_or(0.0);
let chunk_first_y = row_starts.first().copied().unwrap_or(0.0);
let chunk_last_y = row_starts.last().copied().unwrap_or(0.0)
+ if let (Some(&last_h), Some(_last_start)) =
(row_heights.get(row_end - 1), row_starts.last())
{
last_h
} else {
0.0
};
elements.push(VisualElement::Line {
start: Point::new(0.0, chunk_first_y as f64),
end: Point::new(table_width as f64, chunk_first_y as f64),
style: border.clone(),
});
for (chunk_idx, abs_row_idx) in (row_start..row_end).enumerate() {
let y = row_starts[chunk_idx] + row_heights[abs_row_idx];
elements.push(VisualElement::Line {
start: Point::new(0.0, y as f64),
end: Point::new(table_width as f64, y as f64),
style: border.clone(),
});
}
for c in 0..=col_widths.len() {
let x = if c == 0 {
0.0
} else if c < col_starts.len() {
col_starts[c]
} else {
col_starts.last().copied().unwrap_or(0.0) + col_widths.last().copied().unwrap_or(0.0)
};
elements.push(VisualElement::Line {
start: Point::new(x as f64, chunk_first_y as f64),
end: Point::new(x as f64, chunk_last_y as f64),
style: border.clone(),
});
}
}
fn map_text_align(a: TextAlign) -> TextAlign2 {
match a {
TextAlign::Left => TextAlign2::Left,
TextAlign::Center => TextAlign2::Center,
TextAlign::Right => TextAlign2::Right,
TextAlign::Justify => TextAlign2::Left,
}
}
fn split_words(text: &str) -> Vec<&str> {
let mut words = Vec::new();
let mut start = 0;
let mut in_word = false;
for (i, c) in text.char_indices() {
if c.is_whitespace() {
if in_word {
words.push(&text[start..i]);
in_word = false;
}
} else if is_cjk(c) {
if in_word {
words.push(&text[start..i]);
in_word = false;
}
words.push(&text[i..i + c.len_utf8()]);
} else {
if !in_word {
start = i;
in_word = true;
}
}
}
if in_word {
words.push(&text[start..]);
}
words
}
fn is_cjk(c: char) -> bool {
matches!(c,
'\u{4E00}'..='\u{9FFF}'
| '\u{3400}'..='\u{4DBF}'
| '\u{F900}'..='\u{FAFF}'
| '\u{2F800}'..='\u{2FA1F}'
)
}