use crate::render::dimension::Pt;
use super::types::{
CellLayoutEntry, MeasuredTable, TableCellInput, TableRowInput, VerticalMergeState,
};
pub(super) struct RowGroup {
pub(super) start: usize,
pub(super) end: usize, pub(super) height: Pt,
pub(super) splittable: bool,
}
pub fn compute_column_widths(grid_cols: &[Pt], num_cols: usize, available_width: Pt) -> Vec<Pt> {
let total: Pt = grid_cols.iter().copied().sum();
if !grid_cols.is_empty() && total > Pt::ZERO {
let scale = available_width / total;
return grid_cols.iter().map(|w| *w * scale).collect();
}
let n = if num_cols > 0 {
num_cols
} else {
grid_cols.len()
};
if n > 0 {
vec![available_width / n as f32; n]
} else {
Vec::new()
}
}
pub(super) fn build_row_groups(rows: &[TableRowInput], measured: &MeasuredTable) -> Vec<RowGroup> {
let mut groups = Vec::new();
let mut i = 0;
while i < rows.len() {
let start = i;
i = row_group_end(rows, start);
let height: Pt = measured.rows[start..i]
.iter()
.map(|mr| mr.height + mr.border_gap_below)
.sum();
let is_vmerge_span = i - start > 1;
let any_cant_split = rows[start..i].iter().any(|r| r.cant_split == Some(true));
let has_nested_table = rows[start..i]
.iter()
.flat_map(|r| r.cells.iter())
.any(cell_has_nested_table);
let splittable = !is_vmerge_span && !any_cant_split && !has_nested_table;
groups.push(RowGroup {
start,
end: i,
height,
splittable,
});
}
groups
}
pub(super) fn row_group_end(rows: &[TableRowInput], start: usize) -> usize {
let mut end = start + 1;
while end < rows.len()
&& rows[end]
.cells
.iter()
.any(|cell| cell.vertical_merge == Some(VerticalMergeState::Continue))
{
end += 1;
}
end
}
fn cell_has_nested_table(cell: &TableCellInput) -> bool {
use crate::render::layout::section::LayoutBlock;
cell.blocks
.iter()
.any(|b| matches!(b, LayoutBlock::Table { .. }))
}
pub(super) fn expand_rows_for_vmerge(
rows: &[TableRowInput],
row_cell_layouts: &[Vec<CellLayoutEntry>],
row_heights: &mut [Pt],
) {
for (row_idx, row) in rows.iter().enumerate() {
for (cell_ci, cell) in row.cells.iter().enumerate() {
if cell.vertical_merge != Some(VerticalMergeState::Restart) {
continue;
}
let entry = &row_cell_layouts[row_idx][cell_ci];
let content_h = entry.layout.content_height + cell.margins.vertical();
let mut last_merged_row = row_idx;
for (r, row_below) in rows.iter().enumerate().skip(row_idx + 1) {
if is_vmerge_continue(row_below, entry.grid_col) {
last_merged_row = r;
} else {
break;
}
}
if last_merged_row == row_idx {
continue;
}
let spanned: Pt = row_heights[row_idx..=last_merged_row].iter().copied().sum();
if content_h > spanned {
let overflow = content_h - spanned;
let num_rows = (last_merged_row - row_idx + 1) as f32;
let per_row = overflow / num_rows;
for h in &mut row_heights[row_idx..=last_merged_row] {
*h += per_row;
}
}
}
}
}
pub(super) fn find_cell_at_grid_col(
row: &TableRowInput,
target_grid_col: usize,
) -> Option<&TableCellInput> {
let mut col = row.grid_before as usize;
if target_grid_col < col {
return None;
}
for cell in &row.cells {
let span = cell.grid_span.max(1) as usize;
if target_grid_col < col + span {
return Some(cell);
}
col += span;
}
None
}
pub(super) fn is_vmerge_continue(row: &TableRowInput, grid_col: usize) -> bool {
find_cell_at_grid_col(row, grid_col)
.is_some_and(|c| c.vertical_merge == Some(VerticalMergeState::Continue))
}
pub(super) fn cell_index_at_grid_col(row: &TableRowInput, target_grid_col: usize) -> Option<usize> {
let mut col = row.grid_before as usize;
if target_grid_col < col {
return None;
}
for (i, cell) in row.cells.iter().enumerate() {
let span = cell.grid_span.max(1) as usize;
if target_grid_col < col + span {
return Some(i);
}
col += span;
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn equal_distribution_when_no_grid() {
let widths = compute_column_widths(&[], 3, Pt::new(300.0));
assert_eq!(widths.len(), 3);
assert_eq!(widths[0].raw(), 100.0);
assert_eq!(widths[1].raw(), 100.0);
assert_eq!(widths[2].raw(), 100.0);
}
#[test]
fn grid_cols_scaled_to_fit() {
let grid = vec![Pt::new(100.0), Pt::new(200.0)];
let widths = compute_column_widths(&grid, 2, Pt::new(600.0));
assert_eq!(widths[0].raw(), 200.0);
assert_eq!(widths[1].raw(), 400.0);
}
#[test]
fn grid_cols_already_fit() {
let grid = vec![Pt::new(150.0), Pt::new(150.0)];
let widths = compute_column_widths(&grid, 2, Pt::new(300.0));
assert_eq!(widths[0].raw(), 150.0);
assert_eq!(widths[1].raw(), 150.0);
}
#[test]
fn zero_cols_empty_result() {
let widths = compute_column_widths(&[], 0, Pt::new(300.0));
assert!(widths.is_empty());
}
#[test]
fn zero_summing_grid_falls_back_to_equal_distribution() {
let grid = vec![Pt::ZERO, Pt::ZERO, Pt::ZERO];
let widths = compute_column_widths(&grid, 3, Pt::new(300.0));
assert_eq!(
widths.iter().map(|w| w.raw()).collect::<Vec<_>>(),
vec![100.0, 100.0, 100.0]
);
}
#[test]
fn negative_grid_total_falls_back_to_equal_distribution() {
let grid = vec![Pt::new(-40.0), Pt::new(10.0)];
let widths = compute_column_widths(&grid, 2, Pt::new(300.0));
assert_eq!(
widths.iter().map(|w| w.raw()).collect::<Vec<_>>(),
vec![150.0, 150.0],
"no column may come out negative"
);
}
#[test]
fn zero_summing_grid_uses_its_own_length_when_num_cols_is_zero() {
let grid = vec![Pt::ZERO, Pt::ZERO];
let widths = compute_column_widths(&grid, 0, Pt::new(300.0));
assert_eq!(
widths.iter().map(|w| w.raw()).collect::<Vec<_>>(),
vec![150.0, 150.0]
);
}
use super::super::types::CellVAlign;
use crate::render::geometry::PtEdgeInsets;
fn empty_cell(grid_span: u32) -> TableCellInput {
TableCellInput {
blocks: Vec::new(),
margins: PtEdgeInsets::ZERO,
grid_span,
shading: None,
cell_borders: None,
vertical_merge: None,
vertical_align: CellVAlign::Top,
}
}
fn row_with_offsets(cells: Vec<TableCellInput>, grid_before: u32) -> TableRowInput {
TableRowInput {
cells,
height_rule: None,
is_header: None,
cant_split: None,
grid_before,
border_overrides: None,
}
}
#[test]
fn find_cell_skips_grid_before() {
let row = row_with_offsets(vec![empty_cell(1), empty_cell(1)], 1);
assert!(find_cell_at_grid_col(&row, 0).is_none());
assert!(find_cell_at_grid_col(&row, 1).is_some());
assert!(find_cell_at_grid_col(&row, 2).is_some());
assert!(find_cell_at_grid_col(&row, 3).is_none());
}
#[test]
fn cell_index_at_grid_col_skips_grid_before() {
let row = row_with_offsets(vec![empty_cell(1), empty_cell(1)], 1);
assert_eq!(cell_index_at_grid_col(&row, 0), None);
assert_eq!(cell_index_at_grid_col(&row, 1), Some(0));
assert_eq!(cell_index_at_grid_col(&row, 2), Some(1));
assert_eq!(cell_index_at_grid_col(&row, 3), None);
}
#[test]
fn find_cell_with_grid_span_after_grid_before() {
let row = row_with_offsets(vec![empty_cell(2)], 2);
assert!(find_cell_at_grid_col(&row, 0).is_none());
assert!(find_cell_at_grid_col(&row, 1).is_none());
assert!(find_cell_at_grid_col(&row, 2).is_some());
assert!(find_cell_at_grid_col(&row, 3).is_some());
assert!(find_cell_at_grid_col(&row, 4).is_none());
}
use super::super::types::MeasuredRow;
use crate::render::layout::cell::CellLayout;
use crate::render::layout::section::LayoutBlock;
fn merged_cell(vmerge: Option<VerticalMergeState>) -> TableCellInput {
TableCellInput {
vertical_merge: vmerge,
..empty_cell(1)
}
}
fn plain_row(cells: Vec<TableCellInput>) -> TableRowInput {
row_with_offsets(cells, 0)
}
fn measured(rows: &[(f32, f32)]) -> MeasuredTable {
MeasuredTable {
rows: rows
.iter()
.map(|&(h, gap)| MeasuredRow {
entries: Vec::new(),
borders: Vec::new(),
height: Pt::new(h),
leading_gap: Pt::ZERO,
border_gap_below: Pt::new(gap),
})
.collect(),
table_width: Pt::new(100.0),
}
}
#[test]
fn row_group_end_is_one_row_when_nothing_continues() {
let rows = vec![
plain_row(vec![merged_cell(None)]),
plain_row(vec![merged_cell(None)]),
];
assert_eq!(row_group_end(&rows, 0), 1);
assert_eq!(row_group_end(&rows, 1), 2, "last row still yields len");
}
#[test]
fn row_group_end_spans_consecutive_continue_rows() {
let rows = vec![
plain_row(vec![merged_cell(Some(VerticalMergeState::Restart))]),
plain_row(vec![merged_cell(Some(VerticalMergeState::Continue))]),
plain_row(vec![merged_cell(Some(VerticalMergeState::Continue))]),
plain_row(vec![merged_cell(None)]),
];
assert_eq!(row_group_end(&rows, 0), 3, "restart + two continues");
assert_eq!(row_group_end(&rows, 3), 4);
}
#[test]
fn row_group_end_triggers_on_a_continue_in_any_column() {
let rows = vec![
plain_row(vec![merged_cell(None), merged_cell(None)]),
plain_row(vec![
merged_cell(None),
merged_cell(Some(VerticalMergeState::Continue)),
]),
];
assert_eq!(row_group_end(&rows, 0), 2);
}
#[test]
fn build_row_groups_treats_plain_rows_as_separate_splittable_groups() {
let rows = vec![
plain_row(vec![merged_cell(None)]),
plain_row(vec![merged_cell(None)]),
];
let groups = build_row_groups(&rows, &measured(&[(20.0, 0.0), (30.0, 0.0)]));
assert_eq!(groups.len(), 2);
assert!(groups.iter().all(|g| g.splittable));
assert_eq!(groups[0].height.raw(), 20.0);
assert_eq!(groups[1].height.raw(), 30.0);
}
#[test]
fn build_row_groups_height_includes_the_border_gap_below() {
let rows = vec![plain_row(vec![merged_cell(None)])];
let groups = build_row_groups(&rows, &measured(&[(20.0, 3.0)]));
assert_eq!(groups[0].height.raw(), 23.0);
}
#[test]
fn build_row_groups_marks_a_vmerge_span_unsplittable() {
let rows = vec![
plain_row(vec![merged_cell(Some(VerticalMergeState::Restart))]),
plain_row(vec![merged_cell(Some(VerticalMergeState::Continue))]),
];
let groups = build_row_groups(&rows, &measured(&[(20.0, 0.0), (20.0, 0.0)]));
assert_eq!(groups.len(), 1);
assert_eq!((groups[0].start, groups[0].end), (0, 2));
assert_eq!(groups[0].height.raw(), 40.0, "span height is the sum");
assert!(!groups[0].splittable);
}
#[test]
fn build_row_groups_marks_a_cant_split_row_unsplittable() {
let mut row = plain_row(vec![merged_cell(None)]);
row.cant_split = Some(true);
let groups = build_row_groups(&[row], &measured(&[(20.0, 0.0)]));
assert!(!groups[0].splittable);
}
#[test]
fn build_row_groups_marks_a_nested_table_row_unsplittable() {
let nested = TableCellInput {
blocks: vec![LayoutBlock::Table {
rows: Vec::new(),
col_widths: Vec::new(),
cell_spacing: Pt::ZERO,
border_config: None,
indent: Pt::ZERO,
alignment: None,
float_info: None,
style_id: None,
}],
..empty_cell(1)
};
let groups = build_row_groups(&[plain_row(vec![nested])], &measured(&[(20.0, 0.0)]));
assert!(!groups[0].splittable);
}
fn layout_entry(content_height: f32, grid_col: usize) -> CellLayoutEntry {
CellLayoutEntry {
layout: CellLayout {
commands: Vec::new(),
content_height: Pt::new(content_height),
lines: Vec::new(),
},
cell_x: Pt::ZERO,
cell_w: Pt::new(100.0),
grid_col,
}
}
#[test]
fn expand_spreads_overflow_across_the_merge_span() {
let rows = vec![
plain_row(vec![merged_cell(Some(VerticalMergeState::Restart))]),
plain_row(vec![merged_cell(Some(VerticalMergeState::Continue))]),
];
let layouts = vec![vec![layout_entry(84.0, 0)], vec![layout_entry(0.0, 0)]];
let mut heights = [Pt::new(14.0), Pt::new(14.0)];
expand_rows_for_vmerge(&rows, &layouts, &mut heights);
assert_eq!(heights[0].raw(), 42.0);
assert_eq!(heights[1].raw(), 42.0);
assert_eq!(
heights[0].raw() + heights[1].raw(),
84.0,
"the span must total the restart cell's content height"
);
}
#[test]
fn expand_leaves_heights_alone_when_the_content_already_fits() {
let rows = vec![
plain_row(vec![merged_cell(Some(VerticalMergeState::Restart))]),
plain_row(vec![merged_cell(Some(VerticalMergeState::Continue))]),
];
let layouts = vec![vec![layout_entry(10.0, 0)], vec![layout_entry(0.0, 0)]];
let mut heights = [Pt::new(30.0), Pt::new(30.0)];
expand_rows_for_vmerge(&rows, &layouts, &mut heights);
assert_eq!(heights[0].raw(), 30.0);
assert_eq!(heights[1].raw(), 30.0);
}
#[test]
fn expand_does_nothing_for_a_lone_restart() {
let rows = vec![plain_row(vec![merged_cell(Some(
VerticalMergeState::Restart,
))])];
let layouts = vec![vec![layout_entry(84.0, 0)]];
let mut heights = [Pt::new(14.0)];
expand_rows_for_vmerge(&rows, &layouts, &mut heights);
assert_eq!(
heights[0].raw(),
14.0,
"a one-row span is not expanded — measure.rs must size this row"
);
}
#[test]
fn expand_matches_continues_by_grid_column_not_cell_index() {
let rows = vec![
plain_row(vec![
merged_cell(Some(VerticalMergeState::Restart)), merged_cell(None), ]),
row_with_offsets(vec![merged_cell(Some(VerticalMergeState::Continue))], 1),
];
let layouts = vec![
vec![layout_entry(84.0, 0), layout_entry(0.0, 1)],
vec![layout_entry(0.0, 1)],
];
let mut heights = [Pt::new(14.0), Pt::new(14.0)];
expand_rows_for_vmerge(&rows, &layouts, &mut heights);
assert_eq!(
[heights[0].raw(), heights[1].raw()],
[14.0, 14.0],
"the Continue is in column 1; column 0's restart has no continuation"
);
}
}